kangyana / daily-question

When your heart is set on something, you get closer to your goal with each passing day.
https://www.webpack.top
MIT License
3 stars 0 forks source link

【Q118】如何将 CommonJS 转化为 ESM #118

Open kangyana opened 1 year ago

kangyana commented 1 year ago

由于 Bundless 构建工具的兴起,要求所有的模块都是 ESM 模块化格式。

目前社区有一部分模块同时支持 ESM 与 CommonJS,但仍有许多模块仅支持 CommonJS/UMD,因此将 CommonJS 转化为 ESM 是全部模块 ESM 化的过渡阶段。

1. ESM 与 CommonJS 的导入导出的不同

在 ESM 中,导入导出有两种方式:

代码示例如下:

// Named export/import
export { sum };
import { sum } from "sum";

// Default export/import
export default sum;
import sum from "sum";

而在 CommonJS 中,导入导出的方法只有一种:

// 实际上的 exports
exports = module.exports;

// 以下两个是等价的
exports.a = 3;
module.exports.a = 3;

正因为有二者的不同,因此在二者转换的时候有一些兼容问题需要解决。

2. exports 的转化

正因为,二者有所不同,当 exports 转化时,既要转化为 export {},又要转化为 export default {}

// Input:  index.cjs
exports.a = 3;

// Output: index.mjs
// 此处既要转化为默认导出,又要转化为具名导出!
export const a = 3;
export default { a };

如果仅仅转为 export const a = 3 的具名导出,而不转换 export default { a },将会出现什么问题?以下为例:

// Input: CJS
exports.a = 3; // index.cjs

const o = require("."); // foo.cjs
console.log(o.a); // foo.cjs

// Output: ESM
// 这是有问题的错误转换示例:
// 此处 a 应该再 export default { a } 一次
export const a = 3; // index.mjs

import o from "."; // foo.mjs
console.log(o.a); // foo.mjs 这里有问题,这里有问题,这里有问题

3. module.exports 的转化

对于 module.exports,我们可以遍历其中的 key (通过 AST),将 key 转化为 Named Export,将 module.exports 转化为 Default Export

// Input:  index.cjs
module.exports = {
  a: 3,
  b: 4,
};

// Output: index.mjs
// 此处既要转化为默认导出,又要转化为具名导出!
export default {
  a: 3,
  b: 4,
};
export const a = 3;
export const b = 4;

如果 module.exports 导出的是函数如何处理呢,特别是 exportsmodule.exports 的程序逻辑混合在一起? 以下是一个正确的转换结果:

// Input: index.cjs
module.exports = () => {}
exports.a = 3
exports.b = 4

// Output: index.mjs
const sum = () => {}
sum.a = 3
sum.b = 4
export const a = 3
export const b = 4
export default = sum

也可以这么处理,将 module.exportsexports 的代码使用函数包裹起来,此时我们无需关心其中的逻辑细节。

var esm$1 = { exports: {} };

(function (module, exports) {
  module.exports = () => {};
  exports.a = 3;
  exports.b = 4;
})(esm$1, esm$1.exports);

var esm = esm$1.exports;

export { esm as default };

4. 一些复杂的转化

ESM 与 CommonJS 不仅仅是简单的语法上的不同,它们在思维方式上就完全不同,因此还有一些较为复杂的转换。

以下代码涉及到编程逻辑,由于 exports 是一个动态的 Javascript 对象,而它自然可以使用两次,那应该如何正确编译为 ESM 呢?

// input: index.cjs
exports.sum = 0;
Promise.resolve().then(() => {
  exports.sum = 100;
});

以下是一种不会出问题的代码转换结果:

// output: index.mjs
const _default = {};
let sum = (_default.sum = 0);
Promise.resolve().then(() => {
  sum CommonJS 向 ESM 转化,自然有构建工具的参与,比如= _default.sum = 100;
});
export default _default;
export { sum };

5. CommonJS 向 ESM 的构建工具