React.js 组件库设计
课程目标
- 掌握可复⽤的 react 组件设计思路和原则;
- 能够基于业界成熟⽅案搭建⼀个 react 组件库;
- 能够编写⾼质量的组件⽂档;
- 能够不使⽤脚⼿架从 0 - 1 搭建⼀个 react 组件库;
- 能够理解不同组件库架构的差异和各⾃使⽤场景;
- 能够制定标准的代码规范和提交规范;
- 能够掌握组件单元测试的编写;
- 了解组件库发布流程。
课程大纲
- 编写可复⽤⾼质量的 react 组件;
- 编写⾼质量的组件⽂档;
- 业界成熟的组件库脚⼿架介绍;
- 常⻅的组件库架构差异解析;
- 引⼊代码规范和提交规范;
- 构建⼯具选择;
- 单元测试的编写;
- 版本号规范解析。
知识要点
react 组件的设计原则
有意义
- 命名准确,充分表意
- 参数准确,必要的类型检查
- 适当的注释
通⽤性
- 不要耦合特殊的业务代码
- 不要包含特定的代码处理逻辑
⽆状态,⽆副作⽤
- 状态向上层提取,尽量少用内部状态
- 解耦 IO 操作
避免过度封装
- 合理冗余
- 避免过度抽象
单⼀职责
- 一个组件只完成一个功能
- 尽量避免不同组件相互依赖、循环依赖
易于测试
- 更容易的单元测试覆盖
组件⽂档编写
⽂档结构
- 组件描述
- 组件示例及代码演示
- 组件入参描述
业界成熟的组件库脚⼿架
组件库架构差异及各⾃使⽤场景
Multirepo
一个仓库只有一个项目,以一个 npm 包发布,适用于基础组件库
优点
- 项目简单,调试安装方便
缺点
- 项目庞大时构建和发布耗时长
- 组件库使用时需整体引入,造成一定的资源浪费。(可通过 es module 方式解决)
典型案例
Monorepo
⼀个仓库内管理多个项⽬,以多个 npm 包⽅式发布,依赖集中管理,npm 包版本可以集中管理,也可以单独管理。通常适⽤于有⼀定关联的组件,但各组件需要⽀持单独的 npm 包发布和安装。
优点
- 共同依赖统⼀管理,版本控制更加容易,依赖管理会变的⽅便。
- ⽀持组件的单独发布和单独构建。
- 使⽤时可以单独引⼊。
缺点
- 项目搭建复杂度高
典型案例
Monorepo 管理⼯具
- lerna
- yarn workspace
- pnpm
引⼊代码规范和提交规范
Eslint & Prettier
⼀个⾼质量的组件库,eslint 和 prettier 是必须的,能够帮助我们统⼀整个仓库的代码规范。
也可以使用业界成熟的 eslint 配置
- @umijs/fabric
.eslintrc.js 1
2
3
4
5
6module.exports = {
extends: [require.resolve('@umijs/fabric/dist/eslint')],
rules: {
// your rules
},
}.stylelintrc.js 1
2
3
4
5
6module.exports = {
extends: [require.resolve('@umijs/fabric/dist/stylelint')],
rules: {
// your rules
},
}.prettierrc.js 1
2
3
4
5const fabric = require('@umijs/fabric');
module.exports = {
...fabric.prettier,
};
以上是 eslint 的配置,需要靠我们⼿动执⾏,万⼀我们忘记⼿动执⾏怎么办?
我们可以使⽤ lint-stased:
staged 是 Git ⾥的概念,表示暂存区,lint-staged 表示只检查并矫正暂存区中的⽂件。⼀来提⾼校验效率,⼆来可以为⽼的项⽬带去巨⼤的⽅便。
1 | { |
Typescript
推荐在项⽬中使⽤ typescript,良好的类型定义也是⼀个必须标准。
常⽤的tsconfig.json配置:
1 | { |
commitizen & commitlint & husky
commitizen 帮助我们⾃动⽣成统⼀格式的提交前缀,能够在多⼈协作开发时,保持统⼀格式的提交记录。
commitizen 有很多提交规则,由于我们使⽤ lerna 搭建项⽬。所以使⽤ cz-lerna-changelog 规则:
1 | { |
commitlint 能够检查错误格式的commit提交。
1 | module.exports = { extends: ['@commitlint/config-conventional'] } |
husky 能拦截格式错误的 commit 提交
1 | { |
组件库⽂档
Docz
docz 的使⽤⽅法很简单,在安装完后,在 package.json 中加⼊如下命令,就可以使⽤了
1 | { |
构建⼯具的选择
Rollup vs Webpack
Webpack
- 代码分割:Webpack 可以将你的 app 分割成许多个容易管理的分块,这些分块能够在⽤户使⽤你的 app 时按需加载。这意味着你的⽤户可以有更快的交互体验
- 静态资源导⼊:图⽚、CSS 等静态资源可以直接导⼊到你的 app 中,就和其它的模块、节点⼀样能够进⾏依赖管理。
Rollup
- Tree Shaking:是 rollup 提出的⼀个特性,利⽤的 es6 模块的静态特性对导⼊的模块进⾏分析,只抽取使⽤到的⽅法,从⽽减⼩打包体积。
- 配置使⽤简便,⽣成的代码相对于 Webpack 更简洁。
- 可以指定⽣成⽣产中使⽤的各种不同的模块(amd,commonjs,es,umd)。
@umi/father
基于rollup,配置简单,⽀持多种架构的组件库打包。
最简单的配置:
1 | export default { |
Monorepo 配置
1 | import { readdirSync } from 'fs'; |
编写单元测试
jest 是常⽤的单元测试框架。
testing-library 专注于测试 react 组件,与之配套的还有 react-hooks-testing-library,专⻔⽤来测试 react-hooks。
使⽤jest:
结构
编写单元测试所涉及的⽂件应存放于以下两个⽬录:
- mocks/:模拟⽂件⽬录
- [name].mock.json:【例】单个模拟⽂件
- tests/:单元测试⽬录
- [target].test.js:【例】单个单元测试⽂件,[target]与⽬标⽂件名保持⼀致,当⽬标⽂件名为index 时,采⽤其上层⽬录或模块名。
[target].test.js ⽂件常⻅格式
1 | const thirdPartyModule = require('thrid-party-module') |
保证每个 describe 内部只有 mock 对象、⽣命周期钩⼦函数和 test 函数,将模拟对象都添加到 mocks 对象的适当位置,将初始化操作都添加到适当的⽣命周期函数中。
使⽤testing-library
React 测试库是⼀组能让你不依赖 React 组件具体实现对他们进⾏测试的辅助⼯具。它让重构⼯作变得轻⽽易举,还会推动你拥抱有关⽆障碍的最佳实现。React 测试库并不是 Jest 的替代⽅案,因为他们需要彼此,并且有不同的分⼯。
- 利⽤ react 测试库渲染APP组件
- 利⽤ react 测试库获取元素
- 利⽤ Jest 来进⾏写测试⽤例和断⾔
1 | import { render, screen } from '@testing-library/react'; |
获取元素
- getByRole
<div role="alert"></div>
- getByLabelText:
<label for="search" />
- getByPlaceholderText:
<input placeholder="Search" />
- getByAltText:
<img alt="profile" />
- getByDisplayValue:
<input value="JavaScript">
- getByTestId:
<any data-testid="xxx">
除此之外,还有 queryByxxx 和 findByxxx 的查询函数,什么时候⽤ get/query/find ?,需要了解它们的不同。getBy 返回元素或者错误。getBy 在查找不到元素时返回错误,这是⾮常⽅便的,有助于我们在开发的过程中尽早的发现⾃⼰的⽤例发⽣了错误。findBy ⽤于查询⼀个在异步之后会被最终渲染的元素。
- queryByText/findByText
- queryByRole/findByRole
- queryByLabelText/findByLabelText
- queryByPlaceholderText/findByPlaceholderText
- queryByAltText/findByAltText
- queryByDisplayValue/findByDisplayValue
断⾔函数
正常情况下,这些断⾔函数来⾃ Jest,但是 React 测试库拓展了,加⼊了⼀些⾃⼰的断⾔函数。
- toBeDisabled
- toBeEnabled
- toBeEmpty
- toBeEmptyDOMElement
- toBeInTheDocument
- toBeInvalid
- toBeRequired
- toBeValid
- toBeVisible
- toContainElement
- toContainHTML ● toHaveAttribute
- toHaveClass
- toHaveFocus
- toHaveFormValues
- toHaveStyle
- toHaveTextContent
- toHaveValue
- toHaveDisplayValue
- toBeChecked
- toBePartiallyChecked
- toHaveDescription
事件触发/回调处理
我们可以通过 React 测试库的 fireEvent 去模拟⽤户交互⾏为:(输⼊⽂字到Input框内)
Search组件:
1 | function Search({ value, onChange, children }) { |
我们想要测试当我们在 Search 的 Input 框内输⼊值时,onChange 是否有按预期的被调⽤,则需要通过 jest 给我们提供的 fn 函数:
1 | describe('Search', () => { |
可以看到 onChange 通过 fireEvent 触发的情况下,只调⽤了⼀次,这个时候,我们可以使⽤ userEvent 去替代 fireEvent,⽐起 fireEvent,userEvent 更加的贴近⼈类的交互⾏为,在输⼊⽂字的时候,可以看到 onChange 会被调⽤多次(这是因为 userEvent 更加模拟了⼈类的键盘输⼊,keyDown 等)
1 | describe('Search', async () => { |
react-hooks-testing-library
react-hooks-testing-library,是⼀个专⻔⽤来测试 React hook 的库。我们知道虽然 hook 是⼀个函数,可是我们却不能⽤测试普通函数的⽅法来测试它们,因为它们的实际运⾏会涉及到很多 React 运⾏时(runtime)的东⻄,react-hooks-testing-library 的库来允许我们像测试普通函数⼀样测试我们定义的 hook,这个库其实背后也是将我们定义的 hook 运⾏在⼀个 TestComponent ⾥⾯,只不过它封装了⼀些简易的 API 来简化我们的测试。
renderHook
- renderHook ⽤来渲染 hook 的,它会在调⽤的时候渲染⼀个专⻔⽤来测试的 TestComponent 来使⽤我们的 hook。renderHook 的函数签名是 renderHook(callback, options?),它的第⼀个参数是⼀个 callback 函数,这个函数会在 TestComponent 每次被重新渲染的时候调⽤,因此我们可以在这个函数⾥⾯调⽤我们想要测试的hook。
- renderHook 的返回值是 RenderHookResult 对象,这个对象会有下⾯这些属性:
- result:result 是⼀个对象,它包含两个属性,⼀个是 current,它保存的是 renderHookcallback 的返回值,另外⼀个属性是 error,它⽤来存储 hook 在 render 过程中出现的任何错误。
- rerender: rerender 函数是⽤来重新渲染 TestComponent 的,它可以接收⼀个 newProps 作为参数,这个参数会作为组件重新渲染时的 props 值,同样 renderHook 的 callback 函数也会使⽤这个新的 props 来重新调⽤。
- unmount: unmount 函数是⽤来卸载 TestComponent 的,它主要⽤来覆盖⼀些 useEffect cleanup 函数的场景。
act
我们知道组件状态更新的时候(setState),组件需要被重新渲染,⽽这个重渲染是需要 React 进⾏调度的,因此是个异步的过程,我们可以通过使⽤ act 函数将所有会更新到组件状态的操作封装在它的 callback ⾥⾯来保证 act 函数执⾏完之后我们定义的组件已经完成了重新渲染。
1 | // somewhere/useCounter.js |
1 | describe('decrement', () => { |
单元测试编写原则
- 每个单元测试应该有个好名字
- 将内部逻辑与外部请求分开测试
- 要保证单元测试的外部环境尽量和实际使⽤时是⼀致的
- 对服务边界(interface)的输⼊和输出进⾏严格验证
- ⽤断⾔来代替原⽣的报错函数
- 避免随机结果
- 尽量避免断⾔时间的结果
- 测试⽤例之间相互隔离,不要相互影响
- 原⼦性,所有的测试只有两种结果:成功和失败
- 避免测试中的逻辑,即不该包含if、switch、for、while等
- 不要保护起来,try…catch…
- 每个⽤例只测试⼀个关注点
- 3A策略:arrange,action,assert
版本号管理
我们的组件库使⽤ npm 发布,版本号规范也使⽤ npm 标准的 semver 规范。
版本格式:主版本号.次版本号.修订号,版本号递增规则如下:
1.主版本号:当你做了不兼容的 API 修改,
2.次版本号:当你做了向下兼容的功能性新增,
3.修订号:当你做了向下兼容的问题修正。
4.可以使⽤lerna version交互式的选择你的版本号。