文章目录
- 初见 Rollup 的十大常见问题
- 1. 超神奇的 Rollup 英文解释!
- 2. 为什么 ESM 要比 CommonJS 要好呢?
- 3. 什么是 tree-shaking ?
- 4. 如何使用 Rollup 处理 CommonJS?
- 5. 为什么 node-resolve 不是一个内置功能?
- 6. 为什么在进行代码分割时,入口块中会出现额外的导入?
- 7. 如何将 polyfills 添加到 Rollup 包中?
- 8. Rollup 是用来构建库还是应用程序?
- 9. Rollup 和 Webpack 有什么区别呢?
- 10. 如何在浏览器中运行 Rollup?
- 写在最后
初见 Rollup 的十大常见问题
有看到社区记录了非常多的 Rollup 相关的一些问题,有些也是我刚接触 Rollup 时会疑惑的地方,在这里做一个记录和分享,希望你会喜欢。
1. 超神奇的 Rollup 英文解释!
如果打开 Rollup 官网,你会发现这样一段英文,好吧,上面图片就有。
rollup.js
The JavaScript module bundlerCompile small pieces of code into something larger and more complex
前面两句好像没什么,Rollup 是一个 JavaScript 的模块打包器,但第二句就神奇了:可以将小块代码编译成大块且复杂的代码。
哇!这什么东东,把小的东西整成复杂的?这难道就是传说中真正的高手:简单问题复杂化吗?不知道你是否用同样的疑惑哈,反正我是没太懂。
解释来了:Rollup 是一个 JavaScript 模块打包器,它的主要作用是将多个小的、相互依赖的 JavaScript 文件(也就是通常说的 “模块”)合并成一个或多个大的、更复杂的 JavaScript 文件。这些大的文件通常被称为 chunk,可以被浏览器或其他 JavaScript 运行环境直接执行。
这背后的最大原因是:浏览器或其他 JavaScript 运行环境无法解析工程化下的模块
例如,你可能有一个项目,其中包含许多小的 JavaScript 模块文件,每个文件都执行一个特定的任务。使用 Rollup,你可以将这些小的模块文件打包成一个大的 JavaScript 文件,这样在浏览器或 Node.js 环境中就能够执行了。这样做既利用了工程化带来的收益,也让执行环境能正常工作了。
所以,当我们说 Rollup 可以 “将小块代码编译成大块复杂的代码” 时,其意思是它可以将许多小的、相互依赖的模块文件合并成一个大的、包含所有模块代码的文件。
2. 为什么 ESM 要比 CommonJS 要好呢?
好吧,其实这个问题与 Rollup 没有直接的关系,只是常问,也是一个需要了解的点。
ESM 是 JavaScript 工程化结构的官方标准,有着明确发展方向;而 CommonJS 可以说是历史遗留规范,在 ES 模块提出之前用作权宜之计,官方在正式出 ESM 之前,社区几乎都采用的是 CommonJS 规范。
ES 模块允许进行静态分析,这有助于进行诸如 tree-shaking 和作用域提升之类的优化,同时还提供循环引用和实时绑定之类的高级功能。
3. 什么是 tree-shaking ?
Tree-shaking,也被称为"live code inclusion",是 Rollup 用于剔除在项目中未被真正使用代码的一种处理方式。
live code inclusion: 仅包含活代码(有效的代码)
它是一种 死代码消除方式,但就输出大小而言,它比其他方法更有效。名称源自模块的抽象语法树,算法会首先标记所有相关的语句,然后 “摇动语法树” 以删除所有死代码。它的理念类似于标记和清除垃圾收集算法。尽管这个算法并不局限于 ESM,但是 ESM 让 Rollup 可以将所有模块一起视为一个大的并且具有共享绑定的抽象语法树,所以会使得这个算法更加高效。
4. 如何使用 Rollup 处理 CommonJS?
Rollup 致力于实现 ESM 规范,而不在 Node.js、NPM、require() 和 CommonJS 的行为,所以这些模块的加载并不包含在 Rollup 的核心中;但 Rollup 提供了灵活的插件机制,加载 CommonJS 和使用 Node 的模块位置解析逻辑都均可使用插件来实现,只需 npm install
commonjs 和 node-resolve 插件,然后通过 rollup.config.js 文件启用它们,就应该可以开始使用了。除此之外,如果模块导入存在 JSON 文件,引入 json 插件即可。
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';export default {input: 'src/main.js',output: {file: 'bundle.js',format: 'cjs'},plugins: [nodeResolve(),commonjs(),json()]
};
5. 为什么 node-resolve 不是一个内置功能?
这主要有两个原因:
-
从哲学角度来看,这是因为 Rollup 本质上是对 Node 和浏览器中的原生模块加载器的一种 polyfill。在浏览器中,
import foo from 'foo'
是无法工作的,因为浏览器并不使用 Node 的解析算法,所以如果 Rollup 直接内置了 node-resolve 的话,这可能导致一些误解。 -
从实际层面来看,如果一个问题能够通过良好的 API 或者插件进行分离,那么开发软件就会变得容易得多。Rollup 的核心相当大,那么任何任何让它变得更大的设计都需要慎重地权衡。保持 Rollup 的精简,不仅技术债务的可能小更小,修复错误和添加功能也会更容易。
请参阅这个 issue 以获取更详细的解释。
6. 为什么在进行代码分割时,入口块中会出现额外的导入?
默认情况下,当创建多个块时,入口块的依赖项的导入将作为空导入添加到入口块本身。例如:
// input
// main.js
import value from './other-entry.js';
console.log(value);// other-entry.js
import externalValue from 'external';
export default 2 * externalValue;// output
// main.js
import 'external'; // this import has been hoisted from other-entry.js
import value from './other-entry.js';
console.log(value);// other-entry.js
import externalValue from 'external';
var value = 2 * externalValue;
export default value;
实际上,这并不会影响代码执行的顺序或行为,但它会加快你的代码的加载和解析速度。没有这个优化,JavaScript 引擎需要执行以下步骤来运行 main.js:
- 加载并解析 main.js。在最后,将发现 other-entry.js 的导入。
- 加载并解析 other-entry.js。在最后,将发现 external 的导入。
- 加载并解析 external。
- 执行main.js。
有了这个优化,JavaScript 引擎在解析入口模块后将发现所有传递依赖项,避免了瀑布式加载:
- 加载并解析 main.js。在最后,将发现导入到 other-entry.js 和 external 的导入。
- 加载并解析 other-entry.js 和 external。从 other-entry.js 导入的 external 已经被加载和解析。
- 执行main.js。
可能有些情况下你不希望这种优化,这种情况下,你可以通过 output.hoistTransitiveImports
选项关闭它。当使用 output.preserveModules
选项时,也不会应用这种优化。
7. 如何将 polyfills 添加到 Rollup 包中?
尽管Rollup通常会在打包时尽量保持精确的模块执行顺序,但在两种情况下并非总是如此:代码分割和外部依赖。这个问题在外部依赖中最为明显,参见以下示例:
// main.js
import './polyfill.js';
import 'external';
console.log('main');// polyfill.js
console.log('polyfill');
可以看到,执行顺序是 polyfill.js
→ external
→ main.js
。但如果你将其打包,你会得到:
import 'external';
console.log('polyfill');
console.log('main');
执行顺序会是 external
→ polyfill.js
→ main.js
。哇,怎么回事对吧,这并非是 Rollup 将导入包语句 import 'external'
放在顶部导致的问题,因为无论导入语句位于文件的哪个位置,总是会首先执行的。
这个问题可以通过创建多个 chunk
来解决,比如如果 polyfill.js 最终在与_main.js_ 不同的 chunk
中,那么就会得到正确的执行顺序。然而,Rollup 还没有自动的方式来做到这一点。对于代码分割,情况也是类似,因为 Rollup 始终会试图创建尽可能少的 chunk
。
对于大多数代码来说,这不是问题,因为Rollup可以保证:如果模块 A 导入模块 B,并且没有循环导入的话,那么 B 总是在 A 之前执行的。
然而,对于 polyfills 来说,这将会是一个问题,因为它们通常需要先执行,但又不希望在每个模块中都放置一个 polyfills 的导入。幸运的是,这并不需要:
- 如果所有的外部依赖均不依赖 polyfill,那么只需将 polyfill 导入作为每个静态入口点的第一个语句即可。
- 否则,将 polyfill 作为单独的入口或手动块,就可以确保它始终先执行。
8. Rollup 是用来构建库还是应用程序?
Rollup 已经被许多的 JavaScript 库使用,当然也可以用来构建绝大多数应用程序。然而,如果你想在旧的浏览器上使用代码分割或动态导入,那可能需要一个额外的运行时来处理加载就浏览器缺失的一些 chunk
。我们推荐使用 SystemJS Production Build,因为它很好地与 Rollup 格式的输出集成在一起,能够正确处理所有的 ESM 实时绑定和重新导出边缘情况。或者,你也可以使用 AMD 加载器。
9. Rollup 和 Webpack 有什么区别呢?
Rollup 和 Webpack 都是 JavaScript 模块打包器,但它们的关注点和使用场景有所不同:
- 设计理念:Rollup 更专注于 JavaScript 模块打包,特别是 ES6 模块,它的设计目标是提供更高效的代码打包和更好的代码优化(如tree-shaking)。而 Webpack 则是一个更通用的模块打包器,它不仅可以处理 JavaScript,还可以处理各种类型的资源,如 CSS、图片、字体等。
- 代码分割和动态导入:Webpack 对代码分割和动态导入的支持更为成熟,这使得 Webpack 在构建大型、复杂的前端应用程序时更为强大。而 Rollup 虽然也支持代码分割和动态导入,但可能需要更多的配置和插件。
- 输出格式:Rollup 支持更多的输出格式,包括ES模块、CommonJS、AMD、IIFE、UMD等,这使得 Rollup 更适合用于构建库。而Webpack 主要输出为自定义的模块格式,更适合构建应用程序。
- 开发服务器和热模块替换:Webpack 内置了开发服务器,并支持热模块替换(HMR),这对于开发环境非常有用。而Rollup则需要额外的插件或工具来提供这些功能。
上面所描述的点两者其实均可以做到,只是所侧重点不同。总的来说,Rollup和Webpack各有优势,选择哪一个取决于你的具体需求。
10. 如何在浏览器中运行 Rollup?
好吧,这个问题其实挺意外的,绝大部分情况都不会想在浏览器里面运行 Rollup 吧。
虽然常规的 Rollup 构建依赖于 Node 的一些特性,但 Rollup 也提供了一个用于在浏览器构建的版本,你可以通过以下方式安装它:
npm install @rollup/browser
在你的脚本中,通过以下方式导入它:
import { rollup } from '@rollup/browser';
或者,你可以从 CDN 导入,例如 ESM:
import * as rollup from 'https://unpkg.com/@rollup/browser/dist/es/rollup.browser.js';
UMD:
<script src="https://unpkg.com/@rollup/browser/dist/rollup.browser.js"></script>
这将创建一个全局变量 window.rollup
。由于浏览器构建无法访问文件系统,因此需要提供解析和加载你想要打包的所有模块的插件。如下示例:
const modules = {'main.js': "import foo from 'foo.js'; console.log(foo);",'foo.js': 'export default 42;'
};rollup.rollup({input: 'main.js',plugins: [{name: 'loader',resolveId(source) {if (modules.hasOwnProperty(source)) {return source;}},load(id) {if (modules.hasOwnProperty(id)) {return modules[id];}}}]}).then(bundle => bundle.generate({ format: 'es' })).then(({ output }) => console.log(output[0].code));
这个例子支持了两个导入,main.js 和 foo.js。下面这个例子是另一个使用绝对 URL 作为入口点并支持相对导入的例子。在这种情况下,我们只是重新打包 Rollup 本身,便可使它能用于任何其他 ESM 的URL:
rollup.rollup({input: 'https://unpkg.com/rollup/dist/es/rollup.js',plugins: [{name: 'url-resolver',resolveId(source, importer) {if (source[0] !== '.') {try {new URL(source);// If it is a valid URL, return itreturn source;} catch {// Otherwise make it externalreturn { id: source, external: true };}}return new URL(source, importer).href;},async load(id) {const response = await fetch(id);return response.text();}}]}).then(bundle => bundle.generate({ format: 'es' })).then(({ output }) => console.log(output));
写在最后
Rollup 在社区可以说一直是翘首,大量的库都在用 Rollup 打包,更不用说如今的红人 vite 了,更是以 Rollup 为底层。所以试着去了解一下也是蛮有价值的,当然,社区还有非常多的打包工具,包括 esbuild、tsup、gulp 等等。
不过万变不离其宗,其实打包工具底层基本都是一致的,只是可能在设计理念或者架构上有些差异,但始终绕不开这个范式,也很期待未来前端工程化发展能够有超越当前范式的模式和工具出现哇。
好吧,这是我从 LLM 学到,LLM 的出现就是 AI 在范式上的超越