likes
comments
collection
share

3、样式系统和Design Token -- 渐进式vue3的组件库通关秘籍

作者站长头像
站长
· 阅读数 51

1、 给世界增加点样式吧

世界是多样性的,时刻在发生在变化,有的人的世界安静祥和,有的人的世界则水深火热。

接下来我们将给前一节的 hellow world组件增加一些样式,来反映世界的多样性。

在这之前,我们需要先定义组件的功能。

  • 展示一个固定宽高的世界,默认类型为 normal
  • 所有的世界类型为:normal、peace、danger、bigger,不同类型的世界有不同的样式。通过组件传参设定。

接下来开始实现这一功能吧。

2 世界类型设定

新建 components/hello-world/type.ts 文件,来存放本组件的所有自定义类型。后续所有的组件类型我们也将通过此文件设定。

export enum WorldType {
  NORMAL = 'normal',
  PEACE = 'peace',
  DANGER = 'danger',
  BIGGER = 'bigger',
}

这里我们使用了一个枚举类型,设定了世界的四种类型,normal、peace、danger、bigger。

新建 components/hello-world/styles/index.css 文件,来给不同类型的世界增加一点样式。

.world {
  height: 100px;
  width: 100px;
  font-size: 16px;
  background-color: #fff;
  color: #000;
  text-align: center;
  display: flex;
  justify-content: center;
  align-items: center;
}
.world-danger {
  background-color: #ff4d4f;
  color: #fff;
}
.world-peace {
  background-color: green;
  color: #fff;
  border-radius: 8px;
}
.world-bigger {
  background-color: green;
  color: #fff;
  border-radius: 16px;
  height: 200px;
  width: 200px;
}

最后一步,修改 components/hello-world/hello-world.tsx 文件。

  • 加入props.type 参数
  • 根据不同的类型,挂载不同的样式,显示不同的文字。
import { computed, defineComponent, toRef, type PropType } from 'vue';
// 引入组件的样式
import './styles/index.css';
import { WorldType } from './type';
export default defineComponent({
  props: {
    // 给组件加入参数type,jsx不能通过defineProps设定参数
    type: {
      default: WorldType.NORMAL,
      type: String as PropType<WorldType>,
    },
  },
  setup(props) {
    // 将type转换为响应式对象,方便计算属性使用
    const worldType = toRef(props.type);

    const worldMsg = computed(() => {
      switch (worldType.value) {
        case WorldType.NORMAL:
          return 'boring world';
        case WorldType.PEACE:
          return 'hello world';
        case WorldType.DANGER:
          return 'danger world';
        case WorldType.BIGGER:
          return '广阔天地、大有作为';
        default:
          return 'world 404~~';
      }
    });
    const render = () => {
      return (
        <>
          <div class={[`world-${worldType.value}`, 'world']}>{worldMsg.value}</div>
        </>
      );
    };
    return render;
  },
});

然后修改一下组件的使用文档 docs/components/hello-world.md ,说明一下如何使用。

---
outline: deep
---

# hello world

say hello to the world!

## 代码演示

世界类型有四种:normal、peace、danger、bigger

<script setup>
import { HelloWorld } from '../../components' 
</script>

<div :class="$style['world-container']">
    <HelloWorld type='normal' />
    <HelloWorld type='peace' />
    <HelloWorld type='danger' />
    <HelloWorld type='bigger' />
</div>

<style module>
.world-container {
  display:flex;
  justify-content:space-around;
}
</style>

```vue
<script setup>
import { HelloWorld } from '../../components';
</script>

<HelloWorld />

<div :class="$style['world-container']">
    <HelloWorld type='normal' />
    <HelloWorld type='peace' />
    <HelloWorld type='danger' />
    <HelloWorld type='bigger' />
</div>

<style module>
.world-container {
  display: flex;
  justify-content: space-around;
  margin-top: 20px;
}
</style>
```

## API

通过设置世界不同的类型来展示不同的样式和文字。

| 属性 | 说明       | 类型   | 默认值 |
| ---- | ---------- | ------ | ------ |
| type | 世界的类型 | String | normal |

最终效果如下:

3、样式系统和Design Token -- 渐进式vue3的组件库通关秘籍

这里我们完成了给 ** hello world** 组件样式和逻辑的完善,能够通过不同的type传参,展示不同的样式和文字。

