React-组件库构建
组件打包前置知识
1. 模块规范
在 JavaScript 生态里,代码组织方式有不同的模块化规范,最常见的有以下三种:
CommonJS(CJS)
require()
语法主要用于 Node.js 运行环境
代码示例:
1
2const lodash = require('lodash');
console.log(lodash);缺点:不适用于浏览器,不能 Tree Shaking(优化打包时移除未使用的代码)。
ES Modules(ESM)
import/export
语法现代 JavaScript 规范,适用于浏览器和 Node.js
代码示例:
1
2import { debounce } from 'lodash';
console.log(debounce);优势:支持 Tree Shaking,适合前端打包优化。
UMD(Universal Module Definition)
兼容 CommonJS、AMD(另一种浏览器模块规范)和全局变量
适用于在
<script>
直接引入代码示例:
1
2
3
4
5
6
7
8
9(function (root, factory) {
if (typeof module === "object" && module.exports) {
module.exports = factory();
} else {
root.myLibrary = factory();
}
})(this, function () {
return { foo: "bar" };
});适用于 CDN 直接引入,如:
1
<script src="https://cdn.jsdelivr.net/npm/lodash/lodash.min.js"></script>
2. 编译工具
组件库源码一般使用 TypeScript (TS) 和 JSX,需要转换成浏览器或 Node.js 可运行的 JavaScript。这里涉及两个重要工具:
Babel
主要用于 ES6+ 语法转换,让新语法兼容老浏览器。
组件库通常用它来编译 ESM 和 CJS 代码(即
es
和lib
目录)。配置示例:
1
2
3{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}作用:
- 把 JSX 转换成
React.createElement()
语法 - 把 ES6+ 代码转换成更低版本的 JavaScript
- 把 JSX 转换成
TSC(TypeScript Compiler)
- TypeScript 自带的编译器,主要用于类型检查和代码转换。
- 可以直接编译 TypeScript 到 JavaScript,不负责转换新语法(比如 JSX)。
- 组件库通常用它来编译 TypeScript 并生成
.d.ts
类型声明文件。
3. 打包工具
打包工具的目的是将代码转换成不同的模块格式(CJS、ESM、UMD),博客中提到了以下两种工具:
Webpack
主要用于打包 UMD 代码,通常用于浏览器端的组件库。
例如:
- 你想发布一个 UI 组件库,并让别人直接用
<script>
引入,这就需要 Webpack 生成 UMD 版本。
- 你想发布一个 UI 组件库,并让别人直接用
典型配置:
1
2
3
4
5
6module.exports = {
output: {
library: "MyLibrary",
libraryTarget: "umd"
}
};
Gulp
任务自动化工具,常用于编排多个编译任务。
例如:先用 TSC 编译,再用 Babel 处理。
典型代码:
1
2
3
4
5
6
7
8
9
10
11const gulp = require('gulp');
gulp.task('build-cjs', function () {
return gulp.src('src/**/*.ts').pipe(tsc()).pipe(gulp.dest('lib'));
});
gulp.task('build-esm', function () {
return gulp.src('src/**/*.ts').pipe(babel()).pipe(gulp.dest('es'));
});
gulp.task('default', gulp.parallel('build-cjs', 'build-esm'));
4. 组件库的构建流程
现在,我们回到博客的核心内容:Ant Design、Arco Design、Semi Design 组件库是如何打包的?
1️⃣ 目录结构
博客分析了 antd
、semi-ui
、arco-design
的 node_modules
目录:
1 | lib/ --> CJS 格式 |
这意味着:
- lib(CommonJS) 用于 Node.js
- es(ES Modules) 用于前端打包
- dist(UMD) 用于
<script>
引入
2️⃣ 构建过程
博客中的 Arco Design 例子说明了组件库如何打包:
ESM 和 CJS 代码
先用 TSC 或 Babel 编译
tsx
和ts
TSC
负责 类型检查Babel
负责 转换新语法(如 JSX)产物存放在
lib/
(CJS) 和es/
(ESM)关键代码:
1
2compileTS("es"); // 生成 ESM
compileTS("cjs"); // 生成 CJS
UMD 代码
用 Webpack 打包成
dist/
主要用于
unpkg
(CDN)引入关键代码:
1
webpack({ output: { libraryTarget: "umd" } });
CSS 处理
- Arco 用 Less
- Semi Design 用 SCSS
- 通过
gulp
编译
5. 总结
- 为什么组件库要支持不同模块格式?
- ESM(ES6 模块)适用于现代前端打包工具(如 Webpack、Vite)
- CJS(CommonJS)适用于 Node.js 环境
- UMD(通用模块)适用于
<script>
引入(CDN)
- 组件库的编译过程
- ESM 和 CJS → TSC 或 Babel 编译
- UMD → Webpack 打包
- CSS → Gulp + Less/SCSS 处理
- 常见工具
- Babel:语法转换(JSX、ES6+)
- TSC:TypeScript 编译
- Webpack:UMD 打包
- Gulp:任务管理
你可以这样理解:
- TSC 和 Babel 是负责 编译
- Webpack 是负责 打包
- Gulp 是负责 组织任务
看完这篇博客后,你可以尝试自己创建一个简单的 React 组件库,然后使用 Babel、TSC、Webpack 进行编译打包,这样就能更直观地理解这些概念!🚀
组件库是怎么构建的
来源:React 通关秘籍 - zxg_神说要有光 - 掘金小册
结构
新建一个项目:
1 | mkdir component-lib-test |
分别安装 ant-design、arco-design、semi-design
1 | pnpm install antd |
npm、yarn 会把所有依赖铺平,看着比较乱。而 pnpm 不会,node_modules 下很清晰:
antd
首先看下 antd,分为了 lib、es、dist 3 个目录:
分别看下这三个目录的组件代码:
lib 下的组件是 commonjs 的:
es 下的组件是 es module 的:
dist 下的组件是 umd 的:
然后在 package.json 里分别声明了 commonjs、esm、umd 还有类型的入口:
这样,当你用 require 引入的就是 lib 下的组件,用 import 引入的就是 es 下的组件。
而直接 script 标签引入的就是 unpkg 下的组件。
arco-design
也是一样:
同样是 lib、es、dist 3 个目录,同样是分别声明了 esm、commonjs、umd 的入口。
也就是说,组件库都是这样的,分别打包出 3 份代码(esm、commonjs、umd),然后在 package.json 里声明不同模块规范的入口。
那问题来了,如果我有一个 esm 的模块,怎么分别构建出 esm、commonjs、umd 的 3 份代码呢?
这个问题很容易回答。
umd 的代码用 webpack 打包就行。
esm 和 commonjs 的不用打包,只需要用 tsc 或者 babel 编译下就好了。
我们分别看下这三个组件库都是怎么做的:
构建逻辑
arco-design
它的构建逻辑在 arco-cli 的 arco-scripts 下:
看下这个 index.ts
分别有 build 3 种代码加上 build css 的方法。
我们分别看下:
esm 和 cjs 的编译它封装了一个 compileTS 的方法,然后传入不同的 type。
compileTS 里可以用 tsc 或者 babel 编译:
tsc 编译就是读取项目下的 tsconfig.json,然后 compile:
babel 编译是基于内置配置,修改了下产物 modules 规范,然后编译:
babelConfig 里配置了 typescript 和 jsx 的编译:
再就是umd
和我们分析的一样,确实是用 webpack 来打包:
webpack 配置里可以看到,确实是为了 unpkg 准备的,用了 ts-loader 和 babel-loader:
而 css 部分则是用了 less 编译:
gulp 是用来组织编译任务的,可以让任务串行、并行的执行。
这里的 gulp.series 就是串行执行任务,而 gulp.parallel 则是并行。
所以说,那 3 种代码加上 css 文件是怎么打包的就很清晰了:
其中用到 gulp 只是用来组织编译任务的,可用可不用。
semi-design
它就没有单独分一个 xx-scripts 包了,直接在 semi-ui 的 scripts 目录下。
它也是用到了 gulp 来组织任务。
看下这个 compileLib 的 gulp task:
这里的 compileTSXForESM 和 ForCJS 很明显就是编译组件到 esm 和 cjs 两种代码的。
先用了 tsc 编译再用了 babel 编译:
然后是 umd,也是用了 webpack:
用了 babel-loader 和 ts-loader:
最后是 scss 的编译:
semi-design 把所有组件的 scss 都放在了 semi-foundation 这个目录下来维护:
所以编译的时候就是这样的:
就是把 semi-foundation 这个目录下的所有 scss 编译后合并成了一个文件
而 arco-design 的样式是在组件目录下维护的:
这个倒是没啥大的区别,只是编译的时候改下源码目录就好了。
这就是 semi-design 的 esm、cjs、umd、scss 是如何编译打包的。
和 arco-design 的 scripts 区别大么?
不大,只不过没有单独做一个 xxx-scripts 的包,编译出 esm 和 cjs 代码用的是 tsc + babel,而且用的是 scss 不是 less 而已。
ant-design
它也是单独分了一个包来维护编译打包的 scripts,叫做 @ant-design/tools。
它也有个 gulpfile 定义了很多 task
比如 compile 的 task 是编译出 es 和 cjs 代码的:
是不是很熟悉的感觉?
大家都是这么搞的。
它也是先用了 tsc 再用 babel 编译,最后输出到 es 或者 lib 目录:
打包 umd 代码的时候也是用了 webpack:
只不过它这个 webpack 配置文件是读取的组件库项目目录下的,而不像 arco-design 那样是内置的。
这就是这三个组件库的编译打包的逻辑。
总结
我们分析了 ant-design、semi-design、arco-design 组件库的产物和编译打包逻辑。
它们都有 lib、es、dist 目录,分别放着 commonjs、es module、umd 规范的组件代码。
并且在 package.json 里用 main、module、unpkg 来声明了 3 种规范的入口。
从产物上来看,三个组件库都是差不多的。
然后我们分析了下编译打包的逻辑。
ant-design 和 acro-design 都是单独抽了一个放 scripts 的包,而 semi-design 没有。
它们编译 esm 和 cjs 代码都用了 babel 和 tsc 来编译,只不过 arco-design 是用 tsc 或者 babel 二选一,而 ant-design 和 semi-design 是先用 tsc 编译再用 babel 编译。
打包出 umd 的代码,三个组件库都是用的 webpack,只不过有的是把 webpack 配置内置了,有的是放在组件库项目目录下。
而样式部分,ant-design 是用 css-in-js 的运行时方案了,不需要编译,而 arco-design 用的 less,样式放组件目录下维护,semi-design 用的 scss,单独一个目录来放所有组件样式。
并且编译任务都是用的 gulp 来组织的,它可以串行、并行的执行一些任务。
虽然有一些细小的差别,但从整体上来看,这三大组件库的编译打包逻辑可以说是一模一样的。
写这样的 scripts 麻烦么?
并不麻烦,umd 部分的 webpack 打包大家都会,而 esm 和 cjs 用 babel 或者 tsc 编译也不难,至于 scss、less 部分,那个就更简单了。
所以编译打包并不是组件库的难点。
如果你要写一个组件库,也可以这样来写 scripts。
构建esm和cjs并发布流程
来源:React 通关秘籍 - zxg_神说要有光 - 掘金小册
我们已经写了很多组件了,比如 Calendar、Watermark、OnBoarding 等,但都是用 cra 或者 vite 单独创建项目来写的。
这节我们把它们整合一下,加上构建脚本,发布到 npm,做成和 Ant Design 一样的组件库。
组件库的构建我们上节分析过,就是构建出 esm、commonjs、umd 3 种格式的代码,再加上 css 的构建就好了。
ant design、arco design、semi design 都是这样。
我们再看几个组件库:
1 | mkdir tmp |
安装 react-bootstrap:
1 | pnpm install react-bootstrap |
(用 pnpm 安装,node_modules 下目录比较简洁)
看下 node_modules/react-bootstrap 的 package.json
可以看到,它也有 main 和 module,也就是 commonjs 和 es module 两种入口。
当你 import 的时候,引入的是 esm 的代码。
当你 require 的时候,引入的是 commonjs 的代码。
看一下 esm 和 cjs 下的代码:
当然,它也是支持 umd 的:
看下 build 脚本:
就是分别用 babel 编译出 commonjs 和 esm 的代码就可以了:
用 tsc 也行。
umd 格式代码也同样是 webpack 打包的:
不同于 antd、arco design 和 semi design,它就没有用 gulp 来组织流程。
gulp 本来就不是必须的,可用可不用。
甚至连单独的脚本都不需要,直接 tsc 编译就行:
比如这个 blueprint 组件库:
之前总结的组件库的构建流程是没问题的:
开始
然后我们新建一个项目来试一下:
1 | npx create-vite guang-components |
进入项目,复制几个之前的组件过来:
复制 Calendar、Watermark、Message 这三个组件:
然后安装下依赖:
1 | npm install |
然后去掉 Calendar 和 Message 组件里样式的引入,css 和 js 是分开编译的:
把这些没用的文件删掉:
加一个 index.ts 来导出组件:
1 | import Calendar, { CalendarProps } from './Calendar'; |
添加编译
接下来加上 tsc 和 sass 的编译:
添加一个 tsconfig.build.json 的配置文件:
1 | { |
然后编译下:
1 | npx tsc -p tsconfig.build.json --module ESNext --outDir dist/esm |
看下 dist 的产物:
没啥问题,esm 和 commonjs 格式的代码都生成了。
然后再编译下样式:
1 | npx sass ./src/Calendar/index.scss ./dist/esm/Calendar/index.css |
看下产物:
没问题。
当然,sass 文件多了以后你可以写个 node 脚本来自动查找所有 sass 文件然后编译。
接下来只要把这个 dist 目录发到 npm 仓库就可以了。
1 | "main": "dist/cjs/index.js", |
main 和 module 分别是 commonjs 和 es module 的入口。
types 是 dts 的路径。
files 是哪些文件发布到 npm 仓库,没列出来的会被过滤掉。
并且,还需要把 private: true 和 type: module 的字段给去掉。
发布npm包
然后我们来发布 npm 包:
1 | npm adduser |
执行 npm adduser 命令,会打印一个链接让你登录或者注册:
登录后就可以 npm publish 了:
1 | npm publish |
发布完之后,在 https://www.npmjs.com 就可以搜索到:
我们新建个项目来用用看:
1 | npx create-vite guang-test |
使用npm包
安装依赖:
1 | pnpm install |
在 App.tsx 里用一下:
1 | import { Watermark } from 'guang-components'; |
跑下开发服务:
1 | npm run dev |
浏览器访问下:
再试下另外两个组件:
1 | import dayjs from 'dayjs'; |
这里用到了 dayjs:
1 | pnpm install dayjs |
这里样式受 index.css 影响了,去掉就好了:
再来试下 Message 组件:
1 | import { ConfigProvider, useMessage } from "guang-components" |
没啥问题。
优化依赖
然后我们优化下依赖:
其实用到 guang-components 的项目都会安装 react 和 react-dom 包,不需要把它放在 dependencies 里。
而是放在 peerDependencies 里:
它和 dependencies 一样,都是依赖,但是 dependencies 是子级,而 peerDependencies 是平级。
如果和其他 react 包的版本冲突时,dependencies 会保留一份副本,而 peerDependencies 会提示开发者去解决冲突,不会保留副本。
改下版本号,重新 npm publish:
这样,我们的组件库的 npm 包就发布成功了!
再测试下 commonjs 的代码。
用 cra 创建个项目:
1 | npx create-react-app --template=typescript tmp4 |
进入项目,安装组件库:
1 | npm install --save guang-components |
在 App.tsx 用一下:
1 | const { Watermark } = require('guang-components'); |
注意,这次用 require 引入代码。
然后把开发服务跑起来:
1 | npm run start |
浏览器里看一下:
没啥问题。
案例代码上传了小册仓库。
总结
今天我们把之前写过的部分组件封装成了组件库并发布到了 npm 仓库。
可以直接在项目里引入来用,和 antd 等组件库一样。
构建部分我们分析过很多组件库,都是一样的:
- commonjs 和 esm 的代码通过 tsc 或者 babel 编译产生
- umd 代码通过 webpack 打包产生
- css 代码通过 sass 或者 less 等编译产生
- dts 类型也是通过 tsc 编译产生
我们在 package.json 里配置了 main 和 module,分别声明 commonjs 和 es module 的入口,配置了 types 指定类型的入口。
然后通过 npm adduser 登录,之后 npm publish 发布到 npm。
这样,react 项目里就可以引入这个组件库来用了,之前写过的所有组件都可以加到这个组件库里。
构建umd产物、通过unpkg访问
来源:React 通关秘籍 - zxg_神说要有光 - 掘金小册
上节做了 esm 和 commonjs、scss 代码的编译,并发布到 npm,在项目里使用没啥问题。
绝大多数情况下,这样就足够了。
umd 的打包做不做都行。
想一下,你是否用过 antd 的 umd 格式的代码?
是不是没用过?
但如果你做一个开源组件库,那还是要支持的。
这节我们就来做下 umd 的打包:
前面分析过,大多数组件库都用 webpack 来打包的。
我们先用下 antd 的 umd 的代码。
1 | mkdir antd-umd-test |
新建 index.html
1 |
|
antd 依赖的 react、react-dom、dayjs 包也得用 umd 引入。
跑个静态服务:
1 | npx http-server . |
浏览器访问下:
通过全局变量 antd 来访问各种组件。
我们渲染个 Table 组件试一下:
1 |
|
这里不能直接写 jsx,需要用 babel 或者 tsc 之类的编译一下:
浏览器看一下:
渲染成功!
这就是 umd 的方式如何使用组件库。
我们的组件库也支持下 umd:
加一下 webpack.config.js
1 | const path = require('path'); |
就是从 index.ts 入口开始打包,产物格式为 umd,文件名 guang-components.js,全局变量为 Guang。
用 ts-loader 来编译 ts 代码,指定配置文件为 tsconfig.build.json。
注意打包的时候不要把 react 和 react-dom、dayjs 包打进去,而是加在 external 配置里,也就是从全局变量来访问这些依赖。
安装依赖:
1 | npm install --save-dev webpack-cli webpack ts-loader |
这里的 jsdoc 注释是为了引入 ts 类型的,可以让 webpack.config.js 有类型提示:
对 jsdoc 感兴趣的话可以看我这篇文章:JSDoc 真能取代 TypeScript?
打包一下:
1 | npx webpack |
然后看下产物:
看起来没啥问题。
这三个模块也都是通过直接读取全局变量的方式引入,没有打包进去:
在 package.json 改下版本号,添加 unpkg 的入口,然后发布到 npm:
1 | npm publish |
在 unpkg 访问下:
访问 https://unpkg.com/guang-components 会自动重定向到最新版本的 umd 代码。
回到刚才的 antd-umd-test 项目,添加一个 index2.html,引入 guang-components
1 |
|
浏览器访问下:
可以通过全局变量 Guang 来拿到组件。
css 也是通过 unpkg 来拿到:
然后我们渲染下:
1 |
|
jsx 在 ts playground 编译:
浏览器访问下:
可以看到,umd 的组件库代码生效了。
但是控制台有个报错:
点进去可以看到是 _jsx 这个函数的问题:
react 我们通过 externals 的方式,从全局变量引入。
但是这个 react/jsx-runtime 不会。
这个 react/jsx-runtime 是干啥的呢?
同一份 jsx 代码:
你在 typescript playground 里把 jsx 编译选项切换为 react:
可以看到是不同的编译结果。
React 17 之前都是编译为 React.createElement,这需要运行的时候有 React 这个变量,所以之前每个组件都要加上 import React from ‘react’ 才行。
React 17 之后就加了下面的方式,直接编译为用 react/jsx-runtime 的 api 的方式。不再需要都加上 import React from ‘react’ 了。
我们组件库也是用的这种:
但现在打包 umd 代码的时候,这样有问题。
所以我们把 jsx 编译配置改一下就好了。
修改 jsx 为 react 之后,会有一些报错:
在每个报错的组件加一下 React 全局变量:
再次打包就好了:
改下版本号,重新发布一下:
1 | npm publish |
改下 index2.html 里用的组件库的版本号:
现在就没报错了:
这样,我们的组件库就支持了 umd。
案例代码上传了小册仓库。
总结
前面分析过,组件库基本都会提供 esm、commonjs、umd 三种格式的代码。
这节我们实现了 umd 的支持,通过 webpack 做了打包。
打包逻辑很简单:用 ts-loader 来编译 typescript 代码,然后 react、react-dom 等模块用 externals 的方式引入就好了。
再就是 react 通过 externals 的方式,会导致 react/jsx-runtime 引入有问题,所以我们修改了 tsconfig.json 的 jsx 的编译为 react,也就是编译成 React.createElement 的代码。
虽然 umd 的方式用的场景不多,但我们组件库还是要支持的。