Rollup + Babel 编写和分发 JavaScript 库
目录
搭建工程项目和搭建库项目之间可能有些小区别,以致工程项目用的打包工具 webpack,不太适用于库项目上。
工程项目输出的代码多是在浏览器运行的,调用浏览器专有的接口(document、window)。库项目可能在浏览器环境被引用,也可能在 Node 环境引用,所以库项目的代码要适配多个环境。
这个是实验将编写一个温标转换的库,然后通过 rollup 和 babel 工具编译成不同格式的代码,适应不同的平台。
首先用 yarn 初始化项目:
yarn init
配置 Babel
Babel 是一个代码转换的工具,它能把具有新特性的代码转换成旧平台能识别的等效代码,如箭头函数。
const fn = () => 1
// Babel 会编译成 var fn = function fn() { return 1; };
TypeScript(TS) 是带有类型声明和类型检查的 JS,使用 TypeScript 编写项目可以提供代码提示和重构的便利,减少出错。TS 本身也要翻译成 JS 才能运行,所以可以配置 TS 编译成 JS 代码后,再交给 Babel 做兼容性处理。
安装 Babel 和 TS:
- @babel/core Babel 核心包
- @babel/cli Babel 命令行工具
- @babel/preset-env Babel 兼容性整合包
- @babel/preset-typescript Babel TypeScript 整合包
- typescript TypeScript 编译器工具
yarn add -D @babel/core @babel/cli @babel/preset-env @babel/preset-typescript typescript
# 安装完毕后,初始化 TS 配置
yarn tsc --init
根目录新建 Babel 配置文件 babel.config.json` 编辑
给 Babel 配置整合包。
{
"presets": [
"@babel/preset-env",
"@babel/preset-typescript"
]
}
*试一下: 新建 TS 文件 index.ts` 编写一个箭头函数
const fn: Function = () => 1
执行 Babel 编译即可看到 JS 输出结果:
Babel 默认只编译 JS 代码,所以要增加参数
--extensions
让 Babel 识别 TS。
yarn babel --extensions '.ts' ./index.ts
编写库实现
现在编写温标转换库,实现非常简单,即摄氏度后华氏度的互相转换。
文件 src/utils.ts
/** 保留N位小数,不执行四舍五入,保留0位小数=取整 */
export function formatDecimal(dec: number, fixes: number) {
const scale = Math.pow(10, fixes);
const atoi = (val: any) => parseInt(`${val}`, 10) || 0;
return atoi(dec * scale) / scale;
}
文件 src/unit-transform.ts
这里省略具体实现 查看具体实现(Github)
import { formatDecimal } from './utils';
type TransformOptions = {
scale?: number;
unit: 'c' | 'f';
};
export function createUnitTransform(opts: TransformOptions) {...}
文件 src/index.ts
到这里,定义了 formatDecimal() 和 createUnitTransform()
在入口(index)处把这些方法都导出来,方便开发者直接引用入口,而不是引用具体的文件。
export * from './utils'
export * from './unit-transform'
export { createUnitTransform as default } from './unit-transform'
对于开发者来说,直接从入口引用需要的方法即可。
// import { formatDecimal, createUnitTransform } from 'unit-transform'
// import createUnitTransform from 'unit-transform' 这里等同于 default
现在简单的温标转换库已经完成了。
编写测试
代码库的稳定性非常重要,在库项目完成之际,应该通过测试,尽可能找出更多的bug并修复,保证项目的稳定性。
JEST 是一个 JavaScript 代码测试库。
- jest JEST 核心库
- ts-jest JEST TypeScript 配置库
- @types/jest JEST TypeScript 声明库,提供代码提示
JEST 需要通过配置支持 TypeScript,所以建议用
ts-jest
直接一步创建
yarn add -D jest ts-jest @types/jest
# 安装完毕后,创建配置
yarn ts-jest config:init
文件 _test_/basic.test.ts
编写简单的摄氏度和华氏度互相转换的测试用例。
import createUnitTransform from '../src'
test('c2f', () => {
const units = createUnitTransform({ scale: 0, unit: 'c' })
expect(units.c2f(10)).toBe(50)
expect(units.cf2f(10)).toBe(50)
})
test('f2c', () => {
const units = createUnitTransform({ scale: 0, unit: 'f' })
// 106.66666 -> scale(0) -> 106
expect(units.f2c(224)).toBe(106)
expect(units.cf2c(224)).toBe(106)
})
执行测试
指令 jest
即可执行测试,JEST 会自动扫描项目中名字匹配 .test.*
的代码文件并执行它们。
yarn jest
分发目标代码
在本文的开头处已经说明了,库代码会在各种环境下被引用。TS 文件肯定无法直接执行,JS 可能包含一些不兼容的代码,所以必须针对不同的目标环境,生成特定平台的代码。
常用输出格式
CommonJS
CommonJS(CJS)是 Node 自带的模块化 JS 格式,通用性较好,适用小型 Node 项目和脚本编码。
// 导出
module.exports = { createUnitTransform, formatDecimal }
// 导入
const module = require('./index')
// { createUnitTransform: Function, formatDecimal: Function }
ESM
ES Module(ESM)是 ESMAScript 制定的模块化 JS 格式,包含较多新特性,是目前流行的前端项目编码格式。
ESM 模块化要求突出,通过 import、export 关键字,一些模块化打包工具(webpack)可以轻松实现 Tree Shaking(去掉未引用的代码,缩小产物体积)。
// 导出
export { createUnitTransform, formatDecimal }
export default 1
// 导入
import { formatDecimal } from './index' // Function
import numberValue from './index' // 1
// 对于 webpack 项目来说, createUnitTransform 没有被引用,所以编译后不会包含 createUnitTransform 的代码
UMD
Universal Module Definition(UMD)常用于浏览器,目标代码通常全部写到一个 JS 文件里,执行这个 JS 文件会往全局变量增加一个模块入口。
UMD 打包后的代码体积非常大,通常只在旧的项目、非常轻量的项目上,通过 HTML 引用。
<!DOCTYPE HTML>
<html>
<head>
<!-- 假定这个模块名称叫 unit_transform -->
<script type="text/javascript" src="./index.umd.js"></script>
<script>
window.onload = function () {
console.log(unit_transform) // { createUnitTransform: Function }
}
</script>
</head>
</html>
Babel 编译 CommonJS
Babel 可以编译出 CommonJS、ES Module 代码。但是 babel.config.json
只能配置单个输出,为了能针对特定的配置而生成不同的代码,这里要用到 .babelrc.json 配置文件,它的格式和 babel.config.json 一样。
继续配置 Babel,尝试编译出 CommonJS(CJS)代码。
根目录新建 Babel 配置文件 .babelrc.cjs.json
{
"presets": [
"@babel/preset-env",
"@babel/preset-typescript"
]
}
显式指定配置,目标位置 dist/cjs
,编译 src/
的代码,同时生成 SourceMap
yarn babel --config-file ./.babelrc.cjs.json --extensions '.ts' -d dist/cjs/ src/
# 编译完毕,会在 dist/cjs 看到结果
# dist/cjs/index.js 由于是 CJS 格式,可以通过 node 命令行直接引用
# 例子: node -e 'console.log(require("./dist/cjs"))
Babel 编译 ES Module
其实如果不用 TypeScript,源代码就是一份 ES Module 格式的代码(import、export)。
所以 Babel 在这里再编译一次可以视为仅仅把 TypeScript 转换成 JavaScript。
通过设置 modules
可以关闭 Babel 转换 CJS 的功能,即编译出 ES Module,提供给其他前端项目引用。
数组形式的整合包配置,可以传入参数。
根目录新建 Babel 配置文件 .babelrc.esm.json
{
"presets": [
["@babel/preset-env", {
"modules": false
}],
"@babel/preset-typescript"
]
}
显式指定配置,目标位置 dist/esm
,编译 src/
的代码,同时生成 SourceMap
yarn babel --config-file ./.babelrc.esm.json --extensions '.ts' --source-maps -d dist/esm/ src/
# 编译完毕,会在 dist/esm 看到结果
# dist/esm 并不是兼容 Node 的版本,所以只能通过支持 ESM 的工具识别,如: webpack
Rollup + Babel 打包 UMD
Rollup 是一个类似 webpack 的模块打包工具,但 Rollup 适合把模块打包成单文件的形式,所以在这里可以尝试用 rollup 打包出 UMD 代码。
安装 Rollup:
- rollup Rollup 核心包
- @rollup/plugin-node-resolve 能让 Rollup 引用其他模块的插件
- @rollup/plugin-babel 能让 Rollup 借助 Babel 编译的插件
- @rollup/plugin-typescript 能让 Rollup 启动时读取 TypeScript 配置文件
- rollup-plugin-terser 能让 Rollup 对输出结果进行 Terser 压缩的插件
- rollup-plugin-bundle-size 能让 Rollup 显示 gzip 压缩前后代码大小的插件
yarn add -D rollup @rollup/plugin-node-resolve @rollup/plugin-babel @rollup/plugin-typescript rollup-plugin-terser rollup-plugin-bundle-size
根目录新建 Rollup 配置文件 rollup.config.ts
Rollup 可以支持 TypeScript,故直接使用 TypeScript 编写配置,避开不必要的错误。
@rollup/plugin-babel 在 Rollup 运行时会读取 Babel 默认配置文件,如根目录的
babel.config.json
下方的配置将生成目标代码:
dist/unit_transform.umd.js
未压缩的 UMD 代码,带 SourceMap。dist/unit_transform.umd.umi.js
经过 Terser 压缩的 UMD 代码。
注意 nodeResolve 和 babel,甚至 Rollup 本身只识别 .js
代码,对于 TypeScript 项目,要配置 extensions
让它们识别 TS 文件。
import { defineConfig } from 'rollup'
import { nodeResolve } from '@rollup/plugin-node-resolve'
import { babel } from '@rollup/plugin-babel'
import { terser } from 'rollup-plugin-terser'
// @ts-ignore
import bundleSize from 'rollup-plugin-bundle-size'
export default defineConfig({
input: './src/index.ts',
output: [
{
file: './dist/unit_transform.umd.js',
format: 'umd',
name: 'unit_transform', // umd 导出的名称
sourcemap: 'inline', // 把 SourceMap 写在目标代码里面
},
{
file: './dist/unit_transform.umd.min.js',
format: 'umd',
name: 'unit_transform',
plugins: [terser()],
},
],
plugins: [
bundleSize(), // e.g. 命令行附带 unit_transform.umd.js: 2.91 kB → 923 B (gzip)
nodeResolve({ extensions: ['.ts'] }),
babel({ extensions: ['.ts'] }),
],
})
指定 Rollup 配置文件,执行 Rollup 打包。
注意 –configPlugin typescript 是为了能让 Rollup 读取 TypeScript 配置文件
yarn rollup -c rollup.config.ts --configPlugin typescript
# 打包完成,在 dist/ 看到结果
# 挑其中一份 UMD 代码,复制到浏览器 Console 执行
# 即可通过 unit_transform、window.unit_transform 调用模块
# 其中变量 unit_transform 即是 UMD 导出的名称
TypeScript 生成类型声明
带有具体实现的 js 代码已经生成了,但它们都没有类型声明,其他的 TS 项目引用这个包时没法获得代码提示,所以在这里给库项目生成类型声明文件(.d.ts)。
tsconfig.json
- rootDir 相对于
src/
生成 - declaration 启用生成 .d.ts
- emitDeclarationOnly 仅生成 .d.ts ,不生成JS
JS 部分已经由 Babel 生成了
- declarationDir 生成 .d.ts 的目标目录
- include 仅对
src/
内的 TS 生成 .d.ts编译器将专注 src/ 生成,忽略根目录和测试目录的 TS 文件
{
"compilerOptions": {
...
+ "rootDir": "./src",
+ "declaration": true,
+ "emitDeclarationOnly": true,
+ "declarationDir": "./dist/types",
},
+ "include": ["./src"]
}
配置好 TS,执行生成
yarn tsc
# 生成完毕,结果存放 dist/types
整合项目
整合编译过程
现在把分发部分的构建过程整合起来,手写 yarn build
实现一次性生成所有的分发格式,编译前清空旧的结果 dist/
,所以安装 rimraf 工具进行清空。
yarn add -D rimraf
# 等同于 Unix 的 rm,但它能跨平台执行
package.json
{
...
+ "scripts": {
+ "build:types": "tsc",
+ "build:cjs": "babel --config-file ./.babelrc.cjs.json --extensions '.ts' --source-maps -d dist/cjs/ src/",
+ "build:esm": "babel --config-file ./.babelrc.esm.json --extensions '.ts' --source-maps -d dist/esm/ src/",
+ "build:umd": "rollup -c rollup.config.ts --configPlugin typescript",
+ "build:clean": "rimraf -rf ./dist",
+ "build": "yarn build:clean && yarn build:cjs && yarn build:esm && yarn build:umd && yarn build:types"
+ }
...
}
执行 yarn build
能保证分发结果 dist/
保持最新。
引出 CJS 根入口
虽然 dist/
分发的 ESM、CJS 格式无误,但是入口文件(index.js)毕竟不是在根目录,在项目开发时,经常能在库项目根目录直接引用 index.js,如:
import { Card } from 'antd'
import { Button } from 'bootstrap'
库项目和它们一样,都应该在根目录给出一个入口,供其他项目引用,但是这个入口存放在 dist/
里面,而不是根目录,对于这个情况,可以在根目录直接建立入口(index.js),把 dist/
的模块引出来。
根目录 index.js
// 在此之前,CommonJS 需要包名和路径引用:
// const unit_transform = require('unit_transform/dist/cjs')
module.exports = require('./dist/cjs')
module.exports.default = require('./dist/cjs').default
// 此时,CommonJS 可以通过包名直接引用:
// const unit_transform = require('unit_transform')
因应 package.json
的规范,同时应该把入口写在 package.json 里面:
{
...
+ "main": "dist/cjs/index.js",
...
}
引出 ESM 根入口和 Tree Shaking
上述配置是针对 CommonJS 的,对于 ES Module,webpack 会读取 package.json
中的 module 作为模块入口。
可以把 CommonJS 的入口作为 ES Module 的入口,但 webpack 无法分析哪些代码未被引用,造成前端项目 Tree Shaking 失效。
package.json
配置 Tree Shaking
-
module 表示 ES Module 的模块入口。
-
sideEffects webpack 会读取它,如果为 false,表示所有代码都模块化了,遇到未引用的代码可直接去掉。
// 显式引用了模块,webpack 会引入 createUnitTransform 代码 import { createUnitTransform } from 'unit-transform' // 如果 sideEffect: false,则 webpack 会去掉下方2段代码,导致这部分代码没有被执行 // 如果 sideEffect: ["*.css"],则 webpack 继续引入 ./my-style.css 代码 import './my-code.js' import './my-style.css'
{
...
"main": "dist/cjs/index.js",
+ "module": "dist/esm/index.js",
+ "sideEffects": false,
...
}
引出类型声明入口
同样地, .d.ts 也需要配置入口,否则其他项目的 TS 编译器会搜索根目录的 index.d.ts。
package.json
配置类型声明
{
...
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"sideEffects": false,
+ "types": "dist/types/index.d.ts",
...
}
Husky 自动化: Git 提交前自动编译
由于功能更新、错误修复等原因,修改 src/
源码后,必须执行 yarn build
让 dist/
结果保持最新,才能继续使用 Git Commit。其实很多时候会忘记执行 yarn build
,其他项目拿到的分发还是旧的,于是会出现这种情况:

