{T}

常见问题

ES 模块比 CommonJS 模块更好

ES 模块是官方标准,是 JavaScript 代码结构的明确未来发展方向,而 CommonJS 模块是特殊的兼容型格式,被视为 ES 模块提出之前的一种临时解决方案。ES 模块允许静态分析,可帮助进行优化(如除屑优化和作用域提升),并提供高级功能(如循环引用和实时绑定)

如何在 Node.js 中使用 Rollup 和 CommonJS 模块

Rollup 力求实现 ES 模块的规范,而不是 Node.js、NPM、require() 和 CommonJS 的行为。因此,CommonJS 模块的加载和使用 Node 的模块位置解析逻辑都作为可选插件实现,不包含在 Rollup 核心中。只需 npm install commonjsnode-resolve 插件,然后使用 rollup.config.js 文件启用它们,即可完成设置

为什么 node-resolve 不是内置功能?

有两个主要原因:

  1. Rollup 本质上是一种类似于 Node 和浏览器中本机模块加载器的 polyfill。在浏览器中,import foo from 'foo' 将无法工作,因为浏览器不使用 Node 的解析算法
  2. 在实际层面上,如果这些问题有一个良好的 API 可以清晰地分离,那么开发软件就会更加容易。Rollup 的核心非常庞大,一切可以阻止其变得更大的东西都是好事。同时,修复错误和添加功能也更容易。通过保持 Rollup 精简,技术债务的潜在风险很小

为什么在代码分割时我的入口块中会出现额外的导入?

默认情况下,在创建多个块时,入口块的依赖项导入将作为空导入添加到入口块本身。请看示例:

JavaScript
// 输入
// main.js
import value from './other-entry.js';
console.log(value);

// other-entry.js
import externalValue from 'external';
export default 2 * externalValue;

输出

js
// main.js
import "external" // 这个导入已经从 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

  1. 加载和解析 main.js。最后,将发现对 other-entry.js 的导入
  2. 加载和解析 other-entry.js。最后,将发现对 external 的导入
  3. 加载和解析 external
  4. 执行 main.js

通过此优化,JavaScript 引擎将在解析入口模块后发现所有传递依赖关系,避免了瀑布式加载:

  1. 加载和解析 main.js。最后将发现对 other-entry.jsexternal 的导入
  2. 加载和解析 other-entry.jsexternal。从 other-entry.js 导入 external 已经被加载和解析
  3. 执行 main.js

可能存在不需要此优化的情况,在这种情况下,你可以通过 output.hoistTransitiveImports 选项关闭它。当使用 output.preserveModules 选项时,也不会应用此优化

如何将 polyfill 添加到 Rollup 产物中?

即使 Rollup 在打包时通常会尝试维护精确的模块执行顺序,但在两种情况下,这并不总是成立:代码分割和外部依赖。外部依赖的问题最为明显,参考示例:

JavaScript
// main.js
import './polyfill.js';
import 'external';
console.log('main');

// polyfill.js
console.log('polyfill');

这里的执行顺序是 polyfill.jsexternalmain.js。但当打包代码时,得到以下结果:

JavaScript
import 'external';

console.log('polyfill');

console.log('main');

使用 externalpolyfill.jsmain.js 的执行顺序。这不是由 Rollup 将 import 放在捆绑包顶部引起的问题——无论在文件中的位置如何,import 都会被首先执行。

此问题可以通过创建更多的块来解决:如果 polyfill.js 最终位于与 main.js 不同的块中,正确的执行顺序将得以保留。但在 Rollup 中还没有自动执行此操作的方法。对于代码分割,情况类似,因为 Rollup 正在尝试创建尽可能少的块,同时确保不执行不需要的代码

JavaScript
// main.js
import './polyfill.js';
import 'external';
console.log('main');

// polyfill.js
console.log('polyfill');

打包为多代码文件块输出:

JavaScript
// main.js
import './polyfill.js';
import 'external';

console.log('main');

// polyfill.js
console.log('polyfill');

对于大多数代码而言,这不是一个问题,因为 Rollup 可以保证:

如果模块 A 导入模块 B,且没有循环导入,那么模块 B 总是会在模块 A 之前被执行。

但是这对于 polyfill 来说是一个问题,因为它们通常需要首先执行,同时又通常不希望在每个模块中都放置一个 polyfill 的导入。幸运的是,这并不是必需的:

  1. 如果没有依赖于 polyfill 的外部依赖项,则在每个静态入口点的第一个语句中添加对 polyfill 的导入即可
  2. 否则,将 polyfill 作为单独的入口或 手动添加的块 也会始终确保它首先被执行