但是,我们可以看到,目前组件的样式都是写死的,比如不同类型的背景色、字号、圆角大小等,样式的可定制性极差。同时,我们知道组件库一般都是有固定的风格的,术语叫主色或者品牌色。不同人的世界也是有不同的主题色的,有的人出生就在罗马,而俺就是牛马(开玩笑)。

所以,我们想,有没有一种方法,将组件通用的样式设定抽离出来复用,以方便主题的可定制化改造。尽可能的减少开发者使用的难度。

目前大多数解决方案呢就是使用 css变量,将组件库的主题风格和其他的样式抽离出来,在一个独立的文件中维护。

3、 主题样式+ Design Token

如果我们要使用css变量来统一管理组件库的样式和风格,那么有哪些方面的变量要抽离出来呢?

参考各大品牌的设计系统,在大类上我们参考抽离出来以下类型:

  • Color 颜色
  • Shadow 投影、
  • Typography 字体样式
  • Size 尺寸
  • Space 间距
  • Radius 描边圆角
  • Border Width 描边宽度
  • Opacity透明度

OK,再开始实践之前,先给我们的组件库确定一个主题风格吧,也就是主题色。这一工作通常由设计师进行完成,我们这里参考TDesign, 选择#0052d9作为我们的主题色。

接下来,就开始我们的主题样式构建吧。

3.1 样式变量

新建 components/themes/vars/index.css。目前我们只需要以下几种样式就行了。

  • 颜色 - 普通背景色,peace背景色(主题色),危险背景色,相对应的文字颜色
  • 字体,字号
  • 圆角
  • padding,margin

内容参考如下:

:root {
  /* 品牌色,也可以叫primary */
  --fu-brand-color: #0052d9;

  /* 危险主色 */
  --fu-error-color: #d54941;

  /* 成功主色 */
  --fu-success-color: #2ba471;
}

我们先设置这三种颜色,可以看到,通过css变量,我们给每个颜色取了一个语义化的名字,比如 success-color,其中fu是我们组件库名称的缩写。而这个语义化的名称就是我们所谓的 Design Token 。根据颗粒度的不同,可以分为全局Token组件Token。上面的就是我们的全局Token

接下来为我们的Hello world组件完成组件Token的构建。

新建 components/hello-world/styles/token.css

@import url('../../themes/vars/index.css');
:root {
  --color-world-bg-primary: var(--fu-brand-color);
  --color-world-bg-success: var(--fu-success-color);
  --color-world-bg-error: var(--fu-error-color);
}

这里我们新建了三个组件Token,关于token的命名规范可以参考下图,图片来自

3、样式系统和Design Token -- 渐进式vue3的组件库通关秘籍

然后在 components/hello-world/styles/index.css中引入这个文件并修改相关样式即可。

@import url('./token.css');
.world {
  height: 100px;
  width: 100px;
  font-size: 16px;
  background-color: var(--color-world-bg-primary);
  color: #fff;
  text-align: center;
  display: flex;
  justify-content: center;
  align-items: center;
}
.world-danger {
  background-color: var(--color-world-bg-error);
  color: #fff;
}
.world-peace {
  background-color: var(--color-world-bg-success);
  color: #fff;
  border-radius: 8px;
}
.world-bigger {
  background-color: var(--color-world-bg-success);
  color: #fff;
  border-radius: 16px;
  height: 200px;
  width: 200px;
}

保存预览文档可以看到,我们的样式如期生效了。这里我们将 normal 的世界设置为了主色。

3、样式系统和Design Token -- 渐进式vue3的组件库通关秘籍

其它的比如文字颜色,padding,margin可以同理设置。这里我们先不演示,我们先来看一些目前组件样式的一些问题。

  • 样式无法嵌套
  • 样式的前缀重复,能不能统一设置
  • 其它目前未遇到问题

这里我们引入css预处理器Less来增强我们css的功能。

3.2 引入Less

