使用 rollup 打包 JavaScript SDK

本文最后更新于:2020年12月25日 晚上

吾辈已经写了一个 TypeScript/JavaScript Cli 工具 liuli-cli,如有需要可以使用这个 Cli 直接生成一个开箱即用 SDK 项目,然后就可以直接开始写自己的代码,不需要太过关心下面的内容了 – 因为,它们都已然集成了。

场景

为什么要使用打包工具

如果我们想要写一个 JavaScript SDK,那么就不太可能将所有的代码都写到同一个 js 文件中。当然了,想做的话的确可以做到,但随着 JavaScript SDK 内容的增加,一个 js 文件容易造成开发冲突,以及测试上的困难,这也是现代前端基本上都依赖于打包工具的原因。

为什么打包工具是 rollup

现今最流行的打包工具是 webpack,然而事实上对于单纯的打包 JavaScript SDK 而言 webpack 显得有些太重了。webpack 终究是用来整合多种类型的资源而产生的(ReactJS/VueJS/Babel/TypeScript/Stylus),对于纯 JavaScript 库而言其实并没有必要使用如此 强大 的工具。而 rollup 就显得小巧精致,少许配置就能立刻打包了。

步骤

该记录的代码被吾辈放到了 GitHub,有需要的话可以看下。

前置要求

开始之前,我们必须要对以下内容有所了解

  • JavaScript
  • npm
  • babel
  • uglify
  • eslint

需要打包的代码

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// src/wait.js
/**
* 等待指定的时间/等待指定表达式成立
* @param {Number|Function} param 等待时间/等待条件
* @returns {Promise} Promise 对象
*/
function wait(param) {
return new Promise((resolve) => {
if (typeof param === 'number') {
setTimeout(resolve, param)
} else if (typeof param === 'function') {
var timer = setInterval(() => {
if (param()) {
clearInterval(timer)
resolve()
}
}, 100)
} else {
resolve()
}
})
}

export default wait

// src/fetchTimeout.js
/**
* 为 fetch 请求添加超时选项
* 注:超时选项并非真正意义上的超时即取消请求,请求依旧正常执行完成,但会提前返回 reject 结果
* @param {Promise} fetchPromise fetch 请求的 Promise
* @param {Number} timeout 超时时间
* @returns {Promise} 如果超时就提前返回 reject, 否则正常返回 fetch 结果
*/
function fetchTimeout(fetchPromise, timeout) {
var abortFn = null
//这是一个可以被 reject 的 Promise
var abortPromise = new Promise(function (resolve, reject) {
abortFn = function () {
reject('abort promise')
}
})
// 有一个 Promise 完成就立刻结束
var abortablePromise = Promise.race([fetchPromise, abortPromise])
setTimeout(function () {
abortFn()
}, timeout)
return abortablePromise
}

export default fetchTimeout

// src/main.js
import wait from './wait'
import fetchTimeout from './fetchTimeout'

/**
* 限制并发请求数量的 fetch 封装
*/
class FetchLimiting {
constructor({ timeout = 10000, limit = 10 }) {
this.timeout = timeout
this.limit = limit
this.execCount = 0
// 等待队列
this.waitArr = []
}

/**
* 执行一个请求
* 如果到达最大并发限制时就进行等待
* 注:该方法的请求顺序是无序的,与代码里的顺序无关
* @param {RequestInfo} url 请求 url 信息
* @param {RequestInit} init 请求的其他可选项
* @returns {Promise} 如果超时就提前返回 reject, 否则正常返回 fetch 结果
*/
async _fetch(url, init) {
const _innerFetch = async () => {
console.log(
`执行 execCount: ${this.execCount}, waitArr length: ${
this.waitArr.length
}, index: ${JSON.stringify(this.waitArr[0])}`,
)
this.execCount++
const args = this.waitArr.shift(0)
try {
return await fetchTimeout(fetch(...args), this.timeout)
} finally {
this.execCount--
}
}
this.waitArr.push(arguments)
await wait(() => this.execCount < this.limit)
// 尝试启动等待队列
return _innerFetch()
}
}

export default FetchLimiting

使用 rollup 直接打包

安装 rollup

1
npm i rollup -D

在根目录创建一个 rollup.config.js 配置文件

1
2
3
4
5
6
7
8
9
10
11
12
export default {
// 入口文件
input: 'src/main.js',
output: {
// 打包名称
name: 'bundlea',
// 打包的文件
file: 'dist/bundle.js',
// 打包的格式,umd 支持 commonjs/amd/life 三种方式
format: 'umd',
},
}

添加一个 npm script

1
2
3
"scripts": {
"build": "rollup -c"
}

然后运行 npm run build 测试打包,可以看到 dist 目录下已经有 bundle.js 文件了

好了,到此为止我们已经简单使用 rollup 打包 js 了,下面的内容都是可选项,如果需要可以分节选读。

使用 babel 转换 ES5

然而,我们虽然已经将 main.js 打包了,然而实际上我们的代码没有发生什么变化。即:原本是 ES6 的代码仍然会是 ES6,而如果我们想要尽可能地支持更多的浏览器,目前而言还是需要兼容到 ES5 才行。

所以,我们需要 babel,它能够帮我们把 ES6 的代码编译成 ES5。

附:babel 被称为现代前端的 jquery。

首先,安装 babel 需要的包

1
npm i -D rollup-plugin-babel @babel/core @babel/plugin-external-helpers @babel/preset-env

rollup.config.js 中添加 plugins

1
2
3
4
5
6
7
8
9
10
import babel from 'rollup-plugin-babel'

export default {
plugins: [
// 引入 babel 插件
babel({
exclude: 'node_modules/**',
}),
],
}

添加 babel 的配置文件 .babelrc

1
2
3
4
5
6
7
8
9
10
11
{
"presets": [
[
"@babel/preset-env",
{
"modules": false
}
]
],
"plugins": ["@babel/plugin-external-helpers"]
}

再重新运行 npm run build,可以看到 bundle.js 中的代码已经被编译成 ES5 了。

使用 uglify 压缩生产环境代码

那么,生产中的代码还需要做什么呢?是的,压缩,减小 js 代码的体积是必要的。接下来,我们还需要使用 uglify 压缩我们打包后的 bundle.js 代码。

首先仍然是安装 uglify 相关的包

1
npm i -D rollup-plugin-uglify

然后在 rollup.config.js 中引入插件就好了

1
2
3
4
5
6
7
8
9
// 注意,这里引入需要使用 { uglify } 而非 uglify,因为 uglify 导出自身时使用的是 exports.uglify
import { uglify } from 'rollup-plugin-uglify'

export default {
plugins: [
// js 压缩插件,需要在最后引入
uglify(),
],
}

使用 ESLint 检查代码

如果我们想要需要多人协作统一代码风格,那么可以使用 ESLint 来强制规范。

首先,全局安装 eslint

1
npm i eslint -g

然后使用 eslint cli 初始化

1
eslint --init

下面的三项问题选择

  1. How would you like to configure ESLint? (Use arrow keys)
    Use a popular style guide
  2. Which style guide do you want to follow? (Use arrow keys)
    Standard (https://github.com/standard/standard)
  3. What format do you want your config file to be in? (Use arrow keys)
    JavaScript
  4. Would you like to install them now with npm?
    y

然后,我们发现项目根目录下多出了 .eslintrc.js,这是 eslit 的配置文件。然而,我们需要对其稍微修改一下,不然如果我们的代码中出现了浏览器中的对象,例如 document,eslint 就会傻傻的认为那是个错误!
修改后的 .eslintrc.js 配置

1
2
3
4
5
6
7
module.exports = {
extends: 'standard',
// 添加了运行环境设定,设置 browser 为 true
env: {
browser: true,
},
}

当我们查看打包后的 bundle.js 时发现 eslint 给我们报了一堆错误,所以我们需要排除掉 dist 文件夹
添加 .eslintignore 文件

1
dist

添加 rollup-plugin-eslint 插件,在打包之前进行格式校验

1
npm i -D rollup-plugin-eslint

然后引入它

1
2
3
4
5
6
7
8
import { eslint } from 'rollup-plugin-eslint'

export default {
plugins: [
// 引入 eslint 插件
eslint(),
],
}

这个时候,当你运行 npm run build 的时候,eslint 可能提示你一堆代码格式错误,难道我们还要一个个的去修复么?不,eslint 早已考虑到了这一点,我们可以添加一个 npm 脚本用于全局修复格式错误。

1
2
3
"scripts": {
"lint": "eslint --fix src"
}

然后运行 npm run lint,eslint 会尽可能修复格式错误,如果不能修复,会在控制台打印异常文件的路径,然后我们手动修复就好啦

其他 rollup 配置

添加代码映射文件

其实很简单,只要在 rollup.config.js 启用一个配置就好了

1
2
3
4
5
6
export default {
output: {
// 启用代码映射,便于调试之用
sourcemap: true,
},
}

多环境打包

首先移除掉根目录下的 rollup.config.js 配置文件,然后创建 build 目录并添加下面四个文件

1
2
3
4
5
6
7
8
9
10
11
12
// build/util.js
import path from 'path'

/**
* 根据相对路径计算真是的路径
* 从当前类的文件夹开始计算,这里是 /build
* @param {String} relaPath 相对路径
* @returns {String} 绝对路径
*/
export function calcPath(relaPath) {
return path.resolve(__dirname, relaPath)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// build/rollup.config.dev.js
import { eslint } from 'rollup-plugin-eslint'
import { calcPath } from './util'
import { name } from '../package.json'

export default {
// 入口文件
input: calcPath('../src/main.js'),
output: {
// 打包名称
name,
// 打包的文件
file: calcPath(`../dist/${name}.js`),
// 打包的格式,umd 支持 commonjs/amd/life 三种方式
format: 'umd',
// 启用代码映射,便于调试之用
sourcemap: true,
},
plugins: [
// 引入 eslint 插件,必须在 babel 之前引入,因为 babel 编译之后的代码未必符合 eslint 规范,eslint 仅针对我们 [原本] 的代码
eslint(),
],
}
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
// build/rollup.config.prod.js
import babel from 'rollup-plugin-babel'
// 注意,这里引入需要使用 { uglify } 而非 uglify,因为 uglify 导出自身时使用的是 exports.uglify
import { uglify } from 'rollup-plugin-uglify'
import { eslint } from 'rollup-plugin-eslint'
import { calcPath } from './util'
import dev from './rollup.config.dev'
import { name } from '../package.json'

export default [
dev,
{
// 入口文件
input: calcPath('../src/main.js'),
output: {
// 打包名称
name,
// 打包的文件
file: calcPath(`../dist/${name}.min.js`),
// 打包的格式,umd 支持 commonjs/amd/life 三种方式
format: 'umd',
},
plugins: [
// 引入 eslint 插件,必须在 babel 之前引入,因为 babel 编译之后的代码未必符合 eslint 规范,eslint 仅针对我们 [原本] 的代码
eslint(),
// 引入 babel 插件
babel({
exclude: calcPath('../node_modules/**'),
}),
// js 压缩插件,需要在最后引入
uglify(),
],
},
]
1
2
3
4
5
6
// build/rollup.config.js
import dev from './rollup.config.dev'
import prod from './rollup.config.prod'

// 如果当前环境时 production,则使用 prod 配置,否则使用 dev 配置
export default process.env.NODE_ENV === 'production' ? prod : dev

修改 npm 脚本

1
2
3
4
5
"scripts": {
"build:dev": "rollup -c build/rollup.config.js --environment NODE_ENV:development",
"build:prod": "rollup -c build/rollup.config.js --environment NODE_ENV:production",
"build": "npm run build:dev && npm run build:prod",
}

那么,关于使用 rollup 打包 JavaScript 的内容就先到这里了,有需要的话后续吾辈还会继续更新的!


使用 rollup 打包 JavaScript SDK
https://blog.rxliuli.com/p/53148ce0792e49b6b18bc68ea4eb6b8e/
作者
rxliuli
发布于
2020年2月2日
许可协议