3、样式系统和Design Token -- 渐进式vue3的组件库通关秘籍
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 |
最终效果如下:
这里我们完成了给 ** 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的命名规范可以参考下图,图片来自:
然后在 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 的世界设置为了主色。
其它的比如文字颜色,padding,margin可以同理设置。这里我们先不演示,我们先来看一些目前组件样式的一些问题。
- 样式无法嵌套
- 样式的前缀重复,能不能统一设置
- 其它目前未遇到问题
这里我们引入css预处理器Less来增强我们css的功能。
3.2 引入Less
Less是一种CSS预处理器,与传统的CSS相比有一些优势:
- 变量和混合(Mixins): Less允许定义变量和混合,使得在整个样式表中的重复使用更为便捷,同时也方便了样式的维护和更新。
- 嵌套规则: Less支持嵌套规则,可以更清晰地组织样式结构,减少了选择器的嵌套层级,提高了代码的可读性。
- 算术运算和函数: Less允许在样式表中使用算术运算和函数,使得样式的计算更加灵活,可以动态地生成样式,提高了开发效率。
- 模块化和可重用性: Less的模块化特性使得样式表可以分成多个文件,便于管理和组织,同时也增加了样式的可重用性和可维护性。
- 跨浏览器兼容性: 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 属性
4、总结
本节我们完成了样式系统的设计,通过引入css vars 和 Design Token ,实现了组件库主题样式语义化和变量化,能够很方便的自定义主题样式。
目前还没有做的有以下几点,大家可以先思考一下如何去做:
- 样式文件统一打包
- webpack如何配置less的解析
- 字体、边框等可以尝试去配置一下。
- 如何在组件外部更方便的修改 design Token,比如只想修改某一个组件的某一个样式。
本节代码:feature_1.2_style
下一节:样式系统
欢迎点赞、欢迎star、欢迎讨论、一起学习。
5、参考阅读
转载自:https://juejin.cn/post/7367344206656536610