Less是一种CSS预处理器,与传统的CSS相比有一些优势:

  1. 变量和混合(Mixins): Less允许定义变量和混合,使得在整个样式表中的重复使用更为便捷,同时也方便了样式的维护和更新。
  2. 嵌套规则: Less支持嵌套规则,可以更清晰地组织样式结构,减少了选择器的嵌套层级,提高了代码的可读性。
  3. 算术运算和函数: Less允许在样式表中使用算术运算和函数,使得样式的计算更加灵活,可以动态地生成样式,提高了开发效率。
  4. 模块化和可重用性: Less的模块化特性使得样式表可以分成多个文件,便于管理和组织,同时也增加了样式的可重用性和可维护性。
  5. 跨浏览器兼容性: Less可以编译成普通的CSS文件,在不支持Less的环境下也可以正常使用,保证了样式的跨浏览器兼容性

使用less我们可以更好的模块化的维护主题样式

安装依赖

// 安装less
npm i less-loader less --save-dev

修改 components/hello-world/styles/index.css => components/hello-world/styles/index.less

@import './token.less';
.world {
  height: 100px;
  width: 100px;
  font-size: 16px;
  background-color: @color-world-bg-primary;
  color: #fff;
  text-align: center;
  display: flex;
  justify-content: center;
  align-items: center;
}
.world-danger {
  background-color: @color-world-bg-error;
  color: #fff;
}
.world-peace {
  background-color: @color-world-bg-success;
  color: #fff;
  border-radius: 8px;
}
.world-bigger {
  background-color: @color-world-bg-success;
  color: #fff;
  border-radius: 16px;
  height: 200px;
  width: 200px;
}

修改 components/hello-world/styles/token.css => components/hello-world/styles/token.less

@import '../../themes/vars/index.less';

//
@color-world-bg-primary: var(--fu-brand-color);
@color-world-bg-success: var(--fu-success-color);
@color-world-bg-danger: var(--fu-danger-color);

修改 components/themes/vars/index.css => components/themes/vars/index.less, 内容不变。

最后修改组件代码的样式引入即可,

import { computed, defineComponent, toRef, type PropType } from 'vue';
import './styles/index.less'; // 修改一下这里
import { WorldType } from './type';
export default defineComponent({
  props: {
    // 给组件加入参数type,jsx不能通过defineProps设定参数
    type: {
      default: WorldType.NORMAL,
      type: String as PropType<WorldType>,
    },
  },
  setup(props) {
    const worldType = toRef(props.type);

    const worldMsg = computed(() => {
      switch (worldType.value) {
        case WorldType.NORMAL:
          return 'boring world';
        case WorldType.PEACE:
          return 'hello world';
        case WorldType.DANGER:
          return 'danger world';
        case WorldType.BIGGER:
          return '广阔天地、大有作为';
        default:
          return 'world 404~~';
      }
    });
    const render = () => {
      return (
        <>
          <div class={[`world-${worldType.value}`, 'world']}>{worldMsg.value}</div>
        </>
      );
    };
    return render;
  },
});

在文档里面预览组件看一下吧 。

3.2 色板

上一小节我们可以看到,我们在index.css文件里面引入了三个类型的主色,颜色是固定的。但是,通常情况下,我们需要根据主色,向此颜色的周围衍生出其它的辅助色,比如文字颜色;hover、disbled的状态色,是需要和主色风格统一的,所以就引入了色板这一概念,如果需要修改主题风格,只需要修改基础的色板就可以了,非常的方便。这里我们就补充出着三个主色的色板。

components/themes/vars/index.less

:root {
  /* 品牌色色板 */
  --fu-brand-color-1: #f2f3ff;
  --fu-brand-color-2: #d9e1ff;
  --fu-brand-color-3: #b5c7ff;
  --fu-brand-color-4: #8eabff;
  --fu-brand-color-5: #618dff;
  --fu-brand-color-6: #366ef4;
  --fu-brand-color-7: #0052d9;
  --fu-brand-color-8: #003cab;
  --fu-brand-color-9: #002a7c;
  --fu-brand-color-10: #001a57;

  /* 错误色板 */
  --fu-error-color-1: #fff0ed;
  --fu-error-color-2: #ffd8d2;
  --fu-error-color-3: #ffb9b0;
  --fu-error-color-4: #ff9285;
  --fu-error-color-5: #f6685d;
  --fu-error-color-6: #d54941;
  --fu-error-color-7: #ad352f;
  --fu-error-color-8: #881f1c;
  --fu-error-color-9: #68070a;
  --fu-error-color-10: #490002;

  /* 成功主色 */
  --fu-success-color-1: #e3f9e9;
  --fu-success-color-2: #c6f3d7;
  --fu-success-color-3: #92dab2;
  --fu-success-color-4: #56c08d;
  --fu-success-color-5: #2ba471;
  --fu-success-color-6: #008858;
  --fu-success-color-7: #006c45;
  --fu-success-color-8: #005334;
  --fu-success-color-9: #003b23;
  --fu-success-color-10: #002515;

  // 基础颜色,通过色板选取颜色
  --fu-brand-color: var(--fu-brand-color-7);
  --fu-error-color: var(--fu-error-color-6);
  --fu-success-color: var(--fu-success-color-6);
}

