阅读 2634

前端 UI 组件库搭建指南

近期负责公司内部 React UI 组件库的搭建,过程中踩了不少坑,主要还是对整个搭建过程没有一个比较清晰的概念,本文章就作为整个搭建过程的一个总结吧。

本文将详细讲述搭建一个前端 UI 组件库需要涉及的流程和相关知识、工具,其中参考了一些主流开源组件库的做法,阅读大约需要15分钟。

Monorepo?

Monorepo(Monolithic Repositories)是目前比较流行的一种将多个项目的代码放在同一个库统一管理的代码管理组织方式,这种方式能够比较方便地进行版本管理和依赖管理,通常配合 lerna 管理多个 package。

那么,UI 组件库需不需要使用 Monorepo 这种模式呢?

纵观目前各大开源项目,像 Reactbabel 等生态较为丰富的项目,都是以”一个主包,多个从包“构建的生态系统,比较适合采用 Monorepo 的方式管理复杂的依赖关系,例如 React 的 packages

但对于 UI 组件库来说,每个组件作为一个个独立的单元存在,相互之间的依赖一般比较少,所以对于组件库自身没有必要采用 Monorepo 的方式拆分多个 package。那以组件库为主包、各种自研的工具库作为从包的方式可以使用 Monorepo 进行管理吗?答案是可以的,目前有赞的 zent 就是采用这种方式:

所以到底应不应该采用 Monorepo 的模式呢?我的建议是,如果仅仅是想造好组件库这一个轮子,并且没有依赖关系比较复杂的其他库需要统一管理,那就直接以一个 package 来管理即可,怎么简单怎么来!

目录结构

在确定组件库的目录结构之前,先大致捋一下组件库需要有哪些组成部分:

  • 源代码
  • 示例代码
  • 文档
  • 打包结果
  • 测试代码
  • 打包构建配置、脚本
  • 配置文件(babel、eslint、jest 等)
  • 自动化脚本
  • ...

组成的目录结构大致如下:

├── build                           // 打包脚本
├── docs                            // 文档部署目录(Github Pages)
├── examples                        // 示例代码(本地开发环境)
├── lib                             // 打包结果
├── scripts                         // 自动化脚本
├── site                            // 文档静态站点
├── src                             // 组件库源码
    ├── components                  // 所有组件
        ├── [componentName]         // 单个组件
            ├── __tests__           // 组件测试文件
    ├── styles                      // 样式
    ├── types                       // 类型声明文件
├── tests                           // 测试
├── .babelrc                        // 插件
├── .eslintrc                       // eslint 配置
├── .publish-ci.yml                 // npm 包发布、站点部署 CI 脚本
├── jest.config.js                  // Jest 配置文件
└── package.json                    // package.json
复制代码

组件开发

本地开发环境

首先需要一套可运行的开发环境,以供我们在本地调试、运行组件代码。

参考大多数 UI 组件库的做法,可以examples 下的示例代码组织起来并暴露一个入口,使用 webpack 配置一个 dev-server,后续对组件的调试、运行都在此 dev-server 下进行。

或者也可以使用脚手架工具搭一个单页应用作为本地开发环境,可省去配置 webpack 的麻烦。方法其实有很多种,目的是为了让组件在本地 Run 起来,以方便项目的 developers 和 contributors。

最后,记得将 dev-server 的启动脚本加入 npm scripts 中,例如:

package.json

组件初始化

在开发一个组件之前,需要创建组件目录、创建组件文件、初始化组件模板、创建测试目录/文件等一系列繁琐又重复的工作,这些其实都可以使用脚本自动化实现。

可以在 scripts 目录下写一个 node 脚本实现以上过程,并在 package.json 中添加 scripts 脚本加入到 npm 工作流中:

package.json

scripts/new_component.js:

单元测试

UI 组件作为高度抽象的基础公共组件,编写单元测试是很有必要的。一方面,单元测试能够覆盖到一些端到端测试覆盖不到的点,另一方面,也能提高组件代码的可维护性,保证代码质量。

框架选型

关于单元测试框架的选型,以 React UI 组件库为例,目前比较流行的组合是 Jest + Enzyme

Jest

Jest 是 Facebook 开源的一个前端测试框架,自带断言库,配置简易,提供了 JSDOM、Mock 系统、快照测试、异步代码测试、静态分析结果等测试功能。

Jest 会在以下几个地方寻找测试文件:

  • __tests__ 目录下后缀为 .js 的文件
  • 后缀为 .test.js 的文件
  • 后缀为 .spec.js 的文件

一般会把测试文件放在对应的组件同级目录下,这样在语义上是有意义的,并且引入路径也更短些。

