《高阶前端指北》之深究CommonJS转为ESM过程

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第7天,点击查看活动详情

随着尤大带领Vite生态实力的发展,冲击着前端构建方案。目前社区有一部分同时支持ESM与CommonJS,比如我之前搭建的TS构建脚手架。但仍然有许多模块仅支持CommonJS/UMD,因此我认为CommonJS转化为ESM是全部模块ESM化的一个重要过渡阶段。

ESM与CommonJS的区别

无聊的概念普及,尽可能通过演示来辅助理解。
esm 是将 javascript 程序拆分成多个单独模块,并能按需导入的标准。和webpackbabel不同的是,esm 是 javascript 的标准功能,在浏览器端和 nodejs 中都已得到实现。使用 esm 的好处是浏览器可以最优化加载模块,比使用库更有效率。

ESM中,我们习惯的导入导出方式:
1.按模块名:Named Import/Export 2.默认:Default Import/Export

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

// Default export/import
export default sum;
import sum from "sum";
复制代码

而CommonJS只有一种方法

module.exports = sum;
复制代码

大家看到的exports不过是module.exports的引用而已

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

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

复制代码

一个典型达到面试题,练练手~
问关于 exports 与 module.exports 的区别,以下 console.log 输出什么:

// a.js
exports.a = 3;
module.exports = { b: 4 };

// index.js
const a = require("./a");
console.log(a);
复制代码

因为ESM与CommonJS的区别,在转换时我们要考虑更多的兼容问题。

exports 的转化

上面的问题我们已经知道,当 exports 转化时,既要转化为 export {},又要转化为 export default {}

// Input:  index.cjs
exports.name = '扫地盲僧';

// Output: index.mjs
// 此处既要转化为默认导出,又要转化为具名导出!
export const name = '扫地盲僧';
export default { name };
复制代码

如果仅仅转为 export const name = '扫地盲僧' 的具名导出,而不转换 export default { name },将会出现什么问题呢?我们测试一下:

// 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 这里有问题,这里有问题,这里有问题
复制代码

module.exports 的转化

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

// Input:  index.cjs
module.exports = {
  name: '扫地盲僧',
  age: 18,
};

// Output: index.mjs
// 此处既要转化为默认导出,又要转化为具名导出!
export default {
  name: '扫地盲僧',
  age: 18,
};
export const name = '扫地盲僧';
export const age = 18;
复制代码

如果 module.exports 导出的是函数如何处理呢,特别是 exports 与 module.exports 的程序逻辑混合在一起?

以下是一个正确的转换结果:

// Input: index.cjs
module.exports = () => {}
exports.name = '扫地盲僧'
exports.age = 18

// Output: index.mjs
const fn = () => {}
fn.name = '扫地盲僧'
fn.age = 18
export const name = '扫地盲僧'
export const age = 18
export default = fn
复制代码

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

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

(function (module, exports) {
  module.exports = () => {};
  exports.name = '扫地盲僧';
  exports.age = 18;
})(esm$1, esm$1.exports);

var esm = esm$1.exports;

export { esm as default };
复制代码

一些更棘手的转化

ESMCommonJS 不仅仅是简单的语法上的不同,它们在思维方式上就完全不同,因此还有一一些更棘手的转化。

  • 如何处理 __dirname?
  • 如何处理 require(dynamicString)?
  • 如何处理 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 = _default.sum = 100;
});
export default _default;
export { sum };
复制代码

更深入研究构建转化原理

随着尤大队伍壮大,rollup的生态逐渐完善,基于rollup的转化工具自然也在其中。以下提供链接,跟我一样闲着没事感兴趣的同学可以一起研究。

猜你喜欢

转载自juejin.im/post/7130172855169318948