💅 打造最优雅的主题系统
代码仓库
几乎所有完善的组件库都有一个强大的主题系统,开发者对主题进行个性化的配置就可以改变整个组件库的风格。除了组件库,主题系统在Web应用中也很重要,一个好的主题系统可以帮助我们:
- 减少零碎的样式,有利于项目维护;
- 提高应用的一致性;
- 统一管理全局样式,减少心智压力。
这篇记录了我在开发组件库时在主题系统上做的研究和总结。阅读这篇文章需要良好的CSS基础和一些CSS预处理器的知识。
设计哲学
主题系统的目标并不是编译出一个CSS文件供其他项目使用,而是提供基本能力和封装好的逻辑供项目使用。我们知道,“灵活”与“易于使用”往往是冲突的。好的主题系统在于平衡了两者,是渐进式的。在多人协作开发的场景中,对项目不了解的开发者可以直接上手使用,了解比较深入的开发者可以做出灵活的配置。
下面我们深入浅出地说一说,如何打造一个基本的主题系统,并且针对不同类型的项目做出扩展。
技术栈
因为CSS是没有逻辑的,预处理器是我们实现主题系统的基础。选择什么预处理器都是可以的,在这篇文章中将用Sass
作示例讲解。
CSS变量已经不能算是一个新技术了,除了不再维护的IE浏览器外,几乎所有的浏览器都可以良好地支持。使用CSS变量最大的好处是,我们可以在媒体查询中或用js动态改变CSS变量的值从而改变页面样式。在可变主题的场景中,这是非常方便的。
让主题变得可配置
我们知道,预处理器本质上是对字符串进行语法分析进而直接生成CSS代码的。在编译之前,我们可以覆盖原代码的某一部分,编译后就可以得到全新的CSS代码。
如上面的图所示,在Scss代码中,我们会提供一些默认的变量如colors
, 还会写一些通用的处理逻辑。经过Sass的编译会形成可用的CSS代码,编译之前将默认变量替换成新的值即可生成新的主题,这是绝大多数组件库主题系统的可配置方案。
使用CSS变量配置主题
直接替换Sass变量虽然很方便,但是如果想实现可更改配色的Web应用,会变得非常棘手。因为改变Sass变量意味着要重新编译整个应用的CSS文件,开发者最多可以做到预设几套配色,但要想做到动态配色,开销会非常大。
所以我们的主题系统优先识别CSS变量,在CSS变量无效的情况下回退到默认配色。这样开发者既可以直接修改Sass变量编译默认主题,在主题发布后还可以修改CSS变量来更改配色。
使用js可以动态修改CSS变量的值,这为实时配色预览提供了解决方案。
编码原则
众所周知,sass是逐行编译的,它在编译时不知道后面的内容,只能根据已编译的内容来判断输出的内容。因为这个特性,我们声明变量,函数和一些其他工具时,顺序非常重要。举个例子,如果我们在上面的例子中,这样引入主题:
@import './theme.scss';
$primary-color: red;
很遗憾,颜色将无法被覆盖。因为sass读取到我们所写的primary-color
时,theme.scss
已经被编译完成了。
将所有的代码都写到一个文件中可以避免因为引入顺序带来的问题,但是会带来心智负担。我们的解决方案是:将不同类型的代码分别写在不同的scss文件并且对这些文件分级,确立依赖关系:
级别 | 类型 | 说明 |
---|---|---|
0 | variables 基础变量 |
可配置,可替换的变量,变量的值由开发者手写。 |
0 | utils 基础工具 |
包括纯净的函数和mixins。 |
1 | computed 计算属性 |
根据基础变量和基础工具计算出的变量集合。 |
2 | functions 函数、mixins 混入 |
常用逻辑的封装。 |
3 | inject 开发时需要注入每个组件/页面的声明合集 |
不会生成CSS代码,只提供声明。 |
4 | theme 主题的入口文件 |
生成全局CSS代码。 |
只要遵循:高级引入低级的原则即可。
现在,建立一个theme
文件夹并新建这些文件,然后跟随着文章一点一点地去填满他们。
./src/
├── index.html
├── index.js
├── style.scss
└── theme
├── computed.scss
├── functions.scss
├── inject.scss
├── mixins.scss
├── theme.scss
├── utils.scss
└── variables.scss
颜色管理
我们发现,不管是组件库,或者Web应用中,用到的颜色其实并不是很多,颜色太多华丽呼哨反而影响观感。我们将颜色划分为了主色与辅色。下面是常见的主色命名:
- primary
- secondary
- success
- warning
- danger
除了需要声明主色的基色,我们还需要指定主色的反色,当基色用于背景颜色时,反色可以突出内容。辅色一般是background
与text
的颜色。我们把这些颜色写入variables.scss
中作为默认的颜色:
// Default Colors
// ----------------------------------------------------------------
$colors: (
primary: (
base: #3880ff,
contrast: #fff
),
secondary: (
base: #3dc2ff,
contrast: #fff
),
success: (
base: #2dd36f,
contrast: #fff
),
warning: (
base: #ffc409,
contrast: #000
),
danger: (
base: #eb445a,
contrast: #fff
)
) !default;
$background-color: #fff !default;
$text-color: #000 !default;
// Other Default Values
// ----------------------------------------------------------------
$css-variable-prefix: "sys" !default;
$color-namespace: ".sys-color" !default;
变体
光凭这几种颜色一定是不够的,根据业务经验,我们可以为它们添加一些变体。
基色变体
对于基色来说,变体经常会用于实现一些颜色变换的动画效果,所以我们为每种基色定义了两个变体:
shade
加深,混合12%的黑色tint
变浅,混合10%的白色
在utils.scss
中声明用于计算的工具函数:
// Mixes a color with black to create its shade.
// ------------------------------------------------
@function get-color-shade($color) {
@return mix(#000, $color, 12%);
}
// Mixes a color with white to create its tint.
// ------------------------------------------------
@function get-color-tint($color) {
@return mix(#fff, $color, 10%);
}
// Converts a color to a comma separated rgb.
// --------------------------------------------------------------------------------------------
@function color-to-rgb-list($color) {
@return #{red($color)}, #{green($color)}, #{blue($color)};
}
// Generate a css variable name with custom prefix
// --------------------------------------------------------------------------------------------
@function get-var($name) {
@return "--sys-#{$name}";
}
在functions.scss
中声明获取颜色的函数
@import "./variables.scss";
@import "./utils.scss";
// Gets the specific color's css variable from the name and variation. Alpha/variation are optional.
// --------------------------------------------------------------------------------------------
// Example usage:
// get-color(primary, base) => rgba(var(--sys-color-primary-base-rgb, 56,128,255), 1)
// get-color(secondary, contrast) => rgba(var(--sys-color-primary-contrast-rgb, 255,255,255), 1)
// get-color(primary, base, 0.5) => rgba(var(--sys-color-primary-rgb, 56, 128, 255), 0.5)
// --------------------------------------------------------------------------------------------
@function get-color($key, $variation: base, $alpha: 1) {
$values: map-get($colors, $key);
$value: map-get($values, $variation);
$variable: get-var("color-#{$key}-#{$variation}");
@return rgba(var(#{$variable}, #{color-to-rgb-list($value)}), $alpha);
}
可以看出, 我们优先使用CSS变量的值,如果作用域中没有指定变量,将会回退到variables.scss
中配置的默认颜色。
辅色变体
系统的背景和字体颜色不是一成不变的,在绝大多数项目中,将两种辅色混合即可生成辅色变体,两种基本辅色所占比例不同,辅色变体的颜色也会出现差异。因为CSS本身无法处理混色,所以我们将混色的步长设为5%,并识别相应CSS变量。
在functions.scss
中声明获取辅色的函数
// Get the specific color's css variable from the step. Alpha is optional.
// --------------------------------------------------------------------------------------------
// Example usage:
// get-step-color() => rgba(var(--sys-color-background, 255, 255, 255), 1);
// get-step-color(0, 0.5) => rgba(var(--sys-color-background, 255, 255, 255), 0.5);
// get-step-color(1000) => rgba(var(--sys-color-text, 0, 0, 0), 1);
// get-step-color(50) => rgba(var(--sys-color-step-50, 242, 242, 242), 1);
// --------------------------------------------------------------------------------------------
@function get-step-color($step: 0, $alpha: 1) {
@if ($step == 1000) {
@return get-color-with-css-variable("color-text", $text-color, $alpha);
}
@if ($step == 0) {
@return get-color-with-css-variable(
"color-background",
$background-color,
$alpha
);
}
@if $step < 0 or $step > 1000 {
@error "Color step must be between 0 and 1000.";
@return null;
} @else if $step % 50 != 0 {
@error "Color step must be divisible by 50.";
@return null;
}
$value: mix($text-color, $background-color, $step / 10);
@return get-color-with-css-variable("color-step-#{$step}", $value, $alpha);
}
@function current-color($variation: "base", $alpha: 1) {
@return rgba(var(#{get-var("color-current-#{$variation}")}), $alpha);
}
编写样式时,尽量使用get-color
与get-step-color
函数表示颜色。
动态改变颜色
使用CSS变量最大的优势是:变量的值可被动态改变。举个例子:
我们可以利用这个特性,为每个颜色创建namespace class
,只要添加这些class,就可以动态改变表示颜色的变量,大大减少代码量。配合Sass,声明这些变量非常方便:
@mixin generate-color-namespace($key) {
$value: map-get($colors, $key);
$base: map-get($value, base);
$contrast: map-get($value, contrast);
$shade: get-color-shade($base);
$tint: get-color-tint($base);
#{$color-namespace}.#{$key} {
#{get-var("color-current-base")}: #{color-to-rgb-list($base)};
#{get-var("color-current-contrast")}: #{color-to-rgb-list($contrast)};
#{get-var("color-current-shade")}: #{color-to-rgb-list($shade)};
#{get-var("color-current-tint")}: #{color-to-rgb-list($tint)};
}
}
@each $key, $value in $colors {
@include generate-color-namespace($key);
}
.sys-color.primary {
--sys-color-current-base:56, 128, 255;
--sys-color-current-contrast:255, 255, 255;
--sys-color-current-shade:49, 113, 224;
--sys-color-current-tint:76, 141, 255;
}
.sys-color.secondary {
--sys-color-current-base:61, 194, 255;
--sys-color-current-contrast:255, 255, 255;
--sys-color-current-shade:54, 171, 224;
--sys-color-current-tint:80, 200, 255;
}
.sys-color.success {
--sys-color-current-base:45, 211, 111;
--sys-color-current-contrast:255, 255, 255;
--sys-color-current-shade:40, 186, 98;
--sys-color-current-tint:66, 215, 125;
}
.sys-color.warning {
--sys-color-current-base:255, 196, 9;
--sys-color-current-contrast:0, 0, 0;
--sys-color-current-shade:224, 172, 8;
--sys-color-current-tint:255, 202, 34;
}
.sys-color.danger {
--sys-color-current-base:235, 68, 90;
--sys-color-current-contrast:255, 255, 255;
--sys-color-current-shade:207, 60, 79;
--sys-color-current-tint:237, 87, 107;
}
在functions.scss
中编写获取current-color
的函数:
// Get the current color's css variable from the variation. Alpha/variation are optional.
// --------------------------------------------------------------------------------------------
// Example usage:
// current-color() => rgba(var(--sys-color-current-base), 1);
// current-color(contrast) => rgba(var(--sys-color-current-contrast), 1);
// current-color(base, 0.5) => rgba(var(--sys-color-current-base), 0.5);
// --------------------------------------------------------------------------------------------
@function current-color($variation: "base", $alpha: 1) {
@return rgba(var(#{get-var("color-current-#{$variation}")}), $alpha);
}
注意:此方法完全依赖CSS变量并且没有回退方案,在不支持CSS变量的浏览器中将出现异常。
其他
除颜色外,主题系统还可管理属性,如边框样式,阴影样式,屏幕尺寸等等。与颜色管理相同,只要按照上述的方法组织这些属性即可。
快速开始
使用下面的命令生成主题系统的基本文件
# 在目标文件夹下声明主题文件
$ npx sys-theme ./theme