背景
来也科技的 Web 系统前端使用 React 生态,在组件库上 antd 自然成了不二之选。

为了满足公司的设计标准和一些特定功能场景,过去我们直接 fork antd 的仓库,在上面做定制,最后内部发版。这样虽然可以满足项目的需求,但随着新增和修改的组件逐步增多,组件库变得越来越难以维护:
  • 组件修改方式不统一
    • 有的组件是直接修改源码,有的组件是引入 antd 组件后在上面封装一层
  • 组件文档编写不便
    • antd 内置的文档框架 bisheng 比较老旧,使用和定制成本均比较高
  • 图标库和组件库联调麻烦
    • 图标库和组件库是两个仓库,但图标的预览在组件库。想要联调新增或修改的图标,需要给图标库单独发版,或在本地配置 npm link

因此最近我们重新封装了一版基础组件库,同样基于 antd,但目标重点瞄准提高组件库的可维护性。

方案
对背景中提到的几个问题,我们做了对应的分析和技术选型:
统一使用 HoC 封装组件

两种组件修改方式的优缺点如下表所示:


封装组件(HoC)
修改组件源代码
优点
装饰器模式,定制与源码分离,优雅易维护
保留 antd 全部导出(export)
代码量稍小
缺点
需要手动导出 antd 的全部导出,否则丢失类型定义;
代码量略大
定制的代码混在在源代码中,难以维护
难以应对 antd 大版本升级
可以看到 HoC 虽然代码量略大,但可维护性更好。因此我们修改组件的方式统一为使用 HoC 封装。
使用 dumi 作为组件库的文档工具
首先需要明确的是:组件库和组件库文档是两个东西,只是大部分组件库的仓库中包含文档的代码。在构建上组件库和组件库文档也是独立的。

因此无论我们怎样定制 antd 组件,组件库文档都可以使用单独的工具。我们比较了市面上主流的开源组件库文档工具:
  • bisheng
    • antd 自带的文档工具,几乎只有 antd 自己在使用。年头较久,维护不积极
  • playground:
    • 主流的组件库文档工具,semi-design, arco-design 等组件库均在使用
  • dumi
    • 蚂蚁金服近期开源的文档工具。特点是基于 umi 生态,可以使用 umi 的插件,维护积极。同时有个最大的亮点:使用代码即文档

playground 和 dumi 都有较多优秀的使用案例。但和 playground 相比,dumi 在文档编写上有独特的优势,且切中我们维护文档的痛点。一个 dumi 中文档的代码的例子:


import React from 'react'import { Button } from 'antd'
export default function () { return ( <div> <Button type="primary">Primary ButtonButton> div> )}

可以看到,没有多余的依赖,写法和开发时使用组件完全一致。这样能够极大提升文档的编写效率。

但是引入新的组件库文档工具(只要不是 bisheng),组件文档的代码必然和 antd 现有的代码不同。antd 有 60+ 组件,每个组件少则4、5,多则 20+ 的 demo 文档,重写肯定是不可行的。因此一旦我们选择新的组件库文档工具,至少需要开发一个文档代码转换脚本,自动完成基础的转换工作,然后做少量人工纠正

好在这样的工作只需做一次,因为基础组件是有限的。在开发新的基础或业务组件时,demo 文档是全新编写,没有历史包袱。

综上,长远考虑组件库的维护性,我们选择 dumi 作为新的组件库文档工具,同时编写脚本进行 antd 文档到 dumi 文档的转换。
使用 monorepo 管理图标库和组件库
antd 的组件库和图标库是两个仓库: antd 和 @ant-design/icons。在组件库开发时需要联调修改的图标时,需要使用 npm link 做仓库间的本地引用(为联调发版不现实)。npm link 需要在开发者本地配置,不方便同步。因此我们使用 monorepo 来管理这两个仓库。

monorepo 是一种将多个项目代码存储在一个仓库里的软件开发策略,相对于 multi-repo(多仓库)相比:


Monoreop
Multi-repo
优点
  • 依赖、配置复用

  • 方便子项目间依赖


  • 足够简单


