前言
说到组件库,那可是一抓一大把。从Bootstrap开始,再到ElementUI,还有React系的AntD、Fusion……其实,如果你把它们都拉出来看一下,你会发现:它们基本上都是大同小异。最基本的组件,永远都是按钮、输入框、选择器那几个。除了技术栈和API之外,也就一些细节的地方不太一样:你有阴影,我有圆角。你是浅色,我是深色……
所以,为什么不能像软件换肤一样,只需要动动手指,就可以换掉圆角、换掉颜色、换掉大小,然后就是一套崭新的组件库?
于是,阿里巴巴就倒腾出了Fusion,它可以在站点上配置主题、生成主题包、生成自己的站点……一条龙服务。不过,本文可不是来推销Fusion的。我们就说说,现在的框架,都是怎么实现主题包的。
Less / Sass
这应该是目前最通用的方案了。AntD、ElementUI、Fusion都是这样做的。原理其实也很简单:它们定义了一系列的基本样式,如果单个组件被称为“分子”,那这些基本样式就被称为“原子”。它们不定义具体组件长啥样,它们定义了一系列标准,例如:
- 品牌色是什么颜色?
- 基础字号是多少?在它的基础上,再进行增加或减少,衍生出其他几种大小的文字,组成了不同标题的字号。
- 代表各个功能的功能色是什么?
这些定义并不会被直接使用,它们一般是用来给设计师参考用的。设计师在设计时,会尽可能以这些颜色作为标准,从而能让站点“花”而不乱。
接下来,组件会“拿出”自己需要的“原子”,组成自己的样式。例如,Fusion中按钮的样式,会有这么一段定义:
// Primary // ---------------------------------------- /// text /// @namespace statement/normal $btn-pure-primary-color: $color-white !default; /// text /// @namespace statement/hover $btn-pure-primary-color-hover: $color-white !default; /// background /// @namespace statement/normal $btn-pure-primary-bg: $color-brand1-6 !default; /// background /// @namespace statement/hover $btn-pure-primary-bg-hover: $color-brand1-9 !default; /// border color /// @namespace statement/normal $btn-pure-primary-border-color: $color-transparent !default; /// border color /// @namespace statement/hover $btn-pure-primary-border-color-hover: $color-transparent !default; /// border style /// @namespace statement/normal $btn-pure-primary-border-style: $line-solid !default;
然后,在组件里面,使用它们:
&-primary {
border-style: $btn-pure-primary-border-style;
@include button-color(
$btn-pure-primary-color,
$btn-pure-primary-color-hover,
$btn-pure-primary-bg,
$btn-pure-primary-bg-hover,
$btn-pure-primary-border-color,
$btn-pure-primary-border-color-hover
);
}
当你在使用主题的时候,会添加一个loader。它的作用,就是把主题包中的变量,拿去覆盖掉默认的主题变量,然后再编译出最终的样式:
{
test: /\.scss$/,
use: ExtractTextPlugin.extract({
use: [
'css-loader',
'fast-sass-loader',
{
loader: '@alifd/next-theme-loader',
options: {
theme: '@alifd/theme-xxx',
base: '@alifd/next',
},
},
],
}),
},
CSS in JS
这个方案可能大家听得不多,但实际上坑了我几个月时间。设计上和上面的方案其实一模一样,不同的是它的实现方案。它将主题包通过Provider的形式共享到应用中,在子组件中,从Provider中取出主题,并将其拼合到style上。为了方便使用,在React中,一般还会基于useContext封装出用于主题的hooks。例如,rax-ui中,这样定义按钮的样式:
export default (theme: any) => {
const core = theme.Core;
return {
'button': {
height: core['s-8'],
paddingLeft: core['s-4'],
paddingRight: core['s-4'],
borderWidth: core['line-1'],
borderColor: core['color-line1-3']
}
}
}
然后,在按钮中,通过封装的useStyle使用:
// Powered by ShuangYa (sylingd.com)
import { useStyles, StyledComponentProps } from '@rax-ui/styles';
import StyleProvider from './style';
const Button = forwardRef<any, ButtonProps>((props: ButtonProps, ref) => {
const styles = useStyles(StyleProvider, props, (classNames) => {
return {
button: {
...classNames([
'button',
{
'button--block': props.block || props.cell,
},
]),
}
};
});
return (
<View
style={styles.button}
>
{props.children}
</View>
)
}
为什么要这样做?这样做其实有几个好处:
- 不需要额外配置编译链,更简单
- 在JS中可以进行二次计算,更灵活
- 可以很方便的动态切换主题
- 支持Weex
但也有坏处:
- 覆盖样式很麻烦
- 样式出现问题时,比较难以排查
- 需要JS处理,性能稍差
CSS Variables
一开始,CSS in JS用起来问题倒是不大。直到我们将它“搬”上了搭建平台——没法通过常规方式覆盖样式了。
于是,我们就开始了改造。最后选定的方案是CSS Variables,因为这样的工作量比较小。具体平台兼容性可以参考Can I use:

很幸运,iOS 9.3+、Android 5.0+、Chrome 49+均支持CSS Variable,经过测试,阿里小程序也完全支持CSS Variable。所以,CSS Variable也就成为了我们的选择。我们可以通过CSS原生方式实现主题支持了,例如:
/* Powered by ShuangYa (sylingd.com) */
/* 全局CSS里面定义基本样式 */
:root {
--s-4: 10px;
--s-8: 22px;
--line-1: 1px;
--color-line1-3: rgb(0, 0, 0)
}
/* 组件里面取变量 */
:root {
--button-height: var(--s-8);
--button-padding: var(--s-4);
--button-border-width: var(--line-1););
--button-border-color: var(--color-line1-3);
}
.button {
height: var(--button-height);
padding-left: var(--button-padding);
padding-right: var(--button-padding);
border-width: var(--button-border-width);
border-color: var(--button-border-color);
}
这样做唯一的坏处就是,当你打开浏览器的检查元素时,会在CSS列表里看到一大堆变量……
小结
好久不写博客了,最近在折腾组件库相关的东西,所以顺便写写这里面的技术细节吧……