每日一报
esbuild 的一些考虑
为什么 esbuild 转译 commonjs 模块为 esm 模块时,均使用 default
导出
举一个例子,如果转译以下 commonjs
模块:
const obj = {};
const random = Math.floor(Math.random() * 10) + 1;
for (let i = 0; i <= random; i++) {
obj[i] = Math.floor(Math.random() * 10) + 2;
}
module.exports = obj;
esbuild
转译后的结果如下:
var __getOwnPropNames = Object.getOwnPropertyNames;
var __commonJS = (cb, mod) =>
function __require() {
return (
mod ||
(0, cb[__getOwnPropNames(cb)[0]])(
(mod = { exports: {} }).exports,
mod
),
mod.exports
);
};
// node_modules/demo/index.js
var require_demo = __commonJS({
'node_modules/demo/index.js'(exports, module) {
var obj = {};
var random = Math.floor(Math.random() * 10) + 1;
for (let i = 0; i <= random; i++) {
obj[i] = Math.floor(Math.random() * 10) + 2;
}
module.exports = obj;
}
});
export default require_demo();
经过多次测试可以发现 esbuild
转译的 commonjs
模块为 esm
模块时,均以 default
做为导出方式。
为什么 esbuild
会这么做?
本质上的原因是 esbuild
将 commonjs
作为一等公民。esbuild
转译 commonjs
为 esm
的时候是采用与 nodejs
类似的 运行时加载方式,并没有对 commonjs
模块进行静态分析。换句话说 esbuild
并不知道 commonjs
模块具体导出了哪些引用,也就是无法采用具名导出的方式交付给 引用方(importer
)。
例子
const obj = {};
const random = Math.floor(Math.random() * 10) + 1;
for (let i = 0; i <= random; i++) {
obj[i] = Math.floor(Math.random() * 10) + 2;
}
for (const key in obj) {
// bad case
export key = obj[key];
}
你不可能在 非运行时 阶段确切了解到 obj
对象具体包含了哪些引用,那么就不可能通过 具名导出 的方式处理 obj
对象中的每一个属性,也就是说上述的 export key = obj[key]
语句是错误的,是运行时的方案,不符合 export
语句静态的特性。
在 ESM
中,export
语句必须是静态的。这意味着所有的导出(无论是 命名导出 还是 默认导出)都必须在模块 非运行时 阶段就可以明确知道哪些引用需要导出。像上述例子中的 obj
属性具有不确定性,但 obj
引用对象是确定的,因此可以像下述例子中一样通过导出具有确定性的引用(上述中即为 obj
)对象。至于 obj
对象中的属性,那么在 运行时 阶段可以得知,这样既确保了 export
语句的 静态特性 同时又维护了 commonjs
模块的 高度动态性。
const obj = {};
// ... do obj something in runtime
obj.a = 1;
obj.b = 2;
export { obj };
export default obj;
至此已经介绍了针对运行时的属性在静态分析需要如何处理导出,即通过引用对象的方式导出(具名导出或默认导出)。由于 commonjs
自身并没有所谓的具名导出,因此 esbuild
会统一将 commonjs
模块的导出处理为 默认导出。这也就是我们所看到的 esbuild
在 commonjs
的转译产物(esm
)中的最底部均会使用 export default
语句。
var require_demo = __commonJS({
'node_modules/demo/index.js'(exports, module) {
var obj = {};
var random = Math.floor(Math.random() * 10) + 1;
for (let i = 0; i <= random; i++) {
obj[i] = Math.floor(Math.random() * 10) + 2;
}
module.exports = obj;
}
});
export default require_demo();
默认导出的 引用 是确定的,属性是运行时加载确认的。解释完了 esbuild
为什么在转译 commonjs
模块为 esm
模块时,均使用 default
导出。那么上述提到的一个观点。
esbuild
转译commonjs
为esm
的时候是采用与Node
类似的运行时加载
方式。
可能熟悉 rollup
的处理 commonjs
模块的方案可以知道,rollup
把 esm
作为一等公民。也就是说遇到任何非 esm
模块时,rollup
都会要求提供插件来转译模块为 esm
模块。
针对 commonjs
模块的转译,rollup
会通过 @rollup/plugin-commonjs
插件来处理 commonjs
模块,将 commonjs
模块转译为 esm
模块。
那么问题来了:
@rollup/plugin-commonjs
转译 commonjs
模块的行为与 esbuild
转译 commonjs
模块的行为是否会有所不同呢?
从 default strict requires to true
文章中可以知道。@rollup/plugin-commonjs
在先前的版本中(strictRequires = false
| auto
)中会采用静态分析的方式尽可能将 commonjs
转译为具有静态性质的 esm
模块。但是由于 commonjs
模块具有高度动态的特性,将其转化为高度静态化的 esm
模块势必会存在一些边界问题。
文章 最后提出将 strictRequires = true
作为默认配置项。也就是说 @rollup/plugin-commonjs
会将所有的 commonjs
模块都包装在函数中,执行类似 nodejs
加载 commonjs
模块的方式,在运行时确认 commonjs
模块具体导出了哪些引用。
因此现版本的 @rollup/plugin-commonjs
在 strictRequires = true
的配置下,与 esbuild
在转译 commonjs
模块为 esm
模块的行为类似。