Enzyme

Enzyme 是 Airbnb 开源的一个 React 测试类库,提供了一套简洁又强大的 DOM 处理 API。Enzyme 是对官方测试工具库 ReactTestUtils 的二次封装,并内置了 Cheerio(一个号称 “服务端 JQuery” 的爬虫库)。

Jest + Enzyme

以下是一个使用 Jest + Enzyme 编写的单元测试示例:

将测试流程加入工作流

package.json

打包

对于打包后的文件,统一放在 lib 目录下,同时记得要在 .gitignore 中加上 lib 目录,避免将打包结果提交到代码库中。

提供 umd 规范的包

umd 兼容了 AMDCommonJS 两种模块化规范,可同时支持浏览器、Node 两种宿主环境,通过指定 Webpack 配置中的 output.libraryTarget 字段为 umd 即可:

libraryTarget 还可根据需要设置为 windowglobalcommonjs 等值应对不同的打包场景,具体参考 Webpack 配置文档

按需加载

在 UI 组件库的使用场景中,往往有时候只需引入个别组件而非全量组件,那么就要求组件库需要有能够按需加载的能力,支持类似如下方式的引入语法:

实现按需加载,一般有两种方式: Tree Shaking 和单独打包组件。

Tree Shaking

如果通过 Tree Shaking 实现按需加载,那么经过 babel 编译的 umd 包肯定是无法满足的,需要另外提供一份 ES6 Module 规范的包。

但是!Webpack 的 libraryTarget 不支持 ES6 Module 规范的包的导出。只能借助于其他打包工具例如 rollup 来导出 ES6 Module 规范的包,并配合 package.json 指定 module 字段为 ES6 Module 规范的包的路径,以实现使用 ES6 的方式引入包时读取到的是提供的 ES6 Module 规范的包。

单独打包组件

若无法导出 ES6 Module 规范的包,也可以采用目前大多数组件库的做法:将组件单独打包

将组件单独打包需要在 Webpack 中配置多个entry,大致配置如下:

使用以上配置最终会将组件打包到不同的目录下,这样就可以支持按需引入的方式加载组件了:

但是,要实现上文提到的 import { Alert, Button } from 'ui-library' 这种引入方式,还需要借助第三方的 babel 插件来实现引入语句的转换,目前使用比较多的有:


对于 umd 和按需加载的两种打包方式,需要分别提供对应的 npm script,例如:

文档

组件库的文档一般都是对外可访问的,因此需要部署到服务器上,同时也需具备本地预览的功能。

文档生成器

你可以选择自己搭一个用于展示文档的站点,也可以使用文档生成器来生成文档站点,比较推崇使用文档生成器。可以根据自己比较擅长的技术栈选择特定的文档生成器,目前主流的文档生成器有:

  • Docz:React 技术栈,MDX(Markdown + jsx)语法,基于 Gatsby.js
  • Storybook:支持 Vue/React/Angular 等,提供功能丰富的 addons 插件增强文档交互体验。
  • React Styleguidist:React 技术栈,支持在 md 文件中解析 js/jsx 代码块。
  • VuePress:Vue 技术栈,支持在 md 文件中插入 Vue 组件。

以上几种文档生成器均支持在 markdown 文件中插入 js/jsx 或特定的组件标签,区别在于语法风格不同。如果使用的是 React 技术栈,只能说,Docz 所引入的 MDX 语法,真香!甩个图随意感受下:

文档部署

Github Pages

如果项目代码托管在 Github 上的话,可以将文档站点部署在 Github Pages 上。

Github Pages 提供了三种模式,你可以将静态站点放在以下三个位置:

  • master 分支
  • master 分支的 docs 目录
  • gh-pages 分支

例如在 Github 项目的 Settings 下,将 Github Pages 的 Source 改为 master branch/docs folder

通过访问 <username>.github.io/<repository-name> 即可访问到 docs 目录下的资源。

CI/CD

关于文档站点的持续集成和自动化部署,可以借助官方或第三方提供的 CI/CD 工具,例如:

  • Github + Travis CI
  • Github + Github Actions
  • Gitlab + Gitlab CI/CD

发布

组件库的某个版本完成开发工作后,需要将包发布到 npm 上。

关于 package.json 你需要知道的

package.json 中有几个字段值得关注:

name

  • 发布到 npm 上的包名
  • 安装时的包名

格式:英文小写,中划线或下划线分隔。

version

版本号,符合语义化版本规则,即 major.minor.patch

  • major:主版本号,不兼容的修改
  • minor:次版本号,向下兼容的新功能
  • patch:修订号,向下兼容的问题修复

main、unpkg、module/jsnext:main

