打包方案
前面介绍了在不同的模块规范和不同的前端技术体系下,库的适配原理。这部分内容细致又琐碎,使用手动配置的方式会相当麻烦,那么有没有更好的办法呢?
目前比较好的办法就是使用打包工具自动完成打包工作。
根据前面两节的内容,开源库需要支持浏览器、打包工具和Node.js环境,以及不同的模块规范,所以需要提供不同的入口文件。
浏览器(script,AMD,CMD) | 打包工具(webpack、rollup.js) | Node.js | |
---|---|---|---|
入口文件 | index.aio.js | index.esm.js | index.js |
模块规范 | UMD | ES Module | CommonJS |
自身依赖 | 打包 | 打包 | 打包 |
第三方依赖 | 打包 | 不打包 | 不打包 |
# 选择打包工具
既然已经确定了目标,那么接下来就需要选择一款合适的打包工具。JaveScipt社区大多选择webpack
和rollup.js
作为库的打包工具,webpack
是现在非常流行的打包工具,而rollup.js
则被称作下一代打包工具,推荐使用rollup.js作为库的打包工具。
为什么不使用我们更熟悉的webpack呢?我们通过具体示例来对比webpack和rollup.js。假设有两个文件:index.js和bar.js
bar.js对外暴露一个bar函数,代码如下:
export default function bar() {
console.log('bar')
}
2
3
index.js文件引用bar.js文件,代码如下:
import bar from './bar'
bar()
2
webpack
打包输出的内容,index.js
和bar.js
文件的内容在打包内容的最下面。webpack
方案的问题在于会生成很多冗余代码,这对于业务代码来说问题不大,但是对于库来说就不太友好了。
最新发现
但是我试了一下,webpack5打包的内容并没有那么多冗余,以下是webpack5打包内容。(不过webpack34确实有很多冗余)
(()=>{"use strict";console.log("bar")})();
下面的代码是rollup.js
打包输出的内容,可以看到模块完全消失了。那么rollup.js
如何解决模块之间的依赖问题呢?对于打包的代码,rollup.js
巧妙地通过将被依赖的模块放在依赖模块前面的方法来解决模块依赖问题。对比webpack
打包后的代码,rollup.js
的打包方案对于库的开发者来说是接近完美的方案。
'use strict'
function bar() {
console.log('bar')
}
bar()
2
3
4
5
6
7
# 打包步骤
首先安装rollup.js,命令如下:
npm i --save-dev rollup
由于只在开发时才会用到rollup.js
,因此我们通过上面的参数--save-dev
将其安装为开发时依赖,这样会将依赖添加到package.json
文件的DevDependencies
字段中,代码如下:
{
"devDependencies":{
"rollup": "^3.19.1"
}
}
2
3
4
5
rollup.js
的使用方式和webpack
的使用方式类似,需要通过配置文件告诉rollup.js
如何打包。根据上表的结论,存在3种入口文件,因此需要3个配置文件,这里将配置文件统一放到config
目录中。打包输出文件、配置文件、技术体系和模块规范的对应关系如下:
打包输出文件 | 配置文件 | 技术体系 | 模块规范 |
---|---|---|---|
dist/index.js | config/rollup.config.js | Node.js | CommonJS |
dist/index.esm.js | config/rollup.config.esm.js | webpack | ES Module |
dist/index.aio.js | config/rollup.config.sio.js | 浏览器 | UMD |
要打包的代码如下:
export default function clone() {
console.log(clone)
}
2
3
因为代码使用ES Module
编写,所以要在package.json
中加入配置 "type" :"module"
,
接下来,先实现第一个配置文件config/rollup.config.js,
export default {
input: 'src/main.js',
output: {
file: 'dist/index.js',
format: 'cjs'
}
}
2
3
4
5
6
7
input
配置和output
配置表示将src/main.js
文件打包输出为dist/index.js
文件,format
配置表明可以选择的模块方案,其值cjs
的含义是输出模块遵循CommonJS
规范,接下来,运行下面的命令即可实现打包:
npx rollup -c config/rollup.config.js
打包成功后,打开dist/index.js
文件,内容如下:
'use strict';
function clone() {
console.log(clone);
}
module.exports = clone;
2
3
4
5
6
7
接着实现第2个配置文件 config/rollup.config.esm.js
,示例代码如下,其与实现第一个配置文件的代码基本类似,不同点是format
配置的值,此处为es
,表示输出模块遵循ES Module
规范。
export default {
input: 'src/main.js',
output: {
file: 'dist/index.esm.js',
format: 'es'
}
}
2
3
4
5
6
7
打包成功后,打开dis/index.esm.js文件,该文件中的内容如下:
function clone() {
console.log(clone);
}
export { clone as default };
2
3
4
5
最后实现第三个配置文件 config/rollup.config.aio.js,为了将依赖的库也打包进来,需要使用@rollup/plugin-node-resolve插件,首先安装
npm install --save-dev @rollup/plugin-node-resolve
然后修改一下main.js,让其引入一个外部的包
import answer from 'the-answer'
export default function () {
console.log('the answer is' + answer)
}
2
3
4
5
实现config/rollup.config.aio.js文件的完整代码如下,format配置为umd,表示输出模块遵循UMD规范,name配置的值作为全局变量和AMD规范的模块名,plugins配置使用@rollup/plugin-node-resolve插件。
import resolve from '@rollup/plugin-node-resolve';
export default {
input: 'src/main.js',
output: {
file: 'dist/index.aio.js',
format: 'umd',
name:'say'
},
plugins: [ resolve() ]
}
2
3
4
5
6
7
8
9
10
11
打包成功后,打开dist/index.aio.js,该文件中的内容如下:
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.say = factory());
})(this, (function () { 'use strict';
var index = 42;
function main () {
console.log('the answer is' + index);
}
return main;
}));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
每次都执行rollup -c config/rollup.config.js
命令有些繁琐,为了简化构建命令,同时收敛统一构建命令,可以使用npm提供的自定义script功能。在package.json文件中添加下面的代码:
"scripts": {
"build:self":"rollup -c config/rollup.config.js",
"build:esm":"rollup -c config/rollup.config.esm.js",
"build:aio":"rollup -c config/rollup.config.aio.js",
"build":"npm run build:self && npm run build:esm && npm run build:aio"
}
2
3
4
5
6
直接运行下面的命令就可以完成对所有方案的打包:
npm run build
由于现在入口文件位于dist目录下,因此需要修改package.json文件中相应的字段,指向dist目录下的构建文件。改动后的内容如下
{
"main": "dist/index.js",
"jsnext:main":"dist/index.js",
"module":"dist/index.esm.js",
}
2
3
4
5
到目前位置,我们的库已经支持了表中的全部环境。运行完"npm run build"命令构建成功后,现在项目目录如下
# 添加banner
一般开源库文件的顶部都会提供一些关于库的说明,如协议信息等。 下面给我们的库添加统一的说明。现在用户使用的文件是自动构建出来的,无法手动添加。其实rollup.js支持添加统一的banner,由于不同的配置文件需要同样的banner,因此可以将banner信息统一放到rollup.js文件中。
// config/rollup.js
import pkg from "../package.json" assert { type: 'json' }
var version = pkg.version
var banner = `/*!
* ${pkg.name} ${version}
* Licensed under MIT
*/
`
export default banner
2
3
4
5
6
7
8
9
10
11
12
然后修改配置文件,添加banner配置:
// config/rollup.config.js
import common from './rollup.js'
export default {
input: 'src/main.js',
output: {
file: 'dist/index.js',
format: 'cjs',
banner:common
}
}
2
3
4
5
6
7
8
9
10
11
# 按需加载
很多时候,在使用一个库可能只会用到其中的一小部分功能,但是却需要加载整个库的内容,这对于Node.js来说问题不大,但对于浏览器端应用来说是不能接受的,好在rollup.js支持按需加载。
- 我们的库要用到另一个库的功能,但是只用到其中一小部分功能,如果将其全部打包过来,则会让打包体积变大,此时通过rollup.js提供的treeshaking功能可以自动屏蔽未被使用的功能。
例如,假设index.js文件只使用了第三方包is.js中的一个isString函数,当不使用treeshaking功能时,会将is.js中的函数全部引用进来。
而在使用了treeshaking功能后,则可以屏蔽is.js中的其他函数,仅引用isString函数。