本文介绍一个基本的组件库需要满足哪些要求,重点介绍了基于umi father构建react组件库的关键点,并提供了示例工程。
WHAT
一个UI组件库的基本要求
支持多种格式
支持umd
cjs
esm
,当然就现在前端开发而言, umd
的支持与否并不是那么重要。
TypeScript
完整的类型定义,支持静态检查。都2020年了,不支持typescript是不是有点说不过去了。
支持全量引入
import { ComponentA } from 'package';
import 'package/dist/index.min.css;
支持按需引入
组件库能够默认支持基于 ESM 的 tree shaking,也能够通过babel-plugin-import实现按需加载。
基于 ESM 的 tree shaking 并不会实现 css 的按需加载,仍然需要手动引入css文件。当然,这里存在一些例外情况,比如,你采用 css in js 的方案。
// babel plugin config
{
plugins: [
...otherPlugins,
[import, {
libraryName: 'package',
style: true,
libraryDirectory: 'lib',
}, 'package'],
]
}
支持主题定制
主题定制与组件库的css方案相关,一般来说,可以通过以下方案实现主题定制:
- sass/less 变量 - antd采用的就是这种方式,但不建议采用这种方式,原因在于应用部署后,这种方式在线切换主题时需要在线解析sass/less,开销大,体验较大
- 采用 css in js 的方案,主题定制即是天然而成的
- css变量 - 如果不考虑IE的话,这是相当推荐的方案
document.documentElement.style.setProperty('--primary-color', '#0170fe');
单元测试
文档
css解决方案
详细待述
- css in js,比如 styled-component
- 预处理,比如less / sass
HOW
开发React组件库有很多方式,推荐使用 umi father, 集组件打包与文档于一体,对于开发组件库十分方便。然后,尽管umi father开发组件库十分便利,但是,要实现上述功能,仍然需要一些额外工作,本节的重点即是这些额外工作。
核心点
babel-plugin-import做了什么
import { PackageA } from 'package';
// 通过babel-plugin-import配置(style配置为true, libraryDirectory配置为lib)后等同于
import PackageA from 'package/lib/package-a';
import PackageA from 'package/lib/package-a/style';
基本原则
- 每个组件不引入本组件的样式文件
- 在全局样式文件内引入各个组件的样式文件
- 使用babel来打包为cjs/esm格式,样式文件不处理,原样输出到目录,此时生成包也不会包含ts类型文件
- 打包为umd格式时,配置两个入口文件(样式入口文件、组件入口文件),最终输出提取为两个文件(组件文件js和样式文件css)以及类型定义文件 - umd的生成文件除了可以以
script
的方式引入外,还有两个作用:1)提供类型定义文件 2)采用npm方式进行工程开发时,采用全量引入的方式时,需要使用umd提取的css文件作为样式文件引入(因为每个组件本身是没有引入任何样式文件的)
package.json
{
"types": "dist/index.d.ts", // 指定ts类型文件入口
"main": "lib/index", // 指定cjs方式入口
"module": "es/index" // 指定esm方式入口
}
因此,对应的生成文件分别对应 dist
lib
es
三个目录
组件关键目录
基于基本原则,设计此目录结构
.
├── package-a // 组件 PackageA 目录
│ ├── __test__ // 单元测试文件目录
│ │ ├── index.spec.tsx // 以 .spec|test.tsx 结尾
│ ├── style // 样式文件目录
│ │ ├── index.less
│ │ ├── index.ts
│ ├── index.tsx
├── package-b // 组件 PackageB 目录
├── style // 样式文件
│ ├── core // 核心文件主要包括mixin/function等
│ │ ├── mixin.less
│ ├── component.less // 所有组件的样式文件入口
│ ├── entry.less // 样式打包入口less
│ ├── entry.ts // 样式打包入口
│ ├── index.less
│ ├── index.ts // 用于单个组件样式引用
│ ├── theme.less // 默认主题
└──
打包配置
建议esm
cjs
umd
分开打包,也就最终执行三次father build
esm打包配置
只编译ts(x)/js(x),不处理less样式文件,样式文件保持原样到输出目录。
// .fatherrc.ts
export default {
runtimeHelpers: true,
entry: 'src/index.ts',
esm: {
type: 'babel',
},
lessInBabelMode: false,
}
cjs打包配置
基本与esm相同
// .fatherrc.ts
export default {
runtimeHelpers: true,
entry: 'src/index.ts',
esm: {
type: 'cjs',
},
lessInBabelMode: false,
}
umd打包配置
umd打包配置存在较大不同,需要配置多入口,除了src/index.ts
入口外,还需以src/style/entry.ts
(样式文件)为入口,umi father将跟进 entry 把项目依赖打包在一起输出一个文件。
// .fatherrc.ts
export default {
entry: ['src/index.ts', 'src/style/entry.ts'],
autoprefixer: {
flexbox: 'no-2009',
},
extractCSS: true,
runtimeHelpers: true,
umd: {
globals: {
react: 'React',
},
minFile: true,
},
overridesByEntry: {
'src/index.ts': {
umd: { name: 'ufs', file: 'index' },
},
'src/style/entry.ts': {
umd: { file: 'entry' },
},
},
}
在打包完成后,我们还需要一些重命名、删除多余文件等操作
// script/build-umd.js
const fs = require('fs-extra');
const util = require('util');
const { exec } = require('child_process');
const path = require('path');
const execSync = util.promisify(exec);
const getRelativePath = pathStr => path.join(__dirname, pathStr);
const build = async () => {
console.info('Build umd');
await execSync('father build');
await fs.removeSync(getRelativePath('../dist/entry.js'));
await fs.removeSync(getRelativePath('../dist/entry.min.js'));
await fs.moveSync(getRelativePath('../dist/entry.css'), getRelativePath('../dist/index.css'));
await fs.moveSync(getRelativePath('../dist/entry.min.css'), getRelativePath('../dist/index.min.css'));
};
build();
单元测试
umi father 集成了 jest 用于单元测试,但针对react组件,还需要一些额外配置。
// jest.config.js
module.exports = {
setupFiles: ['<rootDir>/setupTests.ts'],
// testEnvironment: 'node', // 测试环境可选值为 jsdom(default)/node
};
// setupTests.ts
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
// import 'jsdom-global/register'; // 如果测试环境为node,还需要引入jsdom
configure({ adapter: new Adapter() });
// 一个简单的测试示例
// 测试文件应以 .test|spec.ts(x)|js(x) 结尾
import React from 'react';
import { render } from 'enzyme';
import toJSON from 'enzyme-to-json';
import Hello from '../index';
describe('Hello Component', () => {
it('test render', () => {
const wrapper = render(<Hello />);
expect(toJSON(wrapper)).toMatchSnapshot();
});
});
文档
umi father 包括两块,文档(cli命令为father doc,等同于dumi)和组件打包(cli命令为father build,等同于father-build)。
这一块的内容不详述,参考 dumi
示例工程
Q&A
待述
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。