main包的入口。例如,Node 环境下使用 import pkg from 'package-name' 导入的就是 main 定义的入口文件,可以是 CommonJS 格式或者 umd 格式。

unpkg 定义浏览器环境使用的入口,一般格式为 name.min.js

module 定义用 ES6 模块打包的入口,一般格式为 name.esm.js

示例:

description、keywords

descriptionkeywords 分别定义包的描述和关键字,有助于包的检索,并在检索结果中显示。

author

作者,一般格式是 ${name} ${email}


除了以上字段之外,根目录下的 README.md 内容会在 npm 包的详情页展示。

发布前

执行测试

在发布之前,需要确保 UI 组件库的代码能够通过所有编写的测试用例,运行先前加入工作流的 npm run test 命令即可。

打包构建

在发布 npm 包之前,需要执行打包构建,确保 lib 目录下的内容包含当前需要发布的内容。

更新版本号

使用 npm version 命令自动计算下一个版本号,并将该版本号更新到 package.json 文件中。可以手动指定符合语义化版本规则版本号,也可以使用 npm version 提供的一些快捷命令自动更新版本号。npm version 的语法如下:

npm version [<newversion> | major | minor | patch | premajor | preminor | prepatch | prerelease [--preid=<prerelease-id>] | from-git]
复制代码

如果在一个 Git 仓库下执行 npm version,会默认新增一个同名 Tag,这一默认行为可以用 npm --no-git-tag-version version 禁止。

更多关于 npm version 的用法可以参考官方文档

npm 包发布

  • npm 官网 注册一个账号(或者使用 npm adduser 注册)
  • 查看本地 .npmrc 配置,确保 registry=https://registry.npmjs.org/(可通过 npm config edit 查看或编辑)
  • 运行 npm login 在本地登录 npm 账号
  • 执行 npm publish

以上过程发布的是一个 unscoped 包,当发布的包跟 npm 上现有的包存在命名冲突时,就需要发布一个特定作用域下的 scoped 包:

  • 包名格式需要加上作用域:@scope/package-name
  • 在发布时指定为公共发布:npm publish --access public,以避免被默认识别为私包(私包是收费的T T)

打标签

我们需要为特定的版本运行 git tag -a [tagname] 打上 Tag,并运行 git push origin [tagname] 显式将该 Tag 推送至远程。

这一过程可以在 npm 包发布之前,也可以在 npm 发布之后,在后面的自动化发布中会涉及到这一点。

自动化发布

如果每次发布都需要运行测试、打包构建、更新版本号、npm 包发布等流程,未免有些繁琐,需要借助一些自动化的手段将上述流程串连起来,实现自动化发布。

自动化发布 npm 需要有一个触发点,可以是运行 npm script 时触发 prepost 钩子实现全流程的自动化,例如:定义 preversionpostversion 钩子脚本,在执行 npm version 前后会分别执行这两个脚本。也可以配合 CI/CD 工具,例如在 CI/CD 脚本中监听 master 分支的 tags 推送,一旦有新 tag 推送至远程,就执行上述的发布流程,包括文档站点的部署也可以在这里实现。

维护

CHANGELOG.md

一般开源项目会将 Changelog 维护到一个 Markdown 文件里,例如 CHANGELOG.md

如果你的 commit message 使用的是 Angular 规范 ,那么可以使用 conventional-changelog 工具自动根据 commit message 生成 CHANGELOG.md,以下三种 type 的 commit 会被写入 CHANGELOG.md:

  • feat
  • fix
  • Breaking change

同样,需要将该流程加入到 npm script 工作流中:

以上命令会在 CHANGELOG.md 头部追加上次发布以来符合规范的 commit message。

CONTRIBUTING.md

如果你想让更多的开发者参与到组件库的共建,可以以 Markdown 文档的形式提供一份简单的 Contributing Guide,命名为 CONTRIBUTING.md,大致包含以下内容:

  • Issue/Pull Request 规范
  • 开发环境搭建
  • 开发规范

总结

本文主要总结了搭建一个前端 UI 组件库所涉及到各个步骤:

  • 代码组织方式:是否应该采用 MonoRepo?
  • 目录结构
  • 组件开发:本地开发环境、组件初始化
  • 单元测试:框架选型(Jest + Enzyme,以 React UI 组件库为例)
  • 打包:提供 umd 规范的包、按需加载的两种打包方式
  • 文档:文档生成器对比、文档部署
  • 发布:package.json 相关字段说明、发布前的准备工作、npm 发布流程、自动化发布
  • 维护:CHANGELOG.md 自动生成、CONTRIBUTING.md 主要内容

参考:

文章分类
前端
文章标签