缺点
  • 整体项目庞大

  • CI 复杂

  • 需要配置,有学习成本


  • 项目间依赖麻烦


适用场景
工具集
项目

可以看到,像组件库这样的工具集是非常适合使用 monorepo 进行管理的。通常 monorepo 的实现工具是 Lerna。Lerna 可以满足 Monorepo 和子项目之间的相互引用。但 Lerna 是没有全局依赖的概念的,多个子项目如果有共同的依赖,需要重复安装。而在工具集里这种情况恰恰又是比较常见的,例如 React 等基础库。所以在 Lerna 的基础上引入 Yarn Workspace 来解决这个问题。

Yarn Workspace 支持全局依赖,同时和 Lerna 在目录管理上有部分功能重合。所以通常这么划分两者的职责:
  • Yarn Workspace 负责依赖管理

  • Lerna 负责版本管理


实现
基于以上方案实现的新版组件库结构如下:



.├── lerna.json                        // lerna 配置文件└── packages                          // 子项目目录    ├── components                    // 组件库    │   ├── docs    │   │   └── index.md             // 组件库文档入口    │   ├── src    │       ├── button    │       │   ├── demos            // demo 代码块    │       │   ├── index.md         // 组件文档入口    │       │   ├── index.less       // 组件样式    │       │   └── index.tsx        // 组件(HoC)    │       ├── index.ts              // 组件库总入口    │       └── styles    │           ├── custom.less       // antd 样式变量覆盖    │           └── index.less        // 样式总入口    │   ├── .fatherrc.ts              // 组件库打包配置    │   └── .umirc.ts                 // 组件库文档打包配置    ├── icons                          // 图标库    └── icons-svg                      // SVG 基础库


踩坑
按需加载
由于使用文档库工具选型 dumi,且 dumi 自带了组件打包工具,组件的打包同样由 dumi 完成。按需加载:
JS:生成 ESModule 的包则天然支持按需加载,我们需要在 dumi 打包配置文件中做如下配置:
export default {  esm: 'babel'}

Less:需要配合按需引入的插件,例如 Webpack 环境下使用: babel-plugin-import


"plugins": [  [    'import',    {      libraryName: 'laiyed',      libraryDirectory: 'lib/components',      style: (module) => `${module}/index.css`,    },  ],]



组件类型定义
为了保持 antd 组件大量的类型定义,我们在使用 HoC 封装组件后,需要将 antd 包含的类型定义手动导出在入口文件中:


/** * 组件库的入口文件 */
export type { ButtonProps } from './components/button'export { default as Button } from './components/button'
// ......


组件库文档中的样式

antd 组件文档中,有部分包含手动添加的样式,例如 button 的间距等。这些样式是写在 antd 的文档工具 bisheng 的模板中,在使用脚本迁移组件库文档到 dumi 的过程会丢失。我们需要手动补足这些样式以保证文档显示效果统一(注意这是文档单独的样式,和组件的样式无关)。


/** Button */.__dumi-default-previewer[id^='button'] .ant-btn {  margin-right: 12px;  margin-bottom: 12px;}
.__dumi-default-previewer[id='button-ghost'] .ant-btn { margin-bottom: 0;}
.__dumi-default-previewer[id='button-ghost'] .__dumi-default-previewer-demo > div { padding: 8px; background: rgb(190, 200, 200);}
#demo-btn-disabled { padding: 8px; background: rgb(190, 200, 200);}
#demo-btn-disabled .ant-btn { margin-bottom: 0;}
/** ... */


总结

在基于 antd 定制公司内部组件库的场景,我们选型了像 dumi、monorepo 这样更先进的工具和代码组织方式,提升了组件库的开发效率和可维护性。

antd 本身未见得不了解这些改进,可能只是更多地为了兼容性,毕竟开源和公司内部使用是两个完全不同的场景,服务的开发者数量差别巨大。感谢蚂蚁金服开源了 antd 如此优秀的组件库,让我们在享受海量现成组件的同时,可以用较小的成本改造以满足自身需求。


本文作者:Byteyang