组件打包前置知识

1. 模块规范

在 JavaScript 生态里,代码组织方式有不同的模块化规范,最常见的有以下三种:

  1. CommonJS(CJS)

    • require() 语法

    • 主要用于 Node.js 运行环境

    • 代码示例:

      1
      2
      const lodash = require('lodash');
      console.log(lodash);
    • 缺点:不适用于浏览器,不能 Tree Shaking(优化打包时移除未使用的代码)。

  2. ES Modules(ESM)

    • import/export 语法

    • 现代 JavaScript 规范,适用于浏览器和 Node.js

    • 代码示例:

      1
      2
      import { debounce } from 'lodash';
      console.log(debounce);
    • 优势:支持 Tree Shaking,适合前端打包优化。

  3. 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。这里涉及两个重要工具:

  1. Babel

    • 主要用于 ES6+ 语法转换,让新语法兼容老浏览器。

    • 组件库通常用它来编译 ESM 和 CJS 代码(即 eslib 目录)。

    • 配置示例:

      1
      2
      3
      {
      "presets": ["@babel/preset-env", "@babel/preset-react"]
      }
    • 作用:

      • 把 JSX 转换成 React.createElement() 语法
      • 把 ES6+ 代码转换成更低版本的 JavaScript
  2. TSC(TypeScript Compiler)

    • TypeScript 自带的编译器,主要用于类型检查代码转换
    • 可以直接编译 TypeScript 到 JavaScript,不负责转换新语法(比如 JSX)。
    • 组件库通常用它来编译 TypeScript 并生成 .d.ts 类型声明文件。

3. 打包工具

打包工具的目的是将代码转换成不同的模块格式(CJS、ESM、UMD),博客中提到了以下两种工具:

  1. Webpack

    • 主要用于打包 UMD 代码,通常用于浏览器端的组件库。

    • 例如:

      • 你想发布一个 UI 组件库,并让别人直接用 <script> 引入,这就需要 Webpack 生成 UMD 版本。
    • 典型配置:

      1
      2
      3
      4
      5
      6
      module.exports = {
      output: {
      library: "MyLibrary",
      libraryTarget: "umd"
      }
      };
  2. Gulp

    • 任务自动化工具,常用于编排多个编译任务

    • 例如:先用 TSC 编译,再用 Babel 处理。

    • 典型代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      const 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️⃣ 目录结构

博客分析了 antdsemi-uiarco-designnode_modules 目录:

1
2
3
lib/   --> CJS 格式
es/ --> ESM 格式
dist/ --> UMD 格式

这意味着:

  • lib(CommonJS) 用于 Node.js
  • es(ES Modules) 用于前端打包
  • dist(UMD) 用于 <script> 引入

2️⃣ 构建过程

博客中的 Arco Design 例子说明了组件库如何打包:

  1. ESM 和 CJS 代码

    • 先用 TSC 或 Babel 编译 tsxts

    • TSC 负责 类型检查

    • Babel 负责 转换新语法(如 JSX)

    • 产物存放在 lib/(CJS) 和 es/(ESM)

    • 关键代码:

      1
      2
      compileTS("es"); // 生成 ESM
      compileTS("cjs"); // 生成 CJS
  2. UMD 代码

    • 用 Webpack 打包成 dist/

    • 主要用于 unpkg(CDN)引入

    • 关键代码:

      1
      webpack({ output: { libraryTarget: "umd" } });
  3. CSS 处理

    • Arco 用 Less
    • Semi Design 用 SCSS
    • 通过 gulp 编译

5. 总结

  1. 为什么组件库要支持不同模块格式?
    • ESM(ES6 模块)适用于现代前端打包工具(如 Webpack、Vite)
    • CJS(CommonJS)适用于 Node.js 环境
    • UMD(通用模块)适用于 <script> 引入(CDN)
  2. 组件库的编译过程
    • ESM 和 CJS → TSC 或 Babel 编译
    • UMD → Webpack 打包
    • CSS → Gulp + Less/SCSS 处理
  3. 常见工具
    • Babel:语法转换(JSX、ES6+)
    • TSC:TypeScript 编译
    • Webpack:UMD 打包
    • Gulp:任务管理

