前言
俗话说:“麻雀虽小,五脏俱全”,搭建一个组件库,知之非难,行之不易,涉及到的技术方方面面,犹如海面风平浪静,实则暗礁险滩,处处惊险~
目前团队内已经有较为成熟的 Vue 技术栈的 NutUI 组件库[1] 和 React 技术栈的 yep-react 组件库[2]。然而这些组件库大都从零开始搭建,包括 Webpack 的繁杂配置,Markdown 文件转 Vue 文件功能的开发,单元测试功能的开发、按需加载的 Babel 插件开发等等,完成整个组件库项目实属不易,也是一个浩大的工程。如果我们想快速搭建一个组件库,大可不必如此耗费精力,可以借助业内专业的相关库,经过拼装调试,快速实现一个组件库。
本篇文章就来给大家介绍一下使用 create-react-app 脚手架、docz 文档生成器、node-sass、结合 Netlify 部署项目的整个开发组件库的流程,本着包教包会,不会没有退费的原则,来一场手摸手式教学,话不多说,让我们进入正题:
首先看一下组件库的最终效果:
本文将从以下步骤介绍如何搭建一个 React 组件库:
一、构建本地开发环境
开发一个组件库的首要步骤就是调试本地 React 环境,我们直接使用 React 官方脚手架 create-react-app
,可以省去从底层配置 Webpack+TypeScript+React 的摧残:
1、使用 create-react-app 初始化脚手架,并且安装 TypeScript
npx create-react-app myapp --typescript
注意使用 node 为较高版本 >10.15.0
2、配置 eslint 进行格式化
由于安装最新的 create-react-app 结合 VScode 编辑器即可支持 eslit,但是需要在项目根目录中要添加 .env 这个配置文件,设置 EXTEND_ESLINT=true
这样才会启用 eslint 检测,注意要 重启 vscode
3、组件库系统文件结构
新建 styles 文件夹,包含了基本样式文件,结构如下:
|-styles
| |-variables.scss // 各种变量以及可配置设置
| |-mixins.scss // 全局 mixins
| |-index.scss // 引入全部的 scss 文件,向外抛出样式入口
|-components
| |-Button
| |-button.scss // 组件的单独样式
| |-button.mdx // 组件的文档
| |-button.tsx // 组件的核心代码
| |-button.test.tsx // 组件的单元测试文件
| |-index.tsx // 组件对外入口
4、安装 node-sass 处理器
安装 node-sass 用来编译 SCSS 样式文件:npm i node-sass -D
这样最基本的 react 开发环境就完成了,可以开心的开发组件了。
二、组件库打包编译
本地调试完组件库之后,需要打包压缩编译代码,供其他用户使用,这里我们用的 TypeScript 编写的代码,所以使用 Typescript 来编译项目:
首先在每个组件中新建 index.tsx 文件:
import Button from './button'
export default Button
修改 index.tsx 文件,导入导出各个模块
export { default as Button } from './components/Button'
在根目录新建 tsconfig.build.json,对 .tsx 文件进行编译:
{
"compilerOptions": {
"outDir": "dist",// 生成目录
"module": "esnext",// 格式
"target": "es5",// 版本
"declaration": true,// 为每一个 ts 文件生成 .d.ts 文件
"jsx": "react",
"moduleResolution":"Node",// 规定寻找引入文件的路径为 node 标准
"allowSyntheticDefaultImports": true,
},
"include": [// 要编译哪些文件
"src"
],
"exclude": [// 排除不需要编译的文件
"src/**/*.test.tsx",
"src/**/*.stories.tsx",
"src/setupTests.ts",
]
}
对于样式文件,使用 node-sass 编译 SCSS,抽取所有 SCSS 文件生成 CSS 文件:
"script":{
"build-css": "node-sass ./src/styles/index.scss ./dist/index.css",
}
并且修改 build 命令:
"script":{
"clean": "rimraf ./dist",// 跨平台的兼容
"build": "npm run clean && npm run build-ts && npm run build-css",
}
这样,执行 npm run build
之后,就可以生成对应的组件 JS 和 CSS 文件,为后面使用者按需加载和部署到 npm 上提供准备。
三、本地调试组件库
本地完成组件库的开发之后,在发布到 npm 前,需要先在本地调试,避免带着问题上传到 npm 上。这时就需要使用 npm link 出马了。
什么是 npm link
在本地开发 npm 模块的时候,我们可以使用 npm link 命令,将 npm 模块链接到对应的运行项目中去,方便地对模块进行调试和测试。
使用方法
假设组件库是 reactui 文件夹,要在本地的 demo 项目中使用组件。则在组件库中(要被 link 的地方)执行 npm link
,则生成从本机的 node_modules/reactui
到 组件库的路径 / reactui
中的映射关系。
然后在要使用组件库的文件夹 demo 中执行 npm link reactui
则生成以下对应链条:
在要使用组件的文件夹 demo 中 -[映射到]—> 本机的 node_modules/reactui
—[映射到]-> 开发组件库 reactui 的文件夹 /reactui
需要修改组件库的 package.json 文件来设置入口:
{
"name": "reactui",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
}
然后在要使用组件的 demo 项目的依赖中添加:
"dependencies":{
"reactui":"0.0.1"
}
注意,此时并不用安装依赖,之所以写上该依赖,是为了方便在项目中使用的时候可以有代码提示功能。
然后在 demo 项目中使用:
import { Button } from 'reactui'
在 index.tsx 中引入 CSS 文件
import 'reactui/build/index.css'
正当以为大功告成的时候,下面这个报错犹如一盆冷水从天而降:
经过各种问题排查,在 react 官方网站[3] 上查到以下说法:
Do not call Hooks in class components.
Do not call in event handlers.
Do not call Hooks inside functions passed to useMemo, useReducer, or useEffect.
说的很明白:
原因 1: React 和 React DOM 的版本不一样的问题
原因 2: 可能打破了 Hooks 的规则
原因 3: 在同一个项目中使用了多个版本的 React
官网很贴心,给出了解决方法:
This problem can also come up when you use npm link or an equivalent. In that case, your bundler might “see” two Reacts — one in application folder and one in your library folder. Assuming myapp and mylib are sibling folders, one possible fix is to run npm link ../myapp/node_modules/react from mylib. This should make the library use the application’s React copy.
核心思想在组件库中使用 npm link
方式,引到 demo 项目中的 react; 所以在组件库中执行: npm link ../demo/node_modules/react
具体步骤如下:
-
在代码库 reactui 中执行
npm link
-
在代码库 reactui 中执行
npm link ../../demo/node_modules/react
-
在项目 demo 中执行
npm link reactui
如此可以解决上面 react 冲突问题;于是可以在本地一边快乐的调试组件库,一边快乐的在使用组件的项目中看到最终效果了。
四、组件库发布到 npm
该过程一定要注意使用的是 npm 源!![非常重要]
首先确定自己是否已经登录了 npm:
npm adduser
// 填入用户名;密码;email
npm whoami // 查看当前登录名
修改组件库的 package.json ,注意 files 配置;以及 dependencies 文件的化简:
react 依赖原本是要放在 dependencies 中的,但是可能会和用户安装的 react 版本冲突,所以放在了 devDependencies 中,但是这样话用户如果没有安装 react 则无法使用组件库,所以要在 peerDependencies 中定义前置依赖 peerDependencies,告诉用户 react 和 react-dom 是必要的前置依赖:
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"files": [ // 把哪些文件上传到 npm
"dist"
],
"dependencies": { // 执行 npm i 的时候会安装这些依赖到 node_modules 中
"axios": "^0.19.1",// 发送请求
"classnames": "^2.2.6",//
"react-transition-group": "^4.3.0"
},
"peerDependencies": { // 重要!!,提醒使用者,组件库的核心依赖,必须先安装这些依赖才能使用
"react": ">=16.8.0", // 在 16.8 之后 才引入了 hooks
"react-dom": ">=16.8.0"
}
好了,整个组件库经过上述过程,基本上各个功能已经有了,提及一句:由于组件库使用的是 create-react-app 脚手架,最新的版本已经集成了单元测试功能。还有配置 husky 等规范代码提交,在这里不在做赘述,读者可以自行配置。
五、生成说明文档
目前生成说明文档较好的工具有 storybook[4]、docz[5] 等工具,两者都是很优秀的文档生成工具,但是尺有所短,寸有所长,经过认真调研比较,最终选择了 docz。
工具名称 | 区别一 | 区别二 |
---|---|---|
storybook | 使用特有的API开发文档说明,可以引入markdown文件 | 生成文档的界面带有storybook的痕迹较多一些 |
docz | 完美的结合了react和markdown语法开发文档 | 生成的文档界面是常规的文档界面 |
1、确定选型
1)storybook 的常用编译文档规范相对 docz 而言,略有繁琐
storybook 的编译文档规范如下所示:
//省略 import 引入的代码
storiesOf('Buttons', module)
.addDecorator(storyFn => <div style={{ textAlign: 'center' }}>{storyFn()}</div>)
.add('with text', () => (
<Button onClick={action('clicked')}>Hello Button111</Button>
),{
notes:{markdown} // 将会渲染 markdown 内容
})
对比 docz 的开发文档:
# Button 组件
使用方式如下所示:
import { Playground, Props } from 'docz';
import Button from './index.tsx';
## 按钮组件
<Playground>
<Button btnWidth="100">我是按钮</Button>
</Playground>
** 基本属性 **
| 属性名称 | 说明 | 默认值 |
|--|--|--|
|btnType | 按钮类型 |--|
众所周知,Markdown 是一种轻量级标记语言,它允许人们使用易读易写的纯文本格式编写文档。团队成员在开发文档时,熟练使用 markdown 语法,开发 docz 文档的 mdx 文件,结合了 Markdown 和 React 语法,相比 storybook 要使用很多的 API 来编写文档的方式,无疑减少了很多的学习 storybook 语法的成本。
2)docz 生成的文档样式更加符合个人审美
storybook 生成的文档样式,带有 storybook 的痕迹更为严重一些, 其生成文档界面如下所示:
docz 生成的文档图如下所示:
由上图对比可以看出,docz 生成的界面更加简介,较为常规。
综上,结合默认文档开发习惯和界面风格,我选择了docz,当然仁者见仁、智者见智,读者也可以使用同为优秀的 storybook 尝试,这都不是事儿~
2、使用 docz 开发
确定了 docz 进行开发后,根据官网介绍,在 create-react-app 生成的组件库中进行了安装配置:
npm install docz
安装成功后,就会向 package.json 文件中添加如下配置
{
"scripts": {
"docz:dev": "docz dev",
"docz:build": "docz build",
"docz:serve": "docz build && docz serve"
}
}
这时还需要在项目的根目录下新建 doczrc.js 文件,对 docz 进行配置:
export default {
files: ['./src/components/**/*.mdx','./src/*.mdx'],
dest: 'docsite', // 打包 docz 文档到哪个文件夹下
title: '组件库左上角标题', // 设置文档的标题
typescript: true, // 支持 typescript 语法
themesDir: 'theme', // 主题样式放在哪个文件夹下,后面会讲
menu: ['快速上手', '业务组件'] // 生成文档的左侧菜单分类
}
其中 files 规定了 docz 去对哪些文件进行编译生成文档,如果不做限制,会搜索项目中所有的 md、mdx 为后缀的文件生成文档,因此我在该文件中做了范围限制,避免一些 README.md
文件也被生成到文档中。
此外还需要注意到两点:
1、menu: ['快速上手', '业务组件']
对应着组件库左侧的菜单栏分类,比如在 mdx 文档中在最上面设置组件所属的菜单 menu: 业务组件
, 则 Button 组件属于 "业务组件" 的分类:
---
name: Button
route: /button
menu: 业务组件
---
在 src 中新建欢迎页,路由为跟路径,所属菜单为“快速上手”;
---
name: 快速上手
route: /
---
执行 npm run docz:dev
,就可以打开
介绍到这里,估计有小伙伴会有疑问了,这样生成的网站千篇一律,能否随心所欲的自定义网站的样式和功能呢?当初我也有这种疑问,经过多次尝试,皇天不负苦心人,终于摸索出如下方法:
1、修改 docz 文档本身的样式
根据 docz 官方文档中增加 logo 的方法[6],可以通过自定义组件覆盖原有组件的形式:
Example: If you're using our gatsby-theme-docz which has a Header component located at src/components/Header/index.js you can override the component by creating src/gatsby-theme-docz/components/Header/index.js. Cool right?
所以根据 docz 源代码主题部分代码: https://github.com/doczjs/docz/tree/master/core/gatsby-theme-docz/src
,找到对应的文档组件的代码结构,在组件库项目根目录新建同名称的文件夹:
|-theme
| |-gatsby-theme-docz
| |-components
| |-Header
| |-index.js // 在这里修改自定义的文档组件
| |-styles.js // 在这里修改生成的样式文件
这样在执行 npm run docz:dev
的时候,就会把自定义的代码覆盖原有样式,实现文档的多样化。
2、修改 markdown 文档样式
事情到这里就结束了吗?不!我们的目标不仅如此,因为我发现自动生成的 markdown 格式,并不符合我的审美,比如生成的表格文字居左对齐,并且整个表格样式单一,但是这里属于 markdown 样式的范畴,修改上述文档组件中并不包括这里的代码,那么如何修改 markdown 生成文档的样式呢?
经过我灵机一动又一动,发现既然在上面修改文档组件样式的时候,重写了 component/Header/styles.js 文件,是否可以在该文件中引入自定义的样式呢?文件结构如下:
|-theme
| |-gatsby-theme-docz
| |-components
| |-Header
| |-index.js // 在这里修改自定义的文档组件
| |-styles.js // 在这里修改生成的样式文件
| |-base.css // 这里修改 markdown 生成文档的样式
这样修改后的表格样式如下:
接下来各位小主可以根据自己的审美或者视觉设计的要求自定义文档的样式了。
六、部署文档到服务器
生成的组件库文档只在本地显示是没有意义的,所以需要部署到服务器上,于是第一时间想到的是放在 github 进行托管,打开 github 中的 setting 设置选项,GitHub Pages 设置配置的分支:
这时默认打开的首页路径为:
https://plusui.github.io/plusReact/
但实际上页面有效的访问地址是带有文件夹 docsite 路径的:
https://plusui.github.io/plusReact/docsite/button/index.html
此外,页面引入的其他资源路径,都是绝对路径,如下图资源路径所示:
所以直接把打包后的资源放在 github 上是无法访问各种资源的。
这时我们只好把网站部署到云服务器上了,考虑到服务器配置的繁琐,这里给大家提供一个简便的部署网站:Netlify[7]
Netlify 是一个提供静态网站托管的服务,提供 CI 服务,能够将托管 GitHub,GitLab 等网站上的 Jekyll,Hexo,Hugo 等静态网站。
部署项目的过程也很简单,傻瓜式的点击选择 github 网站中代码路径,以及配置文件夹跟路径,如下图所示:
然后就可以点击生成的网站 url,访问到部署的网站了:
而且很方便的是,一旦完成部署之后,之后再次向代码库中提交代码,Netlify 会自动更新网站。
此外,如果想自定义 url,那么就只能去申请域名了,在自己的云服务器上,解析域名即可。下面简单说一下配置步骤:
1)首先在 Netlify 网站上,选择组件库对应的 Domain settings 下 Custom domains,增加自己的域名:
2)然后打开云服务器中的域名解析中的解析设置,将该域名指向 Netlify:
3)最后打开设置的网址,就可以访问到组件库了:
七、组件按需加载
好了,经过上面的流程,可以在 demo 项目中使用组件库了,但是在 demo 项目中,执行 npm run build
,就会发现生成的静态资源中即使只使用了一个组件,也会把 reactui 组件库中所有的组件打包进来。
所以如何进行按需加载呢?
按需加载首先映入脑海的是使用 babel-plugin-import
插件, 该插件可以在 Babel 配置中针对组件库进行按需加载.
用户需要安装 babel-plugin-impor
插件,然后在 plugins 中加入配置:
"plugins": [
[
"import",
{
"libraryName": "reactui", // 转换组件库的名字
"libraryDirectory": "dist/components", // 转换的路径
"camel2DashComponentName":false, // 设置为 false 来阻止组件名称的转换
"style":true
}
]
]
这样在 demo 项目中使用如下方式:
import { Button } from 'reactui';
就会在 babel 中编译成:
import { Button } from 'reactui/dist/components/Button';
require('reactui/dist/components/Button/style');
但是这样还有些弊端:
1、 用户在使用组件库的时候还需要安装 babel-plugin-import
, 并做相关 plugins 配置;
2、 开发组件库的时候组件对应的样式文件还需要放在 style 文件夹下;
那有没有更为简单的方法呢?在 ant-design 中寻找答案,发现这样一句话 “antd 的 JS 代码默认支持基于 ES modules 的 tree shaking”。 对呀!还可以使用 webpack 的新技术“tree shaking”。
什么是 tree shaking? AST 对 JS 代码进行语法分析后得出的语法树 (Abstract Syntax Tree)。AST 语法树可以把一段 JS 代码的每一个语句都转化为树中的一个节点。DCE Dead Code Elimination,在保持代码运行结果不变的前提下,去除无用的代码。
webpack 4x 中已经使用了 tree shaking 技术,我们只需要在 package.json 文件中配置参数 "sideEffects": false
,来告诉 webpack 打包的时候可以大胆的去掉没有用到的模块即可。这时用户在 demo 项目中使用组件库的时候不需要做任何处理,就可以按需引用 JS 资源了。
不知道大家在看到这里时,是否发现这样配置还是有问题的:即 sideEffects 配置成 false 是有问题的。
因为按照上述配置,就会发现组件的样式不见了!!
经过排查,原因是引入 CSS 样式的代码:import './button.scss'
,可以看到相当于只是引入了样式,并不像其他 JS 模块后面做了调用,在 tree shaking 的时候,会把 css 样式去掉。所以在配置 sideEffects 就要把 CSS 文件排除掉:
"sideEffects": [
"*.scss"
]
通过上述 tree shaking 的方法,可以实现组件库的按需加载功能,打包的文件去除了没有用到的组件代码,同时省去了用户的配置。
八、样式按需加载
通常来说,组件库的 JS 是按需加载的,但是样式文件一般只输出一个文件,即把组件库中的所有文件打包编译成一个 index.css 文件,用户在项目中引入即可;但是如果就是想做按需加载组件的样式文件,该如何去做呢?
这里我提供一种思路,由于 .tsx 文件是由 TS 编译器打包编译的,并没有处理 SCSS,所以我使用了 node-sass 来编译 SCSS 文件,如果需要按需加载 SCSS 文件,则每个组件的 index.tsx 文件中就需要引入对应的 SCSS 文件:
import Button from './button';
import './button.scss';
export default Button;
生成的 SCSS 文件也需要打包到每个组件中,而不是生成到一个文件中:
所以使用了 node-sass 中的 sass.render 函数,抽取每个文件中的样式文件,并打包编译到对应的文件中,代码如下所示:
//省略 import 引入,核心代码如下
function createCss(name){
const lowerName = name.toLowerCase();
sass.render({ // 调用 node-sass 函数方法,编译指定的 scss 文件到指定的路径下
file: currPath(`../src/components/${name}/${lowerName}.scss`),
outputStyle: 'compressed', // 进行压缩
sourceMap: true,
},(err,result)=>{
if(err){
console.log(err);
}
const stylePath = `../dist/components/${name}/`;
fs.writeFile(currPath(stylePath+`/${lowerName}.scss`), result.css, function(err){
if(err){
console.log(err);
}
});
});
}
这样就在生成的 dist 文件中的每个组件中增加了 SCSS 文件,用户通过“按需加载小节”中的方法在引入组件的时候,会调用对应的 index 文件,在 index.js 文件中就会调用对应的 SCSS 文件,从而也实现了样式文件的按需加载。
但是这样还有一个问题,就是在开发组件库的时候每个组件中的 index.tsx 文件中引入的是 SCSS 文件 import './button.scss';
,所以 node-sass 编译后的文件需要是 SCSS 后缀的文件(虽然已经是 CSS 格式),如果生成的是 CSS 文件,则用户在使用组件的时候就会因找不到 SCSS 文件而报错,也就是用户在使用组件的时候,也需要安装 node-sass 插件。
不知大家有没有更好的办法,在组件库开发的时候使用的是 SCSS 文件,编译后生成的是 CSS 后缀的文件,在用户使用组件的中调用的也是 CSS 文件呢?欢迎在文末留言讨论~
结语
以上就是整个搭建组件库的过程,从一开始决定使用现有的 create-react-app 脚手架和 docz 来构成核心功能,到文档的网站部署和 npm 资源的发布,最初感觉应该能够快速完成整个组件库的搭建,实际上如果要想改动这些现有的库来实现自己想要的效果,还是经历了一些探索,不过整个摸索过程也是一种收获和乐趣所在,愿走过路过的小伙伴能有所收获~
参考文章
[1] NutUI 组件库: http://nutui.jd.com/#/index
[2] yep-react 组件库: http://yep-react.jd.com
[3] react 官方网站: https://reactjs.org/warnings/invalid-hook-call-warning.html
[4] storybook: https://storybook.js.org/
[5] docz: https://www.docz.site/
[6] docz 官方文档: https://www.docz.site/docs/gatsby-theme
[7] Netlify: https://app.netlify.com/teams/zhenyulei/sites
[8]基于 Storybook 5 打造组件库开发与文档站建设小结: http://jelly.jd.com/article/5f06fe8505541b015b6a708a