从无到有搭建一套组件库

1,392 阅读10分钟

原文链接:github.com/func-star/b…

简介

在正式开始之前,我们先简单的回顾一下前端领域的发展历程。在最早期的阶段,我们只能裸写 js/css,慢慢的项目中就能看到非常多的重复代码。在这样的背景下,jQuery 和 Bootstrap 应运而生了,它所展示的组件设计思想在之后许多的组件库中都能看到,也给项目开发带来了升级。进而进入 MVVM 主宰阶段,组件化的思想开始像病毒一样在前端领域蔓延开来。

组件化更像是一种思想,是前端工程师在代码复用和功能抽象上的一次实践。最终目的都是提效和项目优化。

会“偷懒”又何尝不是工程师的美德。

组件规划阶段

  • 定制化
  • 扩展性
  • 国际化

定制化

这里讲的定制化,可理解成组件换肤,但绝不止于颜色。像业界很多成熟的组件库都支持这样的定制功能。举两个例子:

像 Ant Design 组件库,它对外暴露了许多的样式变量,接入方可以通过覆盖样式变量的方式来定制符合自己要求的组件。因为 Ant Design 采用的是 Less 预处理器,所以接入的项目也必须支持 Less 编译。

@primary-color: #1890ff; // 全局主色
@link-color: #1890ff; // 链接色
@success-color: #52c41a; // 成功色
@warning-color: #faad14; // 警告色
@error-color: #f5222d; // 错误色
@font-size-base: 14px; // 主字号
@heading-color: rgba(0, 0, 0, 0.85); // 标题色
@text-color: rgba(0, 0, 0, 0.65); // 主文本色
@text-color-secondary : rgba(0, 0, 0, .45); // 次文本色
@disabled-color : rgba(0, 0, 0, .25); // 失效色
@border-radius-base: 4px; // 组件/浮层圆角
@border-color-base: #d9d9d9; // 边框色
@box-shadow-base: 0 2px 8px rgba(0, 0, 0, 0.15); // 浮层阴影

这也是我之前一直采用的方式。

Fusion Design 中台组件库,它可以说是为定制化量身打造的。它梳理出了过千种配置规则,使用者可以通过可视化界面搭配出一套符合自己要求的规则。这些规则和变量最终会转换成 Sass/Less 变量,然后编译后动态的渲染到组件中。这种方式的优点很明显,你不用再去看一大堆规则和变量背后的作用打底是什么。但是耦合性太高也是一个比较严重的问题。

随着浏览器原生能力的加强,CSS Variable 的兼容性已经达到生产环境应用标准。配合上calc(),同样可以达到样式变量配置化的能力。

我们可以借鉴 Fusion Design 的设计思路,提供接入方一个可视化的配置平台,生成一套符合接入方要求的样式变量,不过不再是 Less 的配置变量,而是原生支持的 css 变量,最终打成一个单独的主题包。同时解决了预处理器的环境依赖和样式变量在组件中的侵入。

扩展性

这里讲的扩展性和定制化有一定的重复性,样式变量角度不作重复介绍。扩展性是组件库核心能力非常重要的一个考量因素。为了应对业务方多变的功能需求,接入方在选择组件库的时候都会对扩展性有很高的要求。一般可以从以下几点来衡量一套组件库的扩展性:

  • 组件的 api 是否足够全面
  • 是否有清晰的组件组合关系,组件粒度是否够细
  • 组件是否提供了完整的生命周期钩子
- 组件的 api 是否足够全面

对于开发者而言,选择接入的组件库就像是一家综合超市,最好是我想要的你都有,我不想要的你也有。使用者可以在组件的基础上很低成本的扩展出个性化的业务组件。

- 是否有清晰的组件组合关系,组件粒度是否够细

先举个例子,比如我现在想要一个时间选择器组件(DatePicker),但是组件库中的 DatePicker 并不能满足我的业务需求。这时候我需要重新实现一个 DatePicker,如果自己实现的话成本就会很高。如果用组件库现有的组件进行组合,就能大大降低开发成本。

因此梳理组件间的组合关系就显得尤为重要,我一般把组件划分为基础元件、能力组件、复合组件和业务组件四种类型,然后通过组件组合来形成一整套组件库,这块下面会再介绍。

- 组件是否提供了完整的生命周期钩子

组件的生命周期在实际场景中很少使用,但并非是一个多余的设计。使用者可以对组件的挂载和卸载时机了如指掌,可以在组件更加细分的节点执行业务逻辑。

国际化

这是组件推广开源一个比较重要的因素,像 Ant Design Vue 更是提供了36种语言包。

组件设计阶段

  • 确认组件间组合关系
  • 定制全局 css 变量
  • 定制一致性的 api
  • 国际化基于组件 or 基于配置

确认组件间组合关系

为了达到高扩展性的目的,原子设计理论是一个很恰当的指导方案。原子设计理论认为页面应该是可以组合和分解的,同理组件也是可以组合和分解的。比如可以把复杂组件(复合组件)拆分成能力组件的组合,进而拆分成基础元件,然后复杂组件又可以结合业务场景成为业务组件。

