组件库是怎么支持主题包的

组件库是怎么支持主题包的

前言

说到组件库,那可是一抓一大把。从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列表里看到一大堆变量……

小结

好久不写博客了,最近在折腾组件库相关的东西,所以顺便写写这里面的技术细节吧……