HomeAuthorContactSearch

💅 打造最优雅的主题系统

代码仓库

几乎所有完善的组件库都有一个强大的主题系统,开发者对主题进行个性化的配置就可以改变整个组件库的风格。除了组件库,主题系统在Web应用中也很重要,一个好的主题系统可以帮助我们:

  • 减少零碎的样式,有利于项目维护;
  • 提高应用的一致性;
  • 统一管理全局样式,减少心智压力。


这篇记录了我在开发组件库时在主题系统上做的研究和总结。阅读这篇文章需要良好的CSS基础和一些CSS预处理器的知识。

设计哲学

主题系统的目标并不是编译出一个CSS文件供其他项目使用,而是提供基本能力和封装好的逻辑供项目使用。我们知道,“灵活”与“易于使用”往往是冲突的。好的主题系统在于平衡了两者,是渐进式的。在多人协作开发的场景中,对项目不了解的开发者可以直接上手使用,了解比较深入的开发者可以做出灵活的配置。

下面我们深入浅出地说一说,如何打造一个基本的主题系统,并且针对不同类型的项目做出扩展。

技术栈

因为CSS是没有逻辑的,预处理器是我们实现主题系统的基础。选择什么预处理器都是可以的,在这篇文章中将用Sass作示例讲解。

CSS变量已经不能算是一个新技术了,除了不再维护的IE浏览器外,几乎所有的浏览器都可以良好地支持。使用CSS变量最大的好处是,我们可以在媒体查询中或用js动态改变CSS变量的值从而改变页面样式。在可变主题的场景中,这是非常方便的。

让主题变得可配置

我们知道,预处理器本质上是对字符串进行语法分析进而直接生成CSS代码的。在编译之前,我们可以覆盖原代码的某一部分,编译后就可以得到全新的CSS代码。

PNG图像-C27FF6D23A0F-1.png
如上面的图所示,在Scss代码中,我们会提供一些默认的变量如colors, 还会写一些通用的处理逻辑。经过Sass的编译会形成可用的CSS代码,编译之前将默认变量替换成新的值即可生成新的主题,这是绝大多数组件库主题系统的可配置方案。

使用CSS变量配置主题

直接替换Sass变量虽然很方便,但是如果想实现可更改配色的Web应用,会变得非常棘手。因为改变Sass变量意味着要重新编译整个应用的CSS文件,开发者最多可以做到预设几套配色,但要想做到动态配色,开销会非常大。

所以我们的主题系统优先识别CSS变量,在CSS变量无效的情况下回退到默认配色。这样开发者既可以直接修改Sass变量编译默认主题,在主题发布后还可以修改CSS变量来更改配色。PNG图像-70BC2708DAB7-1.png
使用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应用中,用到的颜色其实并不是很多,颜色太多华丽呼哨反而影响观感。我们将颜色划分为了主色辅色。下面是常见的主色命名:

  1. primary
  2. secondary
  3. success
  4. warning
  5. danger


除了需要声明主色的基色,我们还需要指定主色的反色,当基色用于背景颜色时,反色可以突出内容。辅色一般是backgroundtext的颜色。我们把这些颜色写入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;

变体

光凭这几种颜色一定是不够的,根据业务经验,我们可以为它们添加一些变体。

基色变体

对于基色来说,变体经常会用于实现一些颜色变换的动画效果,所以我们为每种基色定义了两个变体:

  1. shade 加深,混合12%的黑色
  2. 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-colorget-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