rollup 是一个用于 js 的模块打包工具,其作用包括:
- 高效的模块打包
- ES Modules 支持:Rollup 对 ES Modules(ECMAScript 模块)有很好的支持。它可以将多个小的 ES Modules 模块合并成一个或几个优化后的文件,减少网络请求次数,提高应用的加载速度。
- Tree-shaking:Rollup 能够进行有效的 “Tree-shaking”。这意味着它可以检测和去除未被使用的代码,只打包实际用到的代码,从而减小最终生成的包的大小。例如,如果一个模块中有多个函数,但你的项目只使用了其中一个函数,Rollup 会在打包过程中去除未使用的函数,减小包体积。
- 代码优化和转换
- 代码压缩:Rollup 可以与各种代码压缩工具集成,如 UglifyJS、terser 等,对打包后的代码进行压缩,去除不必要的空格、注释等,进一步减小包的大小。
- 语法转换:通过插件,Rollup 可以进行语法转换,例如将新的 JavaScript 语法转换为旧版本浏览器能够理解的语法。例如,使用 Babel 插件可以将 ES6+ 的代码转换为 ES5 代码,以确保在旧浏览器中的兼容性。
- 库和框架开发
- 生成库文件:Rollup 非常适合用于开发 JavaScript 库。它可以生成多种格式的输出,如 CommonJS、UMD(通用模块定义)、ES Modules 等,方便你的库在不同的项目环境中使用。
- 外部依赖处理:在开发库时,Rollup 可以轻松处理外部依赖。你可以指定哪些模块是外部依赖,不被打包进库文件中,而是在运行时从外部加载。这对于减小库的体积和避免重复打包常用的库非常有用。
- 插件生态系统
- 丰富的插件:Rollup 拥有一个活跃的插件生态系统,可以通过插件扩展其功能。例如,有插件可以处理 CSS 文件、图片资源、TypeScript 代码等。这使得 Rollup 可以适应各种项目需求,不仅仅局限于 JavaScript 模块打包。
- 自定义构建流程:通过插件,你可以自定义 Rollup 的构建流程,添加特定的处理步骤或优化策略。例如,你可以在构建过程中进行代码分析、生成文档、自动添加版权信息等。
下面我们通过实战来分别说明上述作用。
高效的模块打包
前端开发最初的样子
在最初的时候,我们写前端页面是这样的。
创建一个文件夹,写 html、css、js,在 html 中引入 css 和 js。发布的话,将整个文件夹部署在静态文件服务器上即可。例如下面的文件夹结构:
web0
├── index.html
└── main.js
其中 index.html 的内容为:
<!DOCTYPE html>
<body>
<h2>商品销量: <span id="goodSale"></span></h2>
<h2>营业额: <span id="turnover"></span></h2>
<script src="./main.js"></script>
</body>
</html>
main.js 的内容为:
var goodSale = 0;
var goodSaleTag = document.getElementById("goodSale");
var turnoverTag = document.getElementById("turnover");
function changeGoodSaleTag() {
goodSaleTag.innerText = goodSale;
}
function changeTurnoverTag() {
turnoverTag.innerHTML = goodSale * 9.9;
}
function updatePage() {
changeGoodSaleTag();
changeTurnoverTag();
}
updatePage();
window.setInterval(() => {
goodSale += 1;
updatePage();
}, 1500);
用浏览器打开 index.html 页面,可以看到商品销量和营业额都在动态改变。
模块拆分
随着我们的项目越来越大,在一个 js 中进行代码开发显然是不合理的。模块化能够有效提高软件开发可维护性的和开发效率,也便于多人协同开发(如果在一个文件中协同开发,光处理冲突就要耗费大量时间)。
我们将 web0 项目的 js 进行模块拆分,将销量和营业额相关逻辑独立开来,拆分后的文件目录结构为:
web1
├── goodSale.js
├── index.html
├── main.js
└── turnover.js
其中 index.html 的内容为:
<!DOCTYPE html>
<body>
<h2>商品销量: <span id="goodSale"></span></h2>
<h2>营业额: <span id="turnover"></span></h2>
<script src="./goodSale.js"></script>
<script src="./turnover.js"></script>
<script src="./main.js"></script>
</body>
</html>
goodSale.js 的内容为:
var goodSale = 0;
var goodSaleTag = document.getElementById("goodSale");
function changeGoodSaleTag() {
goodSaleTag.innerText = goodSale;
}
turnover.js 的内容为:
var turnoverTag = document.getElementById("turnover");
function changeTurnoverTag() {
turnoverTag.innerHTML = goodSale * 9.9;
}
main.js 的内容为:
function updatePage() {
changeGoodSaleTag();
changeTurnoverTag();
}
updatePage();
window.setInterval(() => {
goodSale += 1;
updatePage();
}, 1500);
浏览器打开 web1/index.html 文件,效果与 web0 相同。不过这个版本的前端就可以很容易地进行多人开发了: 商品销售业务相关的同事只需要维护 goodSale.js 文件,营收相关的同事只需要维护 turnover.js 文件。
不过这里有2个问题:
- html 加载 js 文件过多问题,web0 我们只需要加载1个js文件,现在需要加载3个js文件,加载速度相对变慢;
- html 中的 script 脚本执行顺序的改变可能影响程序的执行,将
<script src="./goodSale.js"></script>
放在<script src="./main.js"></script>
后,页面就不能正常显示数据了,控制台报错:Uncaught ReferenceError: changeGoodSaleTag is not defined
。这是因为 js 间存在依赖关系,被依赖的 js 加载晚于依赖 js 的加载导致的。如果项目大了之后,模块之间的依赖非常复杂,如果要人手工维护加载顺序是不可能的。
rollup: ES Modules 支持
要解决上面的两个问题,就可以请 rollup 出马了。
先通过命令 npm install --global rollup
安装。
改造 js 文件为 ES Modules 的格式。
goodSale.js 内容为:
export const good = {
sale: 0,
};
const goodSaleTag = document.getElementById("goodSale");
export function changeGoodSaleTag() {
goodSaleTag.innerText = good.sale;
}
因为在 ES Modules中,import 导入的绑定是只读的,不能重新赋值,因此将 goodSale 放在一个对象中,变为 good.sale。
turnover.js 内容为:
import { good } from "./goodSale";
const turnoverTag = document.getElementById("turnover");
export function changeTurnoverTag() {
turnoverTag.innerHTML = good.sale * 9.9;
}
main.js 内容为:
import { changeGoodSaleTag, good } from "./goodSale";
import { changeTurnoverTag } from "./turnover";
function updatePage() {
changeGoodSaleTag();
changeTurnoverTag();
}
updatePage();
window.setInterval(() => {
good.sale += 1;
updatePage();
}, 1500);
执行 rollup main.js -o dist/bundle.js
命令,将在 dist 文件夹下创建 bundle.js 脚本。
修改 index.html 的脚本导入,只导入 bundle.js: <script src="./dist/bundle.js"></script>
。打开 index.html,可以看到页面正常执行。
再看一下 bundle.js 的内容:
const good = {
sale: 0,
};
const goodSaleTag = document.getElementById("goodSale");
function changeGoodSaleTag() {
goodSaleTag.innerText = good.sale;
}
const turnoverTag = document.getElementById("turnover");
function changeTurnoverTag() {
turnoverTag.innerHTML = good.sale * 9.9;
}
function updatePage() {
changeGoodSaleTag();
changeTurnoverTag();
}
updatePage();
window.setInterval(() => {
good.sale += 1;
updatePage();
}, 1500);
可以看到 rollup 将 3 个 js 文件合并到一个 bundle.js 中,并合理地处理模块间的依赖关系。
rollup: Tree-shaking
rollup 还能将没有用到的代码剔除,不打入 bundle.js 文件中。例如在 main.js 中添加函数 mainFoo
:
function mainFoo() {
console.log("this is mainFoo");
}
在 turnover.js 中添加函数 turnoverFoo
:
export function turnoverFoo() {
console.log("this is mainFoo");
}
这两个函数都只定义,没有使用。通过 rollup main.js -o dist/bundle.js
打包后发现,这两个函数都没进入 bundle.js,这就是 Tree-shaking 的效果。
代码优化和转换
代码压缩
为了减少打包后的 js 文件,可以通过 --compact
参数进行压缩,rollup main.js -o dist/bundle.js --compact
执行后,生成的 bundle.js 为:
const good = {
sale: 0,
};
const goodSaleTag = document.getElementById("goodSale");
function changeGoodSaleTag() {
goodSaleTag.innerText = good.sale;
}const turnoverTag = document.getElementById("turnover");
function changeTurnoverTag() {
turnoverTag.innerHTML = good.sale * 9.9;
}function updatePage() {
changeGoodSaleTag();
changeTurnoverTag();
}
updatePage();
window.setInterval(() => {
good.sale += 1;
updatePage();
}, 1500);
可以看到,相比与之前的21行,--compact
将空行删除了。
另外,随着配置的增大,rollup 命令也越来越复杂,我们可以通过 rollup.config.mjs 将配置信息都存放其中。
rollup main.js -o dist/bundle.js --compact
命令对应的配置文件 rollup.config.mjs 内容为:
export default {
input: "main.js",
output: {
compact: true,
file: "dist/bundle.js",
},
};
执行 rollup -c
,生成相同的 dist/bundle.js,文件大小为 443B。
通过 rollup-plugin-uglify 插件压缩
如果对上述压缩效果不满意,我们可以借助插件 rollup-plugin-uglify
进一步进行压缩。
先通过 npm init -yes
对项目进行初始化,再通过 npm install rollup-plugin-uglify --dev
安装插件。
修改 rullup.config.mjs,引入插件:
import { uglify } from "rollup-plugin-uglify";
export default {
input: "main.js",
output: {
compact: true,
file: "dist/bundle.js",
},
plugins: [uglify()],
};
在通过 rollup -c
生成 dist/bundle.js,结果为:
let good={sale:0},goodSaleTag=document.getElementById("goodSale");function changeGoodSaleTag(){goodSaleTag.innerText=good.sale}let turnoverTag=document.getElementById("turnover");function changeTurnoverTag(){turnoverTag.innerHTML=9.9*good.sale}function updatePage(){changeGoodSaleTag(),changeTurnoverTag()}updatePage(),window.setInterval(()=>{good.sale+=1,updatePage()},1500);
可以看到被再度压缩,查看其大小为 376B。
通过 terser 插件压缩
如果对上述压缩还不满意,例如变量名过长。可以使用 terser 压缩。
先通过 npm rm rollup-plugin-uglify
删除 uglify 插件,因为可能与 terser 存在版本冲突。
安装插件 npm install @rollup/plugin-terser --dev
,修改 rollup.config.mjs :
import terser from "@rollup/plugin-terser";
export default {
input: "main.js",
output: {
compact: true,
file: "dist/bundle.js",
},
plugins: [terser()],
};
打包后 bundle.js 的内容为:
const e={sale:0},n=document.getElementById("goodSale");const t=document.getElementById("turnover");function o(){n.innerText=e.sale,t.innerHTML=9.9*e.sale}o(),window.setInterval((()=>{e.sale+=1,o()}),1500);
bundle.js 的文件大小仅为 205B。
压缩结果总结
压缩根据 | bundle.js 大小 |
---|---|
默认 | 448 B |
添加--compact 参数 | 443 B |
使用 uglify 插件压缩 | 376 B |
使用 terser 插件压缩 | 205 B |
可以看出,一个小小的示例程序,使用 terser 后就能压缩一半以上。
代码转换
我们不能保证用户的浏览器是最新的,毕竟连 windows xp 仍然可以偶尔看到。老版本的浏览器可能不支持新的 js 语法,如果客户要求能在老版本浏览器上打开,那我们要怎么办?难道要放弃新语法,再学一下老语法怎么用,之后才开始开发吗?
有了 Babel 之后,这个问题就迎刃而解了。Babel 可以将使用了最新 JavaScript 语法(如 ES2015+、ESNext)的代码转换为向后兼容的 JavaScript 代码,确保你的代码可以在旧版本的浏览器或其他不支持最新特性的环境中运行。
在使用 rollup 打包时,就可以使用 babel 插件对代码进行转换。
先新建文件夹 babel-test,通过 npm init -y
进行初始化,通过 npm add @babel/preset-env @rollup/plugin-babel --dev
安装 babel 插件。
创建 rollup.config.mjs 配置:
import babel from "@rollup/plugin-babel";
export default {
input: "index.js",
output: {
file: "dist/bundle.js",
},
plugins: [
babel({
exclude: "**/node_modules/**",
presets: [
[
"@babel/preset-env",
{
targets: {
chrome: "78",
},
},
],
],
}),
],
};
该配置指定 rollup 编译成 chrome 72版本能运行的 js 文件。在 index.js 中我们使用 es 比较新的语法 – 可选链操作符 ?.
:
const a = { x: { v: 100 } };
console.log(a.x?.v);
通过 rollup -c
编译出的 bundle.js 文件为:
var _a$x;
const a = {
x: {
v: 100
}
};
console.log((_a$x = a.x) === null || _a$x === void 0 ? void 0 : _a$x.v);
将 targets 中需要的 chrome 修改为 “110”,打包的结果为:
const a = {
x: {
v: 100
}
};
console.log(a.x?.v);
上述就是 rollup 通过 babel 进行代码转换的功能。babel 的功能非常强大,想了解可以查阅相关文档。
库和框架开发
生成库文件
生成 CommonJS 模块规范的库
node.js 默认支持的是 CommonJS 模块规范。
我们在文件夹下建立 foo.js 文件,作为我们要发布的库。
export function say() {
console.log("this is foo");
}
其他人获取该库之后,想在 nodejs 下使用,因此编写了下面的 index.js 代码:
const { say } = require("./foo.js");
say();
执行 node index.js
发现报错了:
lib-test/foo.js:1
export function say() {
^^^^^^
SyntaxError: Unexpected token 'export'
at internalCompileFunction (node:internal/vm:77:18)
at wrapSafe (node:internal/modules/cjs/loader:1288:20)
...
这是因为 CommonJS 规范的 js 文件引入了 ES Module 规范的 js 文件导致的。我们将 foo.js 编译为 CommonJS 模式,rollup.config.mjs 的内容为:
export default {
input: "foo.js",
output: {
dir: "dist",
format: "cjs",
},
};
执行 rollup -c
,生成 dist/foo.js,内容为:
'use strict';
function say() {
console.log("this is foo");
}
exports.say = say;
可以看到已经是 CommonJS 规范了,再将 index.js 中导入语句改为 const { say } = require("./dist/foo.js");
,执行 node index.cjs
的结果为: this is foo
,执行正常。
生成 ES Module 模块规范的库
如果现在还有一个 main.js 文件也希望引用 dist/foo.js (为什么不直接引入 foo.js?因为 dist 文件夹下的才是打包发布的库),main.js 的内容为:
import { say } from "./dist/foo.js";
say();
修改 package.json 中的 type 字段: "type": "module"
,表示以 ES Module 规范运行 node。
执行 node main.js
,不意外地也报错了:
node main.js
(node:94467) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)
lib-test/main.js:1
import { say } from "./dist/foo.js";
^^^^^^
SyntaxError: Cannot use import statement outside a module
at internalCompileFunction (node:internal/vm:77:18)
at wrapSafe (node:internal/modules/cjs/loader:1288:2
...
这是因为我们在 ES Module 规范中尝试引入 CommonJS 规范的包。一种解决方案是修改 rollup.config.mjs 中的 output.format 字段:
export default {
input: "foo.js",
output: {
dir: "dist",
format: "es", // 打包出的为 ES Module 规范的库
},
};
重新打包后,main.js 执行正常。但 CommonJS 模式的 js 文件又不能使用了,那能不能导出在两种模式下都能用的库呢?
答案是可以的。
生成 UMD 格式的库
修改 rollup.config.mjs :
export default {
input: "foo.js",
output: {
file: "dist/foo.js",
format: "umd",
name: "fooLib",
},
};
将导出格式 output.format
改为 umd
,output.name
字段指定导出的模块名为 fooLib
。
修改 main.js 的内容:
import "./dist/foo.js";
fooLib.say();
执行 node main.js
,输出为 this is foo
,表示 UMD 格式的库在 ES Module 的 js 中生效了。
将 package.json 中的 type
改为 commonjs
,执行 node index.js
,也输出 this is foo
,表示 UMD 格式的库在 CommonJS 的 js 中也生效了。
外部依赖处理
在 Rollup 中,可以通过指定外部依赖(external dependencies)来避免将某些模块打包到输出文件中,而是在运行时从外部获取这些依赖。这样可以减小输出文件的大小,并确保使用相同依赖的不同项目可以共享这些依赖。下面我们来测试一下:
先创建一个文件夹 out-dep-test,通过 npm init -y
进行初始化。想要将依赖库打包到输出文件中,我们需要 @rollup/plugin-node-resolve
插件的帮助。
通过 npm install @rollup/plugin-node-resolve -d
安装 rollup 插件,通过 npm install chalk dayjs
安装测试的依赖库。
创建 main.js,其中使用了两个依赖库:
import dayjs from "dayjs";
import chalk from "chalk";
export function useDayjsFunc() {
console.log(dayjs().format("YYYY-MM-DD HH:mm:ss"));
}
export function useChalkFunc() {
console.log(chalk.blueBright("hello, world"));
}
rollup.config.mjs 的内容为:
import resolve from "@rollup/plugin-node-resolve";
export default {
input: "main.js",
output: {
file: "dist/mytest.js",
format: "es",
},
plugins: [resolve()],
external: ["dayjs"],
};
其中 external 表示作为外部库的依赖库,不会被打包到 bundle.js。rollup -c
打包后,查看 mytest.js 的内容为:
import dayjs from 'dayjs';
... clack 中引入的代码 ...
function useDayjsFunc() {
console.log(dayjs().format("YYYY-MM-DD HH:mm:ss"));
}
function useChalkFunc() {
console.log(chalk.blueBright("hello, world"));
}
export { useChalkFunc, useDayjsFunc };
可以看到,dayjs 是通过 import 从外部导入的,而 chalk 用到的代码直接被复制到了 mytest.js 中。
插件生态系统
rollup 的创建非常多,用到的话可以自行摸索,这里不做介绍。