1. 前言


image.png

落魄前端打字员小李下班后,觉得有点饿,于是买了一份炒粉,分量实在太足,吃不完决定打包:

小李:

老板,麻烦打包

老板:

好的,你要哪种包装盒,有 UMD, CMD, AMD, ES modules 等等,由本店金牌打包员 webpack, babel, rollup 为你服务

小李:

好吧,有点尬,今天主要想说下前端普遍的打包方式

2. yarn add xx 之后都下载了啥

以 组件库 antd  为例子

1
yarn add antd

下载下来目录结构长这样子
image.png

抛去俄罗斯套娃的 node_modules  外,可以看到有三个目录,他们分别对应主流的三个模块规范 (AMD,CMD 远古模块规范现在基本没人使用了)

目录 模块规范 含义
dist UMD 兼容 AMD,CMD 的模块,可以直接在浏览器中使用
lib CommonJS 一个文件即一个模块,Node.js 采用的就是这个规范
es ES modules ES6 模块规范

3. 模块的使用方式

UMD


此时,antd  变成了浏览器的全局变量,可以直接使用,好处是无需构建,直接在浏览器使用,缺点是不好维护,无法按需加载

1
2
3
4
<script src="https://unpkg.com/antd"/>
<script type="module">
const { Button } = antd
</script>


ES modules

推荐的方式,由于 import 的 module 都是静态的 , rollup 和 webpack 可以很好的去除无用代码,也就是传说中的 tree shaking  , 确定就是第三方包质量参吃不齐,很多没有提供 es 的代码包

1
import { Button } from 'antd';


CommonJS

缺点是 可以 require 一个 动态的 module, 所以打包工具不能 tree shaking

1
const { Button } = require('antd');

4. 模块的构建


上面说了不同模块规范的使用方式,那么怎样发布 npm  的 时候 打包不同规范的模块呢?前端什么都缺,就是不缺轮子,这里主要讲解 babel , webpack  和 tsc  这三种方式

使用 webpack 打包 UMD

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
55
56
57
const fs = require('fs');
const path = require('path');
const webpack = require('webpack');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');

const { version, name, description } = require('./package.json');

module.exports = {
mode: 'production',
entry: {
[name]: path.resolve(__dirname, './src/index.tsx'),
},

output: {
library: name,
libraryTarget: 'umd', // 输出 UMD 格式的包
umdNamedDefine: true, // 是否将模块名称作为 AMD 输出的命名空间
path: path.resolve(__dirname, 'dist'),
filename: '[name].min.js',
},
// 忽略 react 和 react-dom
externals: {
react: {
root: 'React',
commonjs2: 'react',
commonjs: 'react',
amd: 'react',
},
'react-dom': {
root: 'ReactDOM',
commonjs2: 'react-dom',
commonjs: 'react-dom',
amd: 'react-dom',
},
},
resolve: {
enforceExtension: false,
extensions: ['.js', '.ts', '.tsx'],
},
module: {
rules: [
{
test: /\.ts[x]?$/,
use: [
{
loader: 'awesome-typescript-loader',
},
],
},
],
},
plugins: [
new webpack.BannerPlugin(` \n ${name} v${version} \n ${description}
\n ${fs.readFileSync(path.resolve(__dirname, './LICENSE'))}
`),
],
};


这里的重点 主要是 output.libraryTarget  和 externals , 其他的都是比较常规的配置,如果代码是 js  编写的,替换对应的 loader 即可

image.png

使用 babel 打包

编写 .babelrc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const env = process.env.BABEL_ENV || process.env.NODE_ENV;
const outputModule = process.env.OUTPUT_MODULE;

module.exports = {
presets: [
[
'@babel/preset-env',
{
modules: outputModule || false,
},
],
'@babel/preset-react',
],
plugins: [
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-proposal-class-properties',
],
};

这里的 modules  可选项为  “amd” | “umd” | “systemjs” | “commonjs” | “cjs” | “auto” | false

如果代码是 js  编写的,在命令行执行如下命令

1
2
3
4
5
6
7
8
// umd `-d` 是输出到指定目录
cross-env OUTPUT_MODULE=umd babel src -d dist

// commonjs
cross-env OUTPUT_MODULE=commonjs babel src -d lib