保存预览一下文档,可以看到,组件按照预期工作了。

3.3 深色模式

下面为我们的组件库增加深色模式的适配吧。

新增 components/themes/theme/_light.less 存放浅色模式的配置,这里我们命名以 _ 下划线开通,表示为样式内部使用,不提供组件,组件使用的样式通过非下划线开头的文件引入。

:root,:root[theme-mode="light"] {
  /* 品牌色色板 */
  --fu-brand-color-1: #f2f3ff;
  --fu-brand-color-2: #d9e1ff;
  --fu-brand-color-3: #b5c7ff;
  --fu-brand-color-4: #8eabff;
  --fu-brand-color-5: #618dff;
  --fu-brand-color-6: #366ef4;
  --fu-brand-color-7: #0052d9;
  --fu-brand-color-8: #003cab;
  --fu-brand-color-9: #002a7c;
  --fu-brand-color-10: #001a57;

  /* 错误色板 */
  --fu-error-color-1: #fff0ed;
  --fu-error-color-2: #ffd8d2;
  --fu-error-color-3: #ffb9b0;
  --fu-error-color-4: #ff9285;
  --fu-error-color-5: #f6685d;
  --fu-error-color-6: #d54941;
  --fu-error-color-7: #ad352f;
  --fu-error-color-8: #881f1c;
  --fu-error-color-9: #68070a;
  --fu-error-color-10: #490002;

  /* 成功主色 */
  --fu-success-color-1: #e3f9e9;
  --fu-success-color-2: #c6f3d7;
  --fu-success-color-3: #92dab2;
  --fu-success-color-4: #56c08d;
  --fu-success-color-5: #2ba471;
  --fu-success-color-6: #008858;
  --fu-success-color-7: #006c45;
  --fu-success-color-8: #005334;
  --fu-success-color-9: #003b23;
  --fu-success-color-10: #002515;

  // 基础颜色,通过色板选取颜色
  --fu-brand-color: var(--fu-brand-color-7);
  --fu-error-color: var(--fu-error-color-6);
  --fu-success-color: var(--fu-success-color-6);
}

新增 components/themes/theme/_dark.less 存放浅色模式的配置

:root,
:root[theme-mode='dark'] {
  /* 品牌色色板 */
  --fu-brand-color-1: #1b2f51;
  --fu-brand-color-2: #173463;
  --fu-brand-color-3: #143975;
  --fu-brand-color-4: #103d88;
  --fu-brand-color-5: #0d429a;
  --fu-brand-color-6: #054bbe;
  --fu-brand-color-7: #2667d4;
  --fu-brand-color-8: #4582e6;
  --fu-brand-color-9: #699ef5;
  --fu-brand-color-10: #96bbf8;

  /* 错误色板 */
  --fu-error-color-1: #472324;
  --fu-error-color-2: #5e2a2d;
  --fu-error-color-3: #703439;
  --fu-error-color-4: #83383e;
  --fu-error-color-5: #a03f46;
  --fu-error-color-6: #c64751;
  --fu-error-color-7: #de6670;
  --fu-error-color-8: #ec888e;
  --fu-error-color-9: #edb1b6;
  --fu-error-color-10: #eeced0;

  /* 成功主色 */
  --fu-success-color-1: #193a2a;
  --fu-success-color-2: #1a4230;
  --fu-success-color-3: #17533d;
  --fu-success-color-4: #0d7a55;
  --fu-success-color-5: #059465;
  --fu-success-color-6: #43af8a;
  --fu-success-color-7: #46bf96;
  --fu-success-color-8: #80d2b6;
  --fu-success-color-9: #b4e1d3;
  --fu-success-color-10: #deede8;

  // 基础颜色,通过色板选取颜色,这里和light的色号不一样的。
  --fu-brand-color: var(--fu-brand-color-8);
  --fu-error-color: var(--fu-error-color-6);
  --fu-success-color: var(--fu-success-color-5);
}

