前言
随着企业内部开发的项目逐渐增多,组件的维护工作变得愈益困难.
前端同学通常会面临的困境之一.开发完了A
项目,在A
项目下封装了大量的公共组件.当B
项目启动时,由于设计风格相似,以至于A
项目的很多组件可以直接复制到B
项目下使用.
A
和B
项目开发完毕,后续的C
和D
项目又启动.每当有新项目启动时,手工复制就要重复一遍,这样的工作不仅乏味而且低效.
如果仅仅只是在新项目启动时,将旧项目的组件代码复制一遍还能勉强接受.
倘若有一天,产品经理要求D
项目下的某个公共组件做一下样式的调整,并且A
、B
和C
项目下的对应的公共组件同步更新,那么这样的机械劳动会随着项目数量的增多变得无休无止.
本文以企业已产出的项目为视角,将之前开发好的react
组件抽离出来封装成业务型组件库,而不是打造一款通用型的组件库,抽离出来的业务型组件库和通用型组件库有哪些区别呢?
- 我们打造的组件库以业务组件为主,服务的对象是企业内部.因而我们开发的组件库可能会包含很多业务图片,比如公司的
logo
. - 一般而言,通用型组件库的所有组件都从
0
实现.但业务型组件不一样,它很多功能都是依赖于第三方组件库(比如antd
、Element UI
)做的二次开发.因此业务型的组件库很可能集成了第三方组件库依赖.
源代码在文章末尾,可下载启动运行.
实现
项目搭建
首先本地搭建一个空项目,里面创建文件夹src/components
,需要封装的组件都可以复制到components
下面(如下图所示);
上图案例封装了四个组件,另外还单独创建了一个入口文件index.ts
.
index.ts
内部分别将components
下的所有组件导入并暴露给外部调用.
// index.ts文件
export { default as Button } from "./Button";
export { default as Icon } from "./Icon";
export { default as Logo } from "./Logo";
export { default as Empty } from "./Empty";
我们看下components/Button
组件的文件结构(如下图).
Button
组件包含两个文件,一个是less
样式文件,另一个是逻辑代码index.tsx
.
Button
组件的index.tsx
内容很简单,它引入同级的less
样式,并编写了组件逻辑,最后导出提供给外部使用.
import React from 'react';
import './index.less';
type defaultProps = {
text: string; //按钮的文案
onClick?: any; //点击事件
};
const Button = (props: defaultProps) => {
return (
<div onClick={props.onClick}>
{props.text}
</div>
);
};
export default Button;
文件结构介绍完毕,接下来编写webpack
的配置文件将组件库代码打包输出(代码如下).
webpack
配置文件的思路很简单,入口entry
设置为组件库源代码src/components
下对外暴露的入口文件index.ts
,输出output
设置成dist
文件夹下.
rules
里面主要处理tsx
和less
文件.tsx
文件是所有待封装组件的逻辑代码,因为组件的代码是使用ts
语法编写的,因而先使用ts-loader
将ts
语法转换后js
,再使用babel
去编译(具体配置细节可在源代码中查看).
less
文件要先通过less-loader
将less
语法编译成css
,再使用postcss
使用插件给css
加上满足各大浏览器兼容性的前缀.
postcss
处理完传递给css-loader
处理,最后使用MiniCssExtractPlugin
插件将所有样式文件抽离出来放入app.css
中.
// webpack.config.js文件
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
entry:path.resolve(__dirname,"src/components/index.ts"), //入口
mode:"development",
output: { //输出
library: 'ui-design-demo',
libraryTarget: 'umd',
filename: 'bundle.js',
path:path.resolve(__dirname,"dist")
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
module:{
rules: [
{
test: /\.tsx?$/,
use: ['babel-loader','ts-loader'],
exclude: /node_modules/,
},
{
test: /\.(le|c)ss$/i,
use: [
{
loader:MiniCssExtractPlugin.loader
},
{
loader: "css-loader"
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: ['autoprefixer'],
}
},
},
// 将 less 编译成 CSS
'less-loader'
],
}
]
},
plugins:[
new MiniCssExtractPlugin({
filename:"app.css"
})
]
};
webpack
最终打包生成的文件如下图所示.所有组件的js
代码都封装到bundle.js
,而样式文件被单独抽离放入app.css
.
如果将dist
目录发布到npm
仓库,其他项目该如何引入该组件库呢?
调试引入
为了让其他项目能够引入当前封装的组件库,首先要在组件库根目录下的package.json
修改两个字段(如下图所示)
name
是自定义组件库的名称,其他项目通过import
或者require
引入组件库中的组件时,name
对应的就是被引入的组件库名称.
main
设定了当前组件库的入口文件地址.
组件库的代码并不是开发完后就一成不变的,它可能随时会依据现实情况做出功能或样式的调整.
如果修改了组件库的代码,发布到npm
仓库再让其他项目安装使用,过程过于繁琐.
npm link
可以简化这一流程直接在本地调试组件库的代码.
本地桌面上重新创建一个新的react
测试项目,完成以下三步可直接在测试项目中引入组件库.
- 在组件库的根目录下执行
npm link
命令,将当前组件库链接到全局 - 组件库的根目录下执行命令链接到测试项目下的
react
(不执行这一步可能会报错),比如npm link D:\react-ui-test\node_modules\react
- 在测试项目
react-ui-test
根目录下,执行命令npm link ui-design-demo
链接组件库,ui-design-demo
就是上面定义的name
执行完上述三步后,测试项目就可以直接引入组件库中的组件进行测试了(代码如下).
// App.js 文件
import React from "react";
import { Button } from "ui-design-demo";
import "ui-design-demo/dist/app.css";
function App() {
const clickEvent = () => {
console.log(123);
}
return (
<div className="App">
Hello world
<Button call="按钮" onClick={clickEvent}/>
</div>
);
}
export default App;
由于组件库的css
代码被单独抽离成了app.css
,因此测试项目想使用组件库一般要在项目入口文件index.js
处将app.css
给引入进来(上面代码为了方便看就在App
组件引入了css
).
测试项目启动后运行效果图如下.
有了npm link
的赋能,组件库的调试变得容易.测试项目可以是临时创建的新项目,也可能是已经存在的真实项目.
它们都可以在本地直接启动引入组件库,如果发现组件库的组件有需要调整的地方,直接在组件库里修改编译,测试项目这边可以实时刷新观察效果.
图标处理
一般而言,组件内不可能只包含js
和css
.我们日常开发的很多组件都会用到图标,图标的用途主要有以下两方面.
- 组件库内部的组件需要使用图标
- 外部项目引用组件库时,需要使用组件库对外暴露的图标
为了同时满足上述两点要求,我们在组件库源代码src/components
下新建一个组件Icon
(如下图所示)
Icon
下的less
文件代码如下,直接引入从iconfont
下载的字体图标样式.
// index.less文件
@import url('../../fonts/iconfont.css');
Icon
下的index.tsx
文件代码如下,封装了一个Icon
组件提供给外部使用.
import React from 'react';
import './index.less';
interface defaultProps {
name:string;
size?:number | string;
}
const Icon = (props: defaultProps)=>{
return <i className={`iconfont ${props.name}`} style={{fontSize:`${props.size?props.size:12}px`}}></i>;
}
export default Icon;
紧接着源代码src/components
下的入口文件index.ts
要将Icon
导出,最后修改一下webpack
的配置文件.
配置文件webpack.config.js
要在原来的基础添加上对字体文件的处理,配置完后打包生成的dist
目录下会多出一个fonts
文件夹,专门用来存放字体文件.
// webpack.config.js 文件
... //省略
module:{
rules: [
...,
{
test:/\.(ttf|woff|woff2)$/,
loader:"file-loader",
options: {
name: '[name].[ext]',
outputPath: 'fonts'
}
}
]
}
...
上述工作完成后,测试项目中可以直接引入字体图标进行使用,代码如下.
import React from "react";
import {Button,Icon} from "ui-design-demo";
import "ui-design-demo/dist/app.css";
function App() {
const clickEvent = () => {
console.log(123);
}
return (
<div className="App">
Hello world
<Button call="按钮" onClick={clickEvent}/>
<Icon name="icon-zengjia" size="50"/>
</div>
);
}
export default App;
效果图如下:
图片处理
组件库现在需要增加一个业务组件,用于展现公司的Logo
,这时候需要组件库将图片集成进来.
组件库源代码src/components
下新建组件Logo
,文件结构如下图所示.
Logo
内index.tsx
和样式代码如下.类名为Logo
的div
需要展现一张图片,index.less
使用背景图的方式引入了图片.
这里为什么不在index.tsx
文件内直接使用import
将图片引入进来?因为组件库使用webpack
编译打包后图片路径发生了改变,测试项目引入该组件时可能会因为图片路径不对找不到图片.
但less
通过background
加载背景图的方式可以避免图片路径出错的问题.
这是由于css
在组件库中虽然被webpack
编译了一次,但测试项目的入口文件会引入组件库全局css
,那时测试项目的webpack
还会对css
编译一次,这样就保证了图片路径不会出错.
// index.tsx文件
import React from 'react';
import "./index.less";
const Logo = () => {
return (
<div className="Logo">
</div>
);
};
export default Logo;
------------------------------------
// index.less文件
.Logo {
width: 133px;
height: 114px;
background: url(../../images/logo.png) no-repeat;
}
另外webpack
的配置文件需要增加对图片的处理(代码如下).
// webpack.config.js文件
...
module:{
rules: [
... // 省略
{
test:/\.(gif|jpg|png)$/,
loader:"file-loader",
options: {
name: '[name].[ext]',
outputPath: 'images'
}
}
...
]
}
上述配置做完后,最终打包生成的dist
目录多出一个images
文件夹,专门用来存放组件库下的业务图片(如下图).
加载第三方组件库
如果组件库依赖了第三方组件库,比如ant design
,首先需要在组件库的根目录下使用npm install antd --save
安装依赖库.
antd
安装完后,就可以在组件库下直接使用了.比如在源代码src/components
下新建组件Empty
(代码如下).
// Empty/index.tsx文件
import React from 'react';
import { Empty as Ant_Empty } from 'antd';
const Empty = () => {
return (
<Ant_Empty image={Ant_Empty.PRESENTED_IMAGE_SIMPLE} />
);
};
export default Empty;
Empty
组件是我们基于antd
做的二次封装,我们要在src/components
下的入口文件index.ts
导出.另外入口文件还需要引入antd
的样式文件,并在文件末尾将antd
内部的组件全部导出.
import 'antd/dist/antd.css';
export { default as Button } from "./Button";
export { default as Icon } from "./Icon";
export { default as Logo } from "./Logo";
export { default as Empty } from "./Empty";
export * from "antd";
上述修改完后,组件库的配置文件webpack.config.js
还需要添加一项,将react
和antd
排除,这样我们打包生成bundle.js
体积变得轻量.
module.exports = {
...
externals: {
antd: "antd",
react: "react"
}
...
通过上述配置后,测试项目引入组件库后不再需要安装antd
,测试项目可以使用组件库的自定义组件,也可以直接使用antd
下的组件(代码如下).
// App.js文件(测试项目)
import React from "react";
import {Button,Icon,Logo,Switch} from "ui-design-demo";
import "ui-design-demo/dist/app.css";
function App() {
const clickEvent = () => {
console.log(123);
}
return (
<div className="App">
Hello world
<Button call="按钮" onClick={clickEvent}/>
<Icon name="icon-zengjia" size="50"/>
<Logo/>
<Switch/>
</div>
);
}
测试项目可直接从ui-design-demo
中引用antd
下定义的组件Switch
使用,但要注意ui-design-demo
下的自定义组件不要和antd
下的组件重名(效果图如下).
按需加载
以上介绍的都是组件库的全量加载,这一小节我们探讨一下按需加载的具体实现.
如果在测试项目中使用按需加载的方式引入组件库该如何书写呢?(代码如下)
按需加载的含义就是用到哪个组件就只加载该组件的对应的js
和css
即可,下面代码手动引入了Button
组件下的js
和css
.
这样一来组件库只有Button
组件的代码被测试项目引用,测试项目打包后体积会因此变得小很多.
有的同学会说,为什么antd
的按需加载不用写的这么麻烦,它的书写方式和全量加载一样的,根本不需要手动引入css
?
这是由于babel-plugin-import
这个插件发挥了功能,即使你按照全量加载的方式书写,babel-plugin-import
会自动将语法编译成下面类似的形式.
import React from "react";
import Button from "ui-design-demo/es/Button";
import "ui-design-demo/es/Button/style.css";
function App() {
const clickEvent = () => {
console.log(123);
}
return (
<div className="App">
Hello world
<Button call="按钮" onClick={clickEvent}/>
</div>
);
}
export default App;
现在理解了按需加载的运行机制,那么组件库打包后应该为每一个组件都要创建一个单独的文件夹,并将它们的js
和css
放在一起(如下图所示).
webpack
想要根据组件的个数生成多个目录,入口entry
需要动态生成.output
也要根据组件名称创建单独的文件夹,MiniCssExtractPlugin
插件将每个组件的css
代码抽离出来放入该组件对应的文件夹下.
// webpack.lazy.config.js文件
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const glob = require("glob");
module.exports = (async function () {
const entries = await getEntries();
return {
entry: entries,
mode: "development",
externals: {
antd: "antd",
react: "react"
},
output: {
library: '[name]',
libraryTarget: 'umd',
filename: '[name]/index.js',
path: path.resolve(__dirname, "es")
}
module:{
... //省略
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name]/style.css"
})
]
};
}())
/**
* 获取按需加载的入口
*/
function getEntries () {
return new Promise((resolve) => {
const module = {};
glob("./src/components/**/*.tsx", (err, files) => {
files.forEach((file) => {
const array = file.split("/");
const name = array[array.length - 2];
module[name] = file;
})
console.log(module);
resolve(module);
})
})
}
getEntries();
组件文档
组件库的文档可以采用docz
自动生成,docz
的githup
地址如下:
https://github.com/doczjs/docz
根据docz
官方文档的说明,按照要求一步步做好相关的配置,最后在每一个组件目录下新建一个组件描述文件(代码如下).
// src/components/Button/Button.mdx 文件
---
name: 按钮
route: /Button
menu: 业务组件
---
import { Playground } from 'docz'
import Button from './index.tsx'
import './index.less'
# Button 按钮
button 按钮
## 基本用法
<Playground>
<Button call="按钮" />
</Playground>
输入命令启动docz
就可以查看组件库的在线文档了,如下图所示.