我们拿 DateSelect (时间选择器组件)举个例子例。DateSelect 可以拆分为 DatePicker (时间滚动选择器) 和 Popup (弹出层组件),而 DatePicker 组件可以由多个 Picker (弹出层组件)组成,进而 Picker 又可以在 Hammer (手势库)的基础上开发,而 Popup 也可以在 Mask (蒙层组件)的基础上扩展得到。

未命名

定制全局 css 变量

不仅组件间有组合复用关系,样式变量同样需要。要保证所有的组件都有一致的风格样式,我们需要维护一份全局的样式变量,例如字体颜色,字体大小等等。然后所有组件都在这份全局共有变量的基础上定义私有的样式变量。

:root{
    --namespace-common-color: '#333';
    --namespace-common-font-size-sm: 12px;
    --namespace-common-font-size-md: 14px;
    --namespace-common-font-size-lg: 16px;
    ...
}
.namespace-date-select {
    .date-select-confirm, .date-select-cancel {
        color: var(--namespace-common-color);
        font-size: var(--namespace-common-font-size-sm);
    }
    ...
}
...

定制一致性的 api

整套组件库的开发一般会由多人协同完成,在这种情况下,很容易就会导致同种功能的 api 命名不一致的情况。比如 Dialog 中的取消事件用了 onCancel ,而DateSelect 中的取消事件却用了 cancel。虽然这并不会影响组件库的运行,但是会显得很不规范,没有组件的设计和评审阶段。并且在接入方正式使用的时候也会非常的懵逼,使用成本随之增高。

onMount:        组件挂载                 
onWillMount:    组件卸载
onShow:         显示事件
onHide:         隐藏事件 
onCancel:       取消事件
onConfirm:      确认事件
onDelete:       删除事件
...

国际化基于组件 or 基于配置

在社区里众多的组件库中,对国际化的支持程度不尽相同。主要看组件库设计之初针对的用户群体,如果你只是想打造一个内部使用的组件库,国际化就可以忽略。

基于组件配置的国际化和基于全局配置的国际化两者有一定的差异。如果你想在组件库中支持多语言(比如A组件用中文,B组件用英文),那么选择基于组件进行配置会简单很多。如果不需要支持到这种程度,那么全局配置就省事的多了

组件实现阶段

  • 确认组件的私有 api 和私有样式变量
  • 方法 or 事件
  • 组件质量保证
  • ...

确认组件的私有 api 和私有样式变量

为了让使用者可以定制组件风格,我们需要进行变量抽取,为每一个组件抽取出尽量丰富的样式变量,使用者可以灵活的修改组件的风格。

:root{
    --date-select-font-size: var(--namespace-common-font-size-sm);
    --date-select-color: var(--namespace-common-color);
    ...
}
.namespace-date-select {
    .date-select-confirm, .date-select-cancel {
        color: var(--date-select-color);
        font-size: var(--date-select-font-size);
    }
    ...
}
...

方法 or 事件

不管在日常开发中还是组件开发中,这都是一个比较头疼的问题。用方法回调还是事件通常都能达到我们想要的效果,在什么场景下该用方法什么场景下该用事件就比较犯难了。

我们来分析一下两者都有什么优缺点。

先来看一下回调方法。举个例子,你提交了查询表单,点击查询按钮之后需要刷新一下列表。我们需要把刷新列表的回调方法传递到查询表单,提供给接口返回后进行调用。在简单的父子级组件上应用肯定是非常方便的。然而当组件的嵌套关系复杂之后,你可能需要在兄弟级组件间,甚至是毫不相干的两个组件间进行通信,这就变得很痛苦了,因为回调方法的传递链路会变得很长。总结下来它最大的特点就是链路非常的清晰很容易定位方法来源,而当组件间关系变得复杂之后就变得不好维护了。

再来看一下事件。与方法不同,事件不需要组件间传递方法,它只需要定义一个监听的 key ,然后在正确的时间点触发即可。可以看出它能比较好的解决上面提出的组件间关系复杂的场景,然而它的问题在于,你定义了过多的事件之后项目就会变得很难维护,因为它不像回调方法有清晰的传递关系,出问题了你甚至都不能定位,滥用事件是非常恐怖的一件事情。

在开发组件的过程中,如果是比较简单的父子级关系组件,用方法传递绝对优于事件。而像一些组件嵌套关系非常复杂的复合组件则推荐用事件来处理比较好。

组件质量保证

致力于打造一个优秀的开源组件库,对组件的质量需要有一定的追求和要求。可以从以下几个点来尽量保证组件的质量

  1. 组件开发前的设计评审
  2. 单元测试覆盖率
  3. 交叉 review

组件维护阶段

  • 发版规范
  • ...

发版规范

每一次的发版需要有一条 issure 和分支关联,在 issure 上记录下版本的改造点和优化点,以及对应的开发负责人。