Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码。Rollup是基于ES6 的打包方案,而不是以前的特殊解决方案,如 CommonJS 和 AMD。与Webpack
偏向于应用打包的定位不同,rollup.js
更专注于Javascript
类库打包,我们熟知的Vue
、React
等诸多知名框架或类库都是通过rollup.js
进行打包的。
一、选择rollup
的理由?
webpack
和rollup
在不同场景下,都能发挥自身优势作用。webpack
对于代码分割和静态资源导入有着“先天优势”,并且支持热模块替换(HMR
),而rollup
并不支持。
所以当开发应用时可以优先选择webpack
,但是rollup
对于代码的Tree-shaking
和ES6
模块有着算法优势上的支持,若项目只需要打包出一个简单的包,并是基于ES6
模块开发的,可以考虑使用rollup
。
webpack
从2.0
开始就已经支持Tree-shaking
,并在使用babel-loader
的情况下还可以支持es6 module
的打包。实际上,rollup
已经在渐渐地失去了当初的优势了。但是它并没有被抛弃,反而因其简单的API
、使用方式被许多库开发者青睐,如React
、Vue
等,都是使用rollup
作为构建工具的。
二、封装各项目共用的工具包gdpg-utils
这里以封装各项目共用的工具包 gdpg-utils 为例子讲解rollup
的使用,流程细节不细讲,都已文件代码的形式展示,有需要可结合官网文档配置理解。
1.最终的目录结构:
// 目录由 tree-node-cli 生成
npm i tree-node-cli -g
tree -L 4 -I "node_modules" > dir.md
gdpg-utils
├── CHANGELOG.md
├── LICENSE
├── README.md
├── dist // 打包生产的目录
│ ├── gdpg-utils.common.js
│ ├── gdpg-utils.esm.js
│ ├── gdpg-utils.js
│ └── index.js
├── index.html // 测试用的html
├── package-lock.json
├── package.json
├── rollup.config.js // rollup 配置文件
├── scripts
│ └── publish.js // 推送到远程目录的脚本
├── src // 打包的代码
│ ├── index.ts
│ ├── modules // 模块文件夹
│ │ ├── array
│ │ │ └── index.ts
│ │ ├── brower
│ │ │ └── index.ts
│ │ ├── method
│ │ │ └── index.ts
│ │ ├── native
│ │ │ ├── android.ts
│ │ │ ├── index.ts
│ │ │ └── pc.ts
│ │ ├── number
│ │ │ └── index.ts
│ │ ├── object
│ │ │ └── index.ts
│ │ ├── storage
│ │ │ └── index.ts
│ │ ├── string
│ │ │ └── index.ts
│ │ └── tool
│ │ └── index.ts
│ └── typing.d.ts // ts模块描述文件
├── stats.html // rollup-plugin-visualizer 生成的包分析文件
└── tsconfig.json // typescript 配置
└── yarn.lock
1.1 package.json
{
"name": "gdpg-utils",
"version": "1.0.10",
"description": "gdpg web js utils",
"keywords": [
"utils",
"tool"
],
"main": "dist/gdpg-utils.common.js",
"module": "dist/gdpg-utils.esm.js",
"browser": "dist/gdpg-utils.js",
"scripts": {
"dev": "rollup --config rollup.config.js --watch --environment ENV:dev",
"build": "rollup --config rollup.config.js --environment ENV:prod",
"pub": "node scripts/publish.js"
},
"author": "",
"license": "ISC",
"publishConfig": {
"registry": "http://127.0.0.1:8081/repository/npm-hosted/"
},
"devDependencies": {
"@babel/core": "^7.15.0",
"@babel/preset-env": "^7.15.0",
"@rollup/plugin-alias": "^3.1.5",
"@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-commonjs": "^20.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.0.4",
"@rollup/plugin-typescript": "^8.2.5",
"commander": "^8.1.0",
"dayjs": "^1.10.6",
"js-cookie": "^3.0.0",
"lodash": "^4.17.21",
"rollup": "^2.56.0",
"rollup-plugin-dev": "^1.1.3",
"rollup-plugin-livereload": "^2.0.5",
"rollup-plugin-replace": "^2.2.0",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-visualizer": "^5.5.2",
"shelljs": "^0.8.4",
"store": "^2.0.12",
"tslib": "^2.3.0",
"typescript": "^4.3.5"
}
}
1.2 rollup.config.js
import typescript from '@rollup/plugin-typescript' // typescript插件
import json from '@rollup/plugin-json'; // 允许从json中导入数据
import nodeResolve from '@rollup/plugin-node-resolve' // 帮助寻找node_modules里的包
import commonjs from '@rollup/plugin-commonjs' // 将非ES6语法的包转为ES6可用
import babel from '@rollup/plugin-babel' // rollup 的 babel 插件,ES6转ES5
import dev from 'rollup-plugin-dev'; // 开启本地服务器
import livereload from 'rollup-plugin-livereload'; // 开启热更新
import {
terser
} from 'rollup-plugin-terser';
import {
visualizer
} from 'rollup-plugin-visualizer';
import pkg from './package.json'
export default {
input: "src/index.ts", // 入口文件
output: [{ // 不同类型的出口文件
file: pkg.main,
format: 'cjs', // CommonJS
exports: 'auto'
},
{
file: pkg.module,
format: 'es', // ES模块文件
exports: 'auto'
},
{
file: pkg.browser,
format: 'umd', // 通用模块定义,以amd,cjs和iife为一体
name: 'gdpg-utils',
exports: 'auto'
},
],
plugins: [
json(),
typescript(),
nodeResolve({
browser: true,
main: true
}),
commonjs(),
babel({
exclude: 'node_modules/**', // 忽略 node_modules
babelHelpers: true, // 开启体积优化
}),
process.env.ENV === 'prod' ? terser() : null,
process.env.ENV === 'dev' ? livereload() : null,
process.env.ENV === 'dev' ? dev({
port: 8888,
dirs: '',
}) : null,
process.env.ENV === 'prod' ? visualizer() : null,
],
watch: {
exclude: 'node_modules/**',
include: 'src/**'
}
};
1.3 format字段
这里的format
字段大家看了可能不太理解,尤其是里面的cjs
代表什么意思;由于JS有多种模块化方式,Rollup可以针对不同的模块规范打包出不同的文件,它有以下五种选项:
- amd: 异步模块定义,用于像RequireJS这样的模块加载器
- cjs:CommonJS,适用于 Node 和 Browserify/Webpack
- es:ES模块文件
- iife:自执行模块,适用于浏览器环境
script
标签 - umd:通用模块定义,以amd,cjs和iife为一体
1.4 plugins字段
插件拓展了Rollup处理其他类型文件的能力,它的功能有点类似于Webpack的loader
和plugin
的组合;不过配置比webpack中要简单很多,不用逐个声明哪个文件用哪个插件处理,只需要在plugins
中声明,在引入对应文件类型时就会自动加载。项目使用了下面这些插件
import typescript from '@rollup/plugin-typescript' // typescript插件
import json from '@rollup/plugin-json'; // 允许从json中导入数据
import nodeResolve from '@rollup/plugin-node-resolve' // 帮助寻找node_modules里的包
import commonjs from '@rollup/plugin-commonjs' // 将非ES6语法的包转为ES6可用
import babel from '@rollup/plugin-babel' // rollup 的 babel 插件,ES6转ES5
import dev from 'rollup-plugin-dev'; // 开启本地服务器
import livereload from 'rollup-plugin-livereload'; // 开启热更新
2. src目录的工具代码
2.1 src/index.ts
import * as array from './modules/array'
import * as brower from './modules/brower'
import * as method from './modules/method'
import * as number from './modules/number'
import * as object from './modules/object'
import * as string from './modules/string'
import * as native from './modules/native'
import * as tool from './modules/tool'
export default {
...array,
...brower,
...method,
...number,
...object,
...string,
...native,
...tool,
}
2.2 modules文件夹内容较多,这里简单举src/modules/number/index.ts
和 src/modules/tool/index.ts
的例子
// src/modules/number/index.ts
/**
* @description 生成指定范围的随机小数
*/
export const randomNumberInRange = (min: number, max: number) => Math.random() * (max - min) + min;
/**
* @description 计算数组或多个数字的总和
*/
export const sum = (...arr) => [...arr].reduce((acc, val) => acc + val, 0);
// src/modules/tool/index.ts
/**
* @description 深拷贝
*/
export function deepClone(data: any): any {
const type = Object.prototype.toString.call(data);
let result: {
[key: string]: any
};
if (type === '[object Object]') {
result = {};
} else if (type === '[object Array]') {
result = [];
} else {
return data;
}
Object.keys(data).forEach((key) => {
const value = data[key];
result[key] = deepClone(value);
});
return result;
}
/**
* @description async await 优雅处理方式
*/
export const awaitWrap = <T, U = any>(promise: Promise<T>): Promise<[U | null, T | null]> => promise
.then<[null, T]>((data: T) => [null, data])
.catch<[U, null]>((err) => [err, null]);
/**
* @description 对象数组去重
*/
export const unique = (sourceArr: Array<any>, data: Array<any>, key: string): Array<any> => {
const arr = [...data, ...sourceArr];
return arr.reduce((acc: Array<any>, cur: any) => {
const ids = acc.map((item) => item[key]);
return ids.includes(cur[key]) ? acc : [...acc, cur];
}, []);
};
3.执行开发命令和打包命令
3.1 本地开发
npm run dev
// "dev": "rollup --config rollup.config.js --watch --environment ENV:dev"
// --config rollup.config.js
- --config rollup.config.js // 使用 rollup.config.js 配置文件
- --watch // 监控(监控范围为rollup.config.js的watch配置)src目录的变换,动态打包更新 dist 目录
- --environment ENV:dev // 传递环境变量 ENV:dev
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>gdpg-utils test page</title>
</head>
<body>
<h1>gdpg-utils</h1>
<script type="module">
import utils from '../dist/gdpg-utils.esm.js';
console.log('randomNumberInRange %c⧭', 'color: #1d5673', utils.randomNumberInRange);
console.log('deepClone %c⧭', 'color: #00bf00', utils.deepClone);
</script>
</body>
</html>
关于调试: 我开发包的过程中用到了两种调试方式:
- 方法1:直接通过通过 index.html 调试,(原理:借助
rollup-plugin-dev
和rollup-plugin-livereload
搭建这种方式的调试环境) - 方法2:通过npm link(软链接)在项目中调试正在开发的包
在包目录下执行npm link
;
在项目目录下执行npm link gdpg-utils
即可使用该包(执行npm unlink gdpg-utils
可以删除包链接);
3.2 打包
npm run build
打包结果:
包结构分析:未压缩时在37kb
左右
3.3 推送到npm私库
推送脚本 scripts/publish.js
const path = require('path');
const shelljs = require('shelljs');
const program = require('commander');
const targetFile = path.resolve(__dirname, '../package.json');
const packagejson = require(targetFile);
const currentVersion = packagejson.version;
const versionArr = currentVersion.split('.');
const [mainVersion, subVersion, phaseVersion] = versionArr;
// 默认版本号
const defaultVersion = `${mainVersion}.${subVersion}.${+phaseVersion+1}`;
let newVersion = defaultVersion;
// 从命令行参数中取版本号
program
.option('-v, --versions <type>', 'Add release version number', defaultVersion);
program.parse(process.argv);
if (program.versions) {
newVersion = program.versions;
}
console.log('newVersion:', newVersion);
function publish() {
shelljs.sed('-i', '"name": "ktools"', '"name": "@kagol/ktools"', targetFile);
shelljs.sed('-i', `"version": "${currentVersion}"`, `"version": "${newVersion}"`, targetFile);
shelljs.exec('npm run build');
shelljs.exec('npm publish');
}
publish();
npm run pub
3.4 在vue项目中引入使用
包结构分析:代码压缩+gzip压缩后大小在5kb
左右
// TODO: 单元测试、文档预览、提交规范
三、Tree Shaking
由于Rollup本身支持ES6模块化规范,因此不需要额外配置即可进行Tree Shaking
四、代码分割
Rollup代码分割和Parcel一样,也是通过按需导入的方式;但是我们输出的格式format不能使用iife,因为iife自执行函数会把所有模块放到一个文件中,可以通过amd
或者cjs
等其他规范。
export default {
input: "./index.ts",
output: {
//输出文件夹
dir: "dist",
format: "amd",
},
};
这样我们通过import()
动态导入的代码就会单独分割到独立的js中,在调用时按需引入;不过对于这种amd模块的文件,不能直接在浏览器中引用,必须通过实现AMD标准的库加载,比如Require.js
。
五、小结
通过对Rollup的使用介绍,我们发现它有以下优点:
- 配置简单,打包速度快
- 自动移除未引用的代码(内置tree shaking) 但是他也有以下不可忽视的缺点:
- 开发服务器不能实现模块热更新,调试繁琐
- 浏览器环境的代码分割依赖amd
- 加载第三方模块比较复杂