注意这里浅色和深色的success和brand,在色板上的取色的token是不一样的,所以我们不能直接使用色板的token。

新增 components/themes/theme/_index.less 混合所有主题配置。

@import './_dark.less';
@import './_light.less';

为了使用上的统一性,我们将所有的全局Token使用less语法统一命名,将css vars包装成less这样无论是修改哪里的配置,引用的名称都是一样的。而且后续使用还可以利用less的高级特性。

新建 components/themes/vars/_var.less

// 颜色色板

@brand-color-1: var(--fu-brand-color-1);
@brand-color-2: var(--fu-brand-color-2);
@brand-color-3: var(--fu-brand-color-3);
@brand-color-4: var(--fu-brand-color-4);
@brand-color-5: var(--fu-brand-color-5);
@brand-color-6: var(--fu-brand-color-6);
@brand-color-7: var(--fu-brand-color-7);
@brand-color-8: var(--fu-brand-color-8);
@brand-color-9: var(--fu-brand-color-9);
@brand-color-10: var(--fu-brand-color-10);

@error-color-1: var(--fu-error-color-1);
@error-color-2: var(--fu-error-color-2);
@error-color-3: var(--fu-error-color-3);
@error-color-4: var(--fu-error-color-4);
@error-color-5: var(--fu-error-color-5);
@error-color-6: var(--fu-error-color-6);
@error-color-7: var(--fu-error-color-7);
@error-color-8: var(--fu-error-color-8);
@error-color-9: var(--fu-error-color-9);
@error-color-10: var(--fu-error-color-10);

@success-color-1: var(--fu-success-color-1);
@success-color-2: var(--fu-success-color-2);
@success-color-3: var(--fu-success-color-3);
@success-color-4: var(--fu-success-color-4);
@success-color-5: var(--fu-success-color-5);
@success-color-6: var(--fu-success-color-6);
@success-color-7: var(--fu-success-color-7);
@success-color-8: var(--fu-success-color-8);
@success-color-9: var(--fu-success-color-9);
@success-color-10: var(--fu-success-color-10);

// 基础颜色
@brand-color: var(--fu-brand-color); // 色彩-品牌-可操作
@error-color: var(--fu-error-color); // 色彩-功能-失败
@success-color: var(--fu-success-color); // 色彩-功能-成功

删除 components/themes/vars/index.less

新建 components/themes/base.css 将全局的Token引入,对外提供。

@import './theme/_index.less';
@import './vars/_var.less';

最后修改一下 components/hello-world/styles/token.less

@import '../../themes/base.less';

//
@color-world-bg-primary: @brand-color;
@color-world-bg-success: @success-color;
@color-world-bg-error: @error-color;

保存预览可以看到组件按照预期工作。

修改 docs/components/hello-world/.md 增加切换深色模式的逻辑。

...

<script setup>
import { HelloWorld } from '../../components'
import { ref } from 'vue'
const isDark = ref(false)
const onClick = function(){
    isDark.value = !isDark.value
    document.documentElement.setAttribute('theme-mode', isDark.value ? 'dark' : 'light');
}
</script>

<button @click=onClick>切换模式</button>

...

这里的模式切换的逻辑是在html上增加 theme-mode 属性

3、样式系统和Design Token -- 渐进式vue3的组件库通关秘籍

4、总结

本节我们完成了样式系统的设计,通过引入css varsDesign Token ,实现了组件库主题样式语义化和变量化,能够很方便的自定义主题样式。

目前还没有做的有以下几点,大家可以先思考一下如何去做:

  • 样式文件统一打包
  • webpack如何配置less的解析
  • 字体、边框等可以尝试去配置一下。
  • 如何在组件外部更方便的修改 design Token,比如只想修改某一个组件的某一个样式。

本节代码:feature_1.2_style

下一节:样式系统

欢迎点赞、欢迎star、欢迎讨论、一起学习。

5、参考阅读

转载自:https://juejin.cn/post/7367344206656536610
评论
请登录