你可以这样理解:

  • TSCBabel 是负责 编译
  • Webpack 是负责 打包
  • Gulp 是负责 组织任务

看完这篇博客后,你可以尝试自己创建一个简单的 React 组件库,然后使用 Babel、TSC、Webpack 进行编译打包,这样就能更直观地理解这些概念!🚀

组件库是怎么构建的

来源:React 通关秘籍 - zxg_神说要有光 - 掘金小册

结构

新建一个项目:

1
2
3
4
5
mkdir component-lib-test

cd component-lib-test

npm init -y

分别安装 ant-design、arco-design、semi-design

1
2
3
4
5
pnpm install antd

pnpm install @douyinfe/semi-ui

pnpm install @arco-design/web-react

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
2
3
mkdir tmp
cd tmp
npm init -y

安装 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
2
3
4
5
npm install

npm install --save react-transition-group lodash-es dayjs classnames

npm i --save-dev @types/react-transition-group

然后去掉 Calendar 和 Message 组件里样式的引入,css 和 js 是分开编译的:

把这些没用的文件删掉:

加一个 index.ts 来导出组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Calendar, { CalendarProps } from './Calendar';
import Watermark, { WatermarkProps } from './Watermark';
import { MessageProps, Position, MessageRef} from './Message';
import { useMessage } from './Message/useMessage';
import { ConfigProvider } from './Message/ConfigProvider';

export {
Calendar,
Watermark,
ConfigProvider,
useMessage
}

export type {
CalendarProps,
WatermarkProps,
MessageProps,
Position,
MessageRef
}

添加编译

接下来加上 tsc 和 sass 的编译:

添加一个 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
{
"compilerOptions": {
"declaration": true,
"allowSyntheticDefaultImports": true,
"target": "es2015",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"allowImportingTsExtensions":null,
"strict": true,
},
"include": [
"src"
],
"exclude": [
"src/**/*.test.tsx",
"src/**/*.stories.tsx"
]
}

然后编译下:

1
2
3
npx tsc -p tsconfig.build.json --module ESNext --outDir dist/esm

npx tsc -p tsconfig.build.json --module commonjs --outDir dist/cjs

看下 dist 的产物:

没啥问题,esm 和 commonjs 格式的代码都生成了。

然后再编译下样式:

1
2
3
4
5
6
7
npx sass ./src/Calendar/index.scss ./dist/esm/Calendar/index.css

npx sass ./src/Calendar/index.scss ./dist/cjs/Calendar/index.css

npx sass ./src/Message/index.scss ./dist/esm/Message/index.css

npx sass ./src/Message/index.scss ./dist/cjs/Message/index.css

看下产物:

没问题。

当然,sass 文件多了以后你可以写个 node 脚本来自动查找所有 sass 文件然后编译。

接下来只要把这个 dist 目录发到 npm 仓库就可以了。

1
2
3
4
5
6
7
8
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"types": "dist/esm/index.d.ts",
"files": [
"dist",
"package.json",
"README.md"
],

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
2
3
pnpm install

pnpm install guang-components

在 App.tsx 里用一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Watermark } from 'guang-components';

const App = () => {
return <Watermark
content={['测试水印', '神说要有光']}
>
<div style={{height: 800}}>
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
</div>
</Watermark>
};

export default App;

跑下开发服务:

1
npm run dev

浏览器访问下:

再试下另外两个组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
import dayjs from 'dayjs';
import {Calendar} from 'guang-components';
import 'guang-components/dist/esm/Calendar/index.css';

function App() {
return (
<div>
<Calendar value={dayjs('2024-07-01')}></Calendar>
</div>
);
}

export default App;

这里用到了 dayjs:

1
pnpm install dayjs

这里样式受 index.css 影响了,去掉就好了:

再来试下 Message 组件:

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
import { ConfigProvider, useMessage } from "guang-components"
import 'guang-components/dist/esm/Message/index.css';