// es modules
babel src -d es

直接 改变环境变量 OUTPUT_MODULE 切换不同的输出格式即可,可以看到,在输入 ES modules  的时候,没有指定环境变量,相当于是 modules: false , 因为我们开发的时候,就是采用的 这种规范,所以不需要做额外的转换,原样输出到 es  目录即可

如果代码是 ts  编写的,问题也不大,加上 ts  的 presets  即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const env = process.env.BABEL_ENV || process.env.NODE_ENV;
const outputModule = process.env.OUTPUT_MODULE;

module.exports = {
"presets": [
[
"@babel/preset-env",
{
"modules": outputModule || false
}
],
"@babel/preset-react"
+ "@babel/preset-typescript"
],
"plugins": [
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-proposal-class-properties",
]
}

使用 tsc 打包


tsc 是 TypeScript  提供的 打包工具

首先编写 tsconfig.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"compilerOptions": {
"target": "es5",
"moduleResolution": "node",
"module": "esnext",
"jsx": "react",
"pretty": true,
"sourceMap": false,
"strict": true,
"skipLibCheck": true,
"strictNullChecks": true,
"esModuleInterop": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noImplicitAny": true,
"removeComments": true,
"noFallthroughCasesInSwitch": true,
"lib": ["es2018", "dom"]
},
"include": ["src"]
}


指定不同的模块规范,和 babel 类似,不要忘记 --declaration ,  也就是输出 types  定义,这样别人使用你的包的时候,才有友好的提示

1
2
3
4
5
6
7
8
// umd
tsc -m umd --outDir dist --declaration

// commonjs
tsc -m commonjs --outDir lib --declaration

// es modules
tsc -m esNext --outDir es --declaration

将打包脚本集成到 package.json


为了方便的进行打包,我们可以将写好的打包脚本集成到 npm scripts  里面,最后再配置 npm  的钩子 prepublishOnly  在每次发布前自动打包,这样不会担心代码更新没有重新构建,美滋滋!

1
2
3
4
5
6
7
8
9
10
11
12
// package.json

{
...
"scripts" : {
"build:cjs": "tsc -m commonjs --outDir lib --declaration",
"build:es": "tsc -m esNext --outDir es --declaration",
"build:umd": "tsc -m umd --outDir dist --declaration",
"build": "yarn build:cjs && yarn build:es && yarn build:umd",
"prepublishOnly": "yarn build",
}
}
1
npm publish

5. 指定不同模块的路径


代码也打包了,也发布了,是不是就完事呢?当然不是,这会有个问题,回想下前面说到的 antd

1
import { Button } from 'antd';

image.png
antd  提供了三种模块代码包,我们在使用的时候却并没有指明具体的路径,那我们到底使用的是 dist , es  还是 lib  呢?

查看 antd  代码包下面的 package.json  可以看到这几个字段:

image.png

  • main 表示 Node.js  默认会加载的路径
  • module 表示 ES modules  的路径
  • unpkg 表示 umd  的路径 ( unpkg  是一个免费的 cdn)


所以这两句代码是等价的:

1
2
3
import { Button } from 'antd';

import { Button } from './node_modules/antd/lib/index.js';

所以没有指定具体的路径的时候,默认是加载 lib  目录,也就是 cjs  模块  , 但是这样会引入全量的 antd  组件,结果这个问题也很简单,加载 es  目录 或者使用  babel-plugin-import  即可

1
import { Button } from 'antd/es';

6. 让 webpack 优先加载 ES modules


如果依赖的库提供了 es  目录,我们可以手动指定,但会有些麻烦,我们需要手动查看每个库有没有提供 es  目录,幸运的是 webpack 可以帮我们处理,优先去寻找依赖的 es  目录,如果没找到,再降级为 lib  目录

1
2
3
4
5
6
7
8
// webpack.config.js

module.exports = {
...
resolve: {
mainFields: ['jsnext:main', 'main'],
},
}

7. 结语

配置工程师还真滴不好当,现如今,还出现了  snowpack, vite  之类的打包工具,也非常值得学习,以上是我做一些开源项目学习到的一些打包方面的知识点,希望能对你有帮助。

参考链接

ant-design

webpack-guidebook

react-7h-hooks