常见问题解答
为什么 ES 模块比 CommonJS 模块更好?
ES 模块是官方标准,是 JavaScript 代码结构的明确前进方向,而 CommonJS 模块是一种特殊的历史格式,在 ES 模块被提出之前作为权宜之计。ES 模块允许静态分析,这有助于优化,例如树摇和范围提升,并提供高级功能,例如循环引用和实时绑定。
什么是“树摇”?
树摇,也称为“实时代码包含”,是 Rollup 消除给定项目中实际未使用的代码的过程。它是一种 死代码消除形式,但在输出大小方面可能比其他方法更有效。这个名字来源于模块的 抽象语法树(而不是模块图)。该算法首先标记所有相关语句,然后“摇动语法树”以删除所有死代码。它在思想上类似于 标记-清除垃圾收集算法。即使这种算法不限于 ES 模块,它们也使其更加有效,因为它们允许 Rollup 将所有模块一起视为具有共享绑定的一个大型抽象语法树。
如何在 Node.js 中使用 Rollup 与 CommonJS 模块?
Rollup 努力实现 ES 模块的规范,而不是 Node.js、NPM、require()
和 CommonJS 的行为。因此,CommonJS 模块的加载和 Node 的模块位置解析逻辑的实现都是可选的插件,默认情况下不包含在 Rollup 核心。只需 npm install
commonjs 和 node-resolve 插件,然后使用 rollup.config.js
文件启用它们,您应该一切就绪。如果模块导入 JSON 文件,您还需要 json 插件。
为什么 node-resolve 不是内置功能?
主要有两个原因
从哲学上讲,这是因为 Rollup 本质上是对 Node 和浏览器中本机模块加载器的 polyfill。在浏览器中,
import foo from 'foo'
不会起作用,因为浏览器不使用 Node 的解析算法。在实践层面上,如果这些问题通过良好的 API 清晰地分离,那么开发软件就容易得多。Rollup 的核心相当大,任何阻止它变得更大的东西都是好事。同时,修复错误和添加功能也更容易。通过保持 Rollup 精简,技术债务的可能性很小。
请参阅 此问题 以获取更详细的解释。
为什么在代码拆分时我的入口块中会出现额外的导入?
默认情况下,在创建多个块时,入口块的依赖项的导入将作为空导入添加到入口块本身。 示例
// 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
选项时,此优化也永远不会应用。
如何将 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
放在捆绑包顶部的导致的问题——无论导入位于文件中的哪个位置,导入始终首先执行。这个问题可以通过创建更多块来解决:如果 polyfill.js
位于与 main.js
不同的块中,将保留正确的执行顺序。但是,Rollup 中还没有自动执行此操作的方法。对于代码拆分,情况类似,因为 Rollup 试图创建尽可能少的块,同时确保不会执行不需要的代码。
对于大多数代码来说,这不是问题,因为 Rollup 可以保证
如果模块 A 导入模块 B 并且没有循环导入,那么 B 将始终在 A 之前执行。
但是,对于 polyfills 来说,这是一个问题,因为 polyfills 通常需要首先执行,但通常不希望在每个模块中都放置 polyfill 的导入。幸运的是,这不是必需的
- 如果没有依赖于 polyfill 的外部依赖项,那么将 polyfill 的导入作为每个静态入口点的第一个语句添加就足够了。
- 否则,另外将 polyfill 作为单独的入口或 手动块 将始终确保它首先执行。
Rollup 是用于构建库还是应用程序?
Rollup 已经被许多主要的 JavaScript 库使用,也可以用来构建绝大多数应用程序。但是,如果您想在旧浏览器中使用代码拆分或动态导入,您将需要一个额外的运行时来处理加载缺少的块。我们建议使用 SystemJS Production Build,因为它与 Rollup 的系统格式输出很好地集成,并且能够正确处理所有 ES 模块实时绑定和重新导出边缘情况。或者,也可以使用 AMD 加载器。
如何在浏览器中运行 Rollup 本身
虽然常规的 Rollup 构建依赖于一些 NodeJS 功能,但也有一个浏览器构建可用,它只使用浏览器 API。您可以通过以下方式安装它
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 本身,但它可以用于任何其他公开 ES 模块的 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 it
return source;
} catch {
// Otherwise make it external
return { 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));