携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第7天,点击查看活动详情
随着尤大带领Vite生态实力的发展,冲击着前端构建方案。目前社区有一部分同时支持ESM与CommonJS,比如我之前搭建的TS构建脚手架。但仍然有许多模块仅支持CommonJS/UMD,因此我认为CommonJS转化为ESM是全部模块ESM化的一个重要过渡阶段。
ESM与CommonJS的区别
无聊的概念普及,尽可能通过演示来辅助理解。
esm 是将 javascript 程序拆分成多个单独模块,并能按需导入的标准。和webpack
,babel
不同的是,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 };
复制代码
一些更棘手的转化
ESM
与 CommonJS
不仅仅是简单的语法上的不同,它们在思维方式上就完全不同,因此还有一一些更棘手的转化。
- 如何处理
__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
的转化工具自然也在其中。以下提供链接,跟我一样闲着没事感兴趣的同学可以一起研究。
- @rollup/plugin-commonjs
- cdn.skypack.dev ,无需安装和构建工具即可加载优化的 npm 包。
- jspm.org/,*JSPM 允许来自 npm 的任何包直接加载到支持ESM浏览器中,而无需进一步的工具。