与其他 NPM 包

在某个时刻,项目可能会依赖于从 NPM 安装到 node_modules 文件夹中的软件包。与 Webpack 和 Browserify 等其他打包程序不同,Rollup 默认情况下不知道如何处理这些依赖项,需要添加一些配置。

添加一个名为 the-answer 的简单依赖项,它导出了生命、宇宙和一切问题的答案:

bash
npm install the-answer

修改 src/main.js 文件…

JavaScript
// src/main.js
import answer from 'the-answer';

export default function () {
  console.log('the answer is ' + answer);
}

然后运行 Rollup

bash
npm run build

会看到这样的警告:

JavaScript
(!) Unresolved dependencies
https://github.com/rollup/rollup/wiki/Troubleshooting#treating-module-as-external-dependency
the-answer (imported by main.js)

生成的 bundle.js 仍然可以在 Node.js 中使用,因为 import 声明会被转换为 CommonJS 的 require 语句,但是 the-answer 不会被包含在 bundle 中。此时需要一个插件

@rollup/plugin-node-resolve

@rollup/plugin-node-resolve 插件可以让 Rollup 找到外部模块

bash
npm install -D @rollup/plugin-node-resolve

修改配置文件:

JavaScript
// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';

export default {
  input: 'src/main.js',
  output: {
    file: 'bundle.js',
    format: 'cjs'
  },
  plugins: [resolve()]
};

当再次运行 npm run build 时,不会发出警告 - bundle 包含了导入的模块

@rollup/plugin-commonjs

一些库会暴露出 ES 模块,可以直接导入它们,the-answer 就是这样的一个模块。但是目前,大多数 NPM 上的包都以 CommonJS 模块的方式暴露。在这种情况下需要在 Rollup 处理它们之前将 CommonJS 转换为 ES2015。

@rollup/plugin-commonjs 插件就是用来做这件事的。

注意:大多数情况下,@rollup/plugin-commonjs 应该放在转换模块的其他插件之前 - 这是为了防止其他插件对 CommonJS 检测产生影响。一个例外是 Babel 插件,如果你使用它,请将它放在 commonjs 插件之前

对等依赖

假设正在构建一个具有对等依赖项(peer dependency)的库,例如 React 或 Lodash。如果按照上面描述的设置 externals,你的 rollup 将会打包 所有 导入的依赖项:

js
import answer from "the-answer"
import _ from "lodash"

可以设置哪些导入将被打包,哪些将被视为外部导入。在此示例中,将把 lodash 视为外部导入,但不将 the-answer 视为外部导入

JavaScript
// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';

export default {
	input: 'src/main.js',
	output: {
		file: 'bundle.js',
		format: 'cjs'
	},
	plugins: [
		resolve({
			// 将自定义选项传递给解析插件
			moduleDirectories: ['node_modules']
		})
	],
	// 指出哪些模块应该视为外部模块
	external: ['lodash']
};

现在 lodash 将被视为外部导入,不会与你的库捆绑在一起。

external 键接受模块名称的数组或一个函数,该函数接受模块名称并返回 true,如果应将其视为外部导入。例如:

JavaScript
export default {
	// ...
	external: id => /lodash/.test(id)
};

Babel

使用 Babel 和 Rollup 最简单的方法是使用 @rollup/plugin-babel

bash
npm i -D @rollup/plugin-babel @rollup/plugin-node-resolve

修改配置文件:

JavaScript
// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import babel from '@rollup/plugin-babel';

export default {
	input: 'src/main.js',
	output: {
		file: 'bundle.js',
		format: 'cjs'
	},
	plugins: [resolve(), babel({ babelHelpers: 'bundled' })]
};

创建一个名为 src/.babelrc.json 的新文件:

json
{
  "presets": ["@babel/env"]
}

在运行 rollup 之前,需要安装 babel-coreenv 预设:

bash
npm i -D @babel/core @babel/preset-env

运行 Rollup 现在将进行一次打包 。编辑 src/main.js 使用 ES2015 特性

JavaScript
// src/main.js
import answer from 'the-answer';

export default () => {
	console.log(`the answer is ${answer}`);
};

打包后输出:

JavaScript
'use strict';

var index = 42;

var main = function () {
	console.log('the answer is ' + index);
};

module.exports = main;