function Aaa() {
const message = useMessage();

return <button onClick={() =>{
message.add({
content:'请求成功'
})
}}>成功</button>
}

function App() {

return (
<ConfigProvider>
<div>
<Aaa></Aaa>
</div>
</ConfigProvider>
);
}

export default App;

没啥问题。

优化依赖

然后我们优化下依赖:

其实用到 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const { Watermark } = require('guang-components');

const App = () => {
return <Watermark
content={['测试水印', '神说要有光']}
>
<div style={{height: 800}}>
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quos quod deserunt quidem quas in rem ipsam ut nesciunt asperiores dignissimos recusandae minus, eaque, harum exercitationem esse sapiente? Eveniet, id provident!</p>
</div>
</Watermark>
};

export default App;

注意,这次用 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
2
3
mkdir antd-umd-test
cd antd-umd-test
npm init -y

新建 index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/dayjs@1.11.11/dayjs.min.js"></script>
<script src="https://unpkg.com/antd@5.19.0/dist/antd.min.js"></script>
</head>
<body>

</body>
</html>

antd 依赖的 react、react-dom、dayjs 包也得用 umd 引入。

跑个静态服务:

1
npx http-server .

浏览器访问下:

通过全局变量 antd 来访问各种组件。

我们渲染个 Table 组件试一下:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/dayjs@1.11.11/dayjs.min.js"></script>
<script src="https://unpkg.com/antd@5.19.0/dist/antd.min.js"></script>
</head>
<body>
<div id="root"></div>
<script>
const dataSource = [
{
key: '1',
name: '胡彦斌',
age: 32,
address: '西湖区湖底公园1号',
},
{
key: '2',
name: '胡彦祖',
age: 42,
address: '西湖区湖底公园1号',
},
];

const columns = [
{
title: '姓名',
dataIndex: 'name',
key: 'name',
},
{
title: '年龄',
dataIndex: 'age',
key: 'age',
},
{
title: '住址',
dataIndex: 'address',
key: 'address',
},
];

const container = document.getElementById('root');
const root = ReactDOM.createRoot(container);

root.render(React.createElement(antd.Table, { dataSource: dataSource, columns: columns }));
</script>
</body>
</html>

这里不能直接写 jsx,需要用 babel 或者 tsc 之类的编译一下

浏览器看一下:

渲染成功!

这就是 umd 的方式如何使用组件库。

我们的组件库也支持下 umd:

加一下 webpack.config.js

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
30
31
32
33
34
35
36
37
const path = require('path');

/**
* @type {import('webpack').Configuration}
*/
module.exports = {
mode: 'development',
devtool: 'source-map',
entry: {
index: ['./src/index.ts']
},
output: {
filename: 'guang-components.js',
path: path.join(__dirname, 'dist/umd'),
library: 'Guang',
libraryTarget: 'umd'
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'ts-loader',
options: {
configFile: 'tsconfig.build.json'
}
}
]
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json']
},
externals: {
react: 'React',
'react-dom': 'ReactDOM',
dayjs: 'dayjs'
}
};

就是从 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 有类型提示:

js.png

对 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/dayjs@1.11.11/dayjs.min.js"></script>
<script src="https://unpkg.com/guang-components@0.0.7/dist/umd/guang-components.js"></script>
</head>
<body>
<div id="root"></div>
<script>

</script>
</body>
</html>

浏览器访问下:

可以通过全局变量 Guang 来拿到组件。

css 也是通过 unpkg 来拿到

然后我们渲染下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/dayjs@1.11.11/dayjs.min.js"></script>
<script src="https://unpkg.com/guang-components@0.0.7/dist/umd/guang-components.js"></script>

https://unpkg.com/guang-components@0.0.8/dist/esm/Calendar/index.css
</head>
<body>
<div id="root"></div>
<script>
const container = document.getElementById('root');
const root = ReactDOM.createRoot(container);

root.render(React.createElement(Guang.Calendar, { value: dayjs('2024-07-01') }));
</script>
</body>
</html>

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 的方式用的场景不多,但我们组件库还是要支持的。