React 组件库技术选型实践

1,963 阅读6分钟
原文链接: ewind.us

一般中等规模以上的团队都会为满足定制化开发需求而定制一套自己的组件库。下面介绍了作者在基于 React 实现这样的一个组件库时,对几条关键技术路线的对比与最终方案。

开发流程选型

组件库的基础开发流程,包括了以下几点:

  • 各个组件的编写方式
  • 开发组件库时的示例 / 文档 / 测试方式
  • 组件库的发布 / 引入 / 部署方式

在这个阶段,要解决的问题主要有以下几个:

  1. 如何搭建组件的开发环境?
  2. 组件库的代码仓库是否需要包括其示例和文档?
  3. 每个组件是单独开发、发布,还是合并在同一个仓库中一起发布?

从实现一个【便于开发和引入的轻量级组件库】需求出发,可以给出的方案是:

  1. 每个 UI 组件开发时,即确定好其基础的 API,并实现调用该组件的基础 Demo 页面,将该 Demo 页作为开发调试组件时的 playground,而 Demo 页最后也能与文档结合,提升文档化的效率。
  2. Demo 页面和组件库的文档,均需要单独的构建配置。将它们与组件库本身耦合,并不符合组件库本身的内聚性质。因此可以将 Demo 项目与文档单独为独立项目,与组件库一同开发,分开维护。
  3. 虽然将组件单独发布能够一定程度上提高组件库的迭代效率,但在组件数量较多时,发布一堆 xx-button / xx-dialog / xx-table 的组件,无疑会增加使用者引入时的负担。并且,在较多的子仓库中共享代码时,也需要将公用代码抽取为单独的模块依赖。这样较为复杂的依赖关系,可能会造成低版本组件 A 依赖高版本 Common 模块时的潜在问题。因此,最终在选型决策上仍然是将组件库作为一个统一的 NPM 模块来开发。

具体到开发时的工作流,则是区分了 ui / playground / site 三个代码仓库(后两个会考虑合并),将 UI 仓库的工作区内容通过 npm link 命令链接到 Playground 中,从而在 Playground 中只需编写示例代码,即可正确导入 UI 主仓库的开发中组件依赖。
最后的 Site 仓库则是纯粹的文档仓库,Playground 中的 Demo 通过 <iframe> 标签的形式引入文档页面中,保证代码示例始终与文档一致。

模块打包工具选型

组件库的构建方式,会直接影响使用者的引入。这时,需要权衡使用者引入的难度、使用者项目构建配置的难度以及组件库构建配置的难度,做一个适当的 Trade Off。

在 2017 年中的阶段,JS 社区有几种构建类库的方式:

  1. 不引入构建工具。
  2. 采用 Babel 构建。
  3. 采用 Webpack 构建。
  4. 采用 Rollup 构建。

Node 端的模块常用第一种方式。而对于一个完整的前端项目,模块打包工具是不可或缺的。由于缺少与样式相关的解决方案,因此直接采用 Babel 和 Rollup 搭建前端项目的构建方式较为少见,而 Webpack 则在具体项目中有很高的流行度。

然而在组件库的开发中,Webpack 的流行并不代表它就是必须的解决方案。实际上,由于 Webpack 在【具体业务项目】中已经基本普及,因而【对组件库本身进行打包】的必要性并不大。除非组件库需要支持类似 Bootstrap 那样通过引入 <script> 标签来使用,否则直接提供源码模块的方式是足够兼容 Webpack 1 和 2 的。并且,在取消组件库的打包配置后,能够减少使用者侧的 Webpack 冗余配置(理想情况是,完全将组件库以 JS 模块的形式导入,无需考虑样式与模板的导入问题)。

在组件库实现中,采用了一个与目前主流组件库十分不同的选型策略:完全去除模块打包配置,直接提供组件库源码。虽然这个方案目前不支持传统通过标签导入组件库的方式,但考虑到组件库的应用场景多在附带完整 Webpack 配置的前端项目中,这时源码形式提供的模块能够很好地被导入,且在 Webpack 2 中是支持 Tree Shaking 按需导入的。至于 Webpack 1,则可通过显式指定形如 xx-ui/src/button 的组件模块形式,实现组件按需导入。但是,在 Webpack 项目中导入组件(而不是传统意义上的 NPM 模块或 JS 库插件)时,会涉及 React 技术栈上的样式方案问题,详见下文。

样式方案选型

在 React 上,各类样式解决方案一直处于百花齐放的状态。对组件库而言,目前能够使用的主流方案就有以下几种:

  1. 采用预处理器或 PostCSS 编译样式,在各组件中 import 样式。
  2. 采用 CSS Modules,将 Class 名哈希化至 React 组件中。
  3. 采用 Styled Components,将样式完全作为 JS 模块导入。

上述这三种方案中,3 是从使用便利程度出发最优的。由于 Styled Components 是纯 JS 的依赖,因此组件库可完全不考虑 Webpack 的样式 Loader 相关配置,完全将组件通过 JS 编写实现。

但 Styled Components 同时存在着较为严重的问题:首先,它必须在运行时插入样式,不支持通过 ExtractCSS 等插件提取静态化的 CSS。其次,Styled Components 会使得组件存在难以语义化的 Class Name 而影响调试体验,同时相对传统的 CSS 库方案而言也较难覆盖组件内部标签的样式。最后,它所引入的额外 JS Bundle 体积近 20K gzipped,且性能在各个 CSS in JS 方案中排名垫底,在移动端可能会成为一个较大的瓶颈,这也是最终没有采用该方案的最主要原因。当然,RN 场景下这类完全使用 JS 的样式解决方案仍然是很实用的。

而在 CSS Modules 和预处理器方案的比较中,结合上文中组件库【不包含构建工具】的选型方案,最终选定的方案还是预处理器编译样式,而非 CSS Modules。这个选型方案的理由在于:组件库的导入者,即【具体的业务项目】基本都存在完整的 CSS Loader 配置,只要在组件库中通过相对路径导入编译好的 CSS 文件,即可实现与原生 JS 模块相同的导入体验。而 CSS Modules 等方案同样应在业务项目 Webpack 配置中落实,在组件库的层面引入额外的样式 Bundle 策略,会带来导入时的额外负担。

总结

综上,移动端组件库的几个技术选型点在于:

  • 分拆 UI / Playground / Doc 为独立的 Git 仓库,一同开发,分别维护。
  • 不提供 JS 生产包,由用户以源码形式导入组件。
  • 采用预处理器编译出 CSS 文件,在组件库组件中通过相对路径导入,依赖于用户的 Webpack CSS Loader 配置来实现正确的样式加载及 autoprefix / extract 优化。