改完bug提交完发现忘记build了于是回去build再重新提交
Husky 是一个基于钩子的自动化管理工具,它可以在 Git 一些操作前触发一些自动化动作(如: 提交前更新版本、重新编译proto),一些 React 组件库会利用 Husky 在 Git Commit 后自动刷新文档站点。
回到这里,为了避免每次手动构建的重复工作,试着用 Husky 配置自动化:
# 先在当前项目建立 Git 记录
# git init
# 安装 husky
yarn add -D husky
修改 package.json
这里表示:在每次安装依赖后,yarn 会自动运行 prepare,初始化 husky:
{
...
"scripts": {
"build": ...,
+ "prepare": "husky install"
}
...
}
编辑完毕,再次执行 yarn
,根目录会看到 .husky/
添加一个 Git Hook,告诉 Husky:
“在提交前,记得执行
yarn build
,把最新的编译结果也加到 Git 里面”
yarn husky add .husky/pre-commit "yarn build && git add dist/"
git add .husky/pre-commit
现在 husky 已经配置好了,当项目进行 Git Commit 时,会对 dist/
进行一次重新构建,再把新的结果添加进 Git 中,随着其他改动一起完成一次提交。
附录
打包工具 Rollup(https://rollupjs.org/)
自动化工具 Husky(https://typicode.github.io/husky)
单元测试工具 JEST(https://jestjs.io/)
参考了 Axios(https://github.com/axios/axios)