导航
导航
文章目录
  1. 问题
  2. 价值
  3. 解决方案
  4. 实践
  5. 基础架构
    1. 组件库目录结构
    2. 组件目录结构
    3. 编译后的组件目录结构
    4. 规范
  6. 组件
    1. 样式
  7. 文档
    1. 组件文档
    2. 组件库文档
  8. 打包
    1. 编译打包 ts[x]
    2. 编译打包样式文件
    3. UMD
  9. 迭代维护
    1. CHANGELOG.md
  10. 总结
  11. 参考

组件库搭建实践

前言:从去年年底开始规划项目的重构,也是这样一个机遇,我开始负责项目的一个基础架构以及一些公共基础服务,主要搭建了组件库以及工具类库来提升后续开发以及重构的效率,期间也是踩了不少坑,接下来就作为整个搭建过程的一个总结,本篇主要是组件库的搭建与实践。

一个项目或系统有着大量的业务场景和业务代码,相似的页面和代码层出不穷,那如何管理和抽象这些相似的代码和模块,这肯定是许多团队都会遇到的问题。不断的拷代码?还是抽象成 UI 组件或业务组件?显然后者更高效。

问题

之前的开发流程从产品设计到研发的过程中,最常出现在需求沟通与研发过程中由于缺少统一的规范和标准化体系来指导实践,导致实施环节各方沟通成本高。

  • 认知:产品、研发、设计师对于同一需求都有自己理解的解决方案,缺少统一规范的约束,难以达成共识。
  • 效率:设计效率低,交互原型的维护成本及上下游团队的沟通成本高,易造成不专业的印象。
  • 品质:认知和效率的局限性,最终导致实施落地的产品质量和用户体验难以得到保障。

价值

组件库最大的价值在于提升整个团队的产研效率,使设计质量得以保障的同时提升产品整体的用户体验。

  • 保证产品体验的一致性:对于一个含有多业务系统的大型复杂产品,每个独立的业务系统虽然在功能上有一定区别,但整体的用户体验需要满足基本的一致性。
  • 提升设计师的效率:在需求量巨大且需求来自不同的业务线时,需要逐一绘制页面及组件,造成大量重复劳动,并且在评审及需求沟通环节还可能存在不断地细节调优,所以对于设计师而言,组件的高频复用能大大提升设计效率,使设计师更多的将精力聚焦于理解和解决用户的实际问题。
  • 提升产研团队的效率:通用场景及普通需求直接按规范进行设计和研发,减少上下游对同一页面及组件使用方式的不同理解而产生的多余沟通成本。
  • 利于技术的沉淀:从一个组件库可以扩展到其它的技术方案,比如懒加载、Tree Shaking、文档预览等等。

解决方案

搭建统一的组件库

实践

接下来将详细介绍搭建一个前端组件库需要涉及的流程和相关知识、工具,其中也是参考了一些主流开源组件库的做法。

基础架构

组件库目录结构

目前现在的组件目录结构大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
├── ...
├── package.json
├── README.md // 文档说明
├── tsconfig.json // ts 配置文件
├── tsconfig.build.json // ts 编译配置
├── build // 配置文件
├── esm // es modules目标文件
├── lib // umd目标文件
├── docs // 文档
└── src
├── JsComponents // 原生js组件
├── components // React 组件
├── images
├── style // 公共样式文件
└── index.ts // 入口文件

组件目录结构

组件的目录结构参考了 antd 的规范

1
2
3
4
5
6
7
8
Input
├── Input.tsx // 组件
├── demo.tsx // 示例演示demo
├── index.md // 组件文档说明
├── index.tsx // 组件入口文件
└── style // 样式文件
├── index.scss
└── index.ts

编译后的组件目录结构

1
2
3
4
5
6
7
8
9
├── Input.d.ts
├── Input.js
├── index.d.ts
├── index.js
└── style
├── index.css
├── index.d.ts
├── index.js
└── index.scss

规范

无规矩不成方圆,在组件库开始之初就定义好规范,保证代码的严谨性,配置了以下规则:

  • eslint/@typescript-eslint
  • stylelint
  • git hooks
  • git commit
  • prettier

组件

基础架构定义好之后就可以愉快的进行组件开发了,组件的基本开发流程:

  • 组件初始化
  • 代码 coding
  • 组件 demo
  • 组件文档说明
  • 组件库入口文件导出组件

对于组件初始化,我们也是通过脚本自动化实现,减少这些繁琐重复的工作。

样式

对于组件的样式,一开始我们有两套方案:

我个人的话更喜欢 CSS-in-JS 的方案,不用去考虑样式的打包以及引用方式,但最终考虑到业务场景还需要输出原生的 css 代码提供旧项目引用,所以最后也是采用了常规样式,通过 gulp 打包输出 css 以及 scss 文件,后面打包的时候也会具体介绍一下。

文档

这里介绍一下目前的文档说明以及文档生成方案,首先对于每一个组件,需要在开发组件时编写好组件的说明文档(规则格式看下面组件文档)以及 demo,当我们运行组件库文档预览项目时会通过 node 脚本读取组件的说明文档以及 demo 自动生成对应的路由文件,这样我们就能实时预览文档以及 demo 演示。

组件文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
## Input

#### 输入框组件。

## Input Props:

| 参数 | 说明 | 类型 | 默认值 |
| ---------- | ------------------------ | -------------------------------------------- | ------- |
| disabled | 是否禁用 | boolean | - |
| icon | input 图标 | React.ReactElement | - |
| iconPlace | input 图标渲染位置 | 'left' \| 'right' | 'right' |
| allowClear | 是否可清除,渲染清除图标 | boolean | false |
| showNumber | 是否渲染字数计算 | boolean | false |
| maxLength | 最大输入长度 | number | 70 |
| onChange | input 输入框改变的回调 | (e: ChangeEvent\<HTMLInputElement\>) => void | - |

> Tip:Input 的 props 不止上面列举的项,可传入原生 input 的所有属性

### Usage

```js
import { Input } from "@hzzly/components";

const onChange = (e) => {
console.log("Value: ", e.target.value);
};

ReactDOM.render(<Input icon={<Icon />} onChange={onChange} />, mountNode);
```

组件库文档

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

可以自己搭一个文档站点,也可以使用目前主流的文档生成器(Docz、Storybook、VuePress)来生成文档站点。

这里我们采用的是自己搭建的一个单独的 React 项目,也就是上面的 docs 文件夹,其实自己搭也比较简单,首先是思路:

  • readFile 读取 src 下组件的 md 文件和 demo 文件保存起来
  • writeFile 写入路由配置以及对应的路由文件
  • 通过不同的路由渲染对应的组件文档和演示

主要借助的是 node 读写文件的便捷性,对于文档自动生成方案在后面文章会具体介绍一下,先明确一下思路就行。

打包

对于打包后的文件,统一放在 ems 目录下,顾名思义我们需要打包成 ESModule 的规范。

1
2
3
4
5
6
7
8
9
10
11
// packages.json
{
...
"scripts": {
"clean": "rimraf dist",
"build": "npm run clean && tsc -p tsconfig.build.json && gulp -f ./build/gulpfile.js", // 组件库打包
"start": "cross-env webpack-dev-server --config ./build/webpack.dev.js", // 组件开发调试环境
"build:umd": "cross-env NODE_ENV='production' webpack --config ./build/webpack.umd.js", // umd打包
},
...
}

编译打包 ts[x]

在入口文件我们需要以 ESModule 的规范导出组件:

1
export { default as Input } from "./components/Input";

打包工具的话我们就不能使用 webpack 来打包我们的组件库,可以使用 rollupTS 来编译组件,这里我们采用的是 TS 编译的方案,配置 tsconfig.build.json :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"compilerOptions": {
"outDir": "ems",
"target": "es6",
"module": "esnext",
"moduleResolution": "node",
"declaration": true, // 生成声明文件 .d.ts
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"jsx": "react"
},
"include": ["src"],
"exclude": [
// 排除编译文件
"node_modules",
"src/**/demo.tsx",
"**/*.md"
]
}

编译打包样式文件

上面我们已经成功编译了 tstsx 文件,但是对于我们的样式文件还没处理(TS 无法编译样式文件),样式文件根据我们上面的代码规范,需要打包编译到对应的组件文件夹下,这样就可以跟着组件路径来引入:

1
import "@hzzly/components/esm/input/style";

这里我们采用 gulp 来处理样式文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 生成css到对应组件
function scss2css() {
return gulp
.src(paths.styles)
.pipe(base64({ maxImageSize: 2000 }))
.pipe(sass()) // 处理sass文件
.pipe(autoprefixer()) // 根据browserslistrc增加前缀
.pipe(cssmin({ compatibility: "ie9" })) // 压缩
.pipe(gulp.dest(paths.dest.esm));
}

// 拷贝scss到对应的组件
function copyScss() {
return gulp
.src(paths.styles)
.pipe(base64({ maxWeightResource: 10000 }))
.pipe(gulp.dest(paths.dest.esm));
}

UMD

这里还有一个业务场景是 JsComponents 需要打包成 umd 的格式提供旧框架通过链接的方式直接引用,所以我们还配置了 umd 的打包规范,分别单独打包 js 原生组件, 将组件单独打包需要在 Webpack 中配置多个entry,大致配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const entry = {};

const names = fs
.readdirSync(path.resolve(__dirname, `../src/JsComponents`))
.filter((f) => f.indexOf('.') < 0);
names.forEach((name) => {
entry[name.toLocaleLowerCase()] = path.resolve(__dirname, `../src/JsComponents/${name}/index.ts`);
});

module.exports = {
...
entry,
output: {
path: path.resolve(__dirname, '../lib'),
filename: '[name].min.js',
publicPath: './',
},
...
}

迭代维护

CHANGELOG.md

组件日常维护占整个组件库生命周期的很大一部分,组件库做起来了以后,组件功能后续会不断迭代,也许是 bug fix,也可能 feature,这些组件的迭代我们通过 PR 和 issue 来管理,同时,我们需要管理好组件的 changelog,为了规范我们也是将 changelog 维护到一个 Markdown 文件里,通过 conventional-changelog 工具自动根据 commit message 生成 CHANGELOG.md**。

总结

到这里,也算是对最近在组件库的探索做了个总结,从零开始构建了一个比较完整的组件库,这段经历也是让我在架构思维以及业务层面有了新的认识,也学到了不少的知识。当然,还有很多不完善的地方,也是在慢慢优化完善,在组件化这条路上,我们还有很多事情要做,加油!!!

参考