前言
说到组件库,那可是一抓一大把。从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列表里看到一大堆变量……
小结
好久不写博客了,最近在折腾组件库相关的东西,所以顺便写写这里面的技术细节吧……