JavaScript 模块规范对比:ESM v.s. CommonJSJavaScript 模块规范对比:ES Modu
JavaScript 模块规范对比:ESM v.s. CommonJS
CommonJS modules
// 引入
// 1) OK
let { stat, exists, readfile } = require('fs');
const { stat, exists, readfile } = require('fs');
// 2) OK
const __fs = require('fs');
__fs.stat();
// 3) OK
require('fs').stat();
// 导出
// 1)
function add(a, b) { return a + b }
module.exports = { add };
// 2)
module.exports.add = function(a, b) {
return a + b;
}
- CommonJS 长久以来一直是 Node.js 采用的模块规范 -> 兼容性更好
- CommonJS 的
require
语法是同步的,动态的- 同步的
- 动态的:在运行时动态加载的
- 运行时整体加载一整个模块,生成一个对象
- 可以允许路径动态生成
const foo = require(getFooPath(bar));
- 第一次加载某个模块时,Node 会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的
module.exports
属性
require
返回的是该模块的 exports 对象 -> 本质上就是一个 js 对象- ❕CommonJS 加载的是模块 exports 的拷贝 -> 一旦输出一个值,模块内部的变化就影响不到这个值
ESM (ECMAScript modules)
- ESM 规范随着 ES6 标准的普及逐渐成为主流模块规范,正在逐步支持
- 浏览器端:原生支持 ✅;开发 ✅ ;依赖适配 ✅
- 服务器端:
Nodejs v16
之后已经稳定支持 ESM;但是生态中存在大量 CommonJS 依赖 - npm package: 考虑兼容性建议同时支持 ESM 和 CommonJS,或只支持 CommonJS
- ESM 的
import
语法是异步的,静态的- 异步的
- 此时称为
import()
表达式 (import()
expression) - 在函数体可以用
import()
异步加载模块 -> 此时类似于一个异步的require
- 此时称为
- 静态的:作用于编译时的
- 此时称为
import
声明 (import
statement) - 顶级
import
在编译时即能确定依赖关系,可以 Tree Shaking 优化依赖 - 顶级
import
会被自动提升 (Hoisting),优先执行
- 此时称为
- 异步的
- ❕
import
返回的不是一个 js 对象 - ❕ESM 是动态引用 ->
import
在编译阶段生成一个只读引用,运行时动态地去被加载的模块取值,并且不会缓存值
import
声明用法 - 顶级 import
// import 声明
// 1) 具名导出
export const add = (a, b) => a + b;
import { add } from 'package';
// or
import * as Package from 'package'; // 整体加载模块
Package.add();
// 2) 别名
const foo = () => {};
export { exportedFoo as foo };
import { exportedFoo } from 'package';
// 可以在 import 的时候指定 alias
import { exportedFoo as foo } from 'package';
// 3) 默认导出
export default add
import add from 'package'; // OK
import exportedAdd from 'package'; // OK
// 4) export 与 import 的复合写法
export { foo, bar } from 'package';
export * from 'package';
// 5) 副作用导入
// 不会导入任何东西但是会执行模块的全局代码,相当于运行一段脚本
import "/modules/my-module.js";
import()
表达式用法 - 动态 import
// 动态 import() 表达式
const loadModule = async () => {
try {
await import('some-package') // 调用该函数时动态加载 some-package
} catch (e) {
console.log(e); // error handling
}
}
Node.js import
Node.js 环境中使用 import
必须带文件后缀;index
文件必须显示声明。
A file extension must be provided when using the
import
keyword to resolve relative or absolute specifiers. Directory indexes (e.g.'./startup/index.js'
) must also be fully specified.当使用' import '关键字解析相对或绝对说明符时,必须提供文件扩展名。目录中的 index 文件(例如: " ./startup/index.js' ')也必须指明。
Node.js 还支持导入 file:
, node:
和 data:
URL schema,不过不太常用。详见 Node.js 文档 - ESM URLs。
指定规范
ESM
package.json
添加字段{ "type": "module" }
- 文件后缀指定为
.mjs
\.mts
tsconfig
指定compilerOptions.module
为module
CommonJS
package.json
添加字段{ "type": "commonjs" }
- 文件后缀指定为
.cjs
\.cts
tsconfig
指定compilerOptions.module
为commonjs
ES Module 和 CommonJS Module 的相互引入
CommonJS module 中引入 ES module
async IIFE + import
CommonJS module 在一般情况下不能直接使用 require
引入 ESM,可以通过异步立即执行函数 (async IIFE) + import 表达式
// 在 foo.js 中引入 bar.mjs
let barModule;
(async () => {
barModule = await import('./bar.mjs');
})();
npm package - esm
借助第三方库 esm 也可以实现 require
引入 ESM
const barModule = require('esm')('./bar.mjs'/*, options*/);
实验性 flag - --experimental-require-module
2024 年 5 月 Node.js 最新版加入了一个新的实验性 flag - --experimental-require-module
,在开启 flag 的情况下支持调用 require
直接引入一个 ESM
// 1) 默认导出 foo.mjs
const foo = () => {};
export default foo;
// 需要从 default 属性上取得默认导出
const { default: foo } = require('./foo.mjs');
foo(); // OK
// 2) 具名导出 foo.mjs
export const foo = () => {};
// 可以直接解构
const { foo } = require('./foo.mjs');
foo(); // OK
# Run with flag
node --experimental-require-module index.js
注意 -
require
只能同步引用模块!通过这种方式引入 ESM 需要保证这个模块以及它的依赖中不包含顶级
await
,否则会抛出错误ERR_REQUIRE_ASYNC_MODULE
。
Node.js 文档 - Loading ECMAScript modules using require()
Node.js Adds Sync ESM Support: A Milestone for Developers - NodeSource
ES module 中引入 CommonJS module
ESM 的 import 对于 CommonJS module 有着较好的兼容性支持。CommonJS module 中的 module.exports
对象会被映射到 default
属性上。
// foo.cjs
module.exports = {
foo: () => 'foo.cjs';
}
// 以下两种导入方式效果相同,相当于默认导出
import { default as fooCjs } from './foo.cjs';
// or
import fooCjs from './foo.cjs';
fooCjs.foo(); // OK
// 以下两种导入方式效果相同
import * as fooCjsAll from './foo.cjs'; // 命名空间导入
// or
const fooCjsAll = await import('./foo.cjs'); // 动态 import 表达式
fooCjsAll.default.foo(); // OK
注意 - import CommonJS module 不能解构
CommonJS module 不能被静态解析只能整体加载
import { foo } from './foo.cjs'; // ❌ import fooCjs from './foo.cjs'; const { foo } = fooCjs; // ✅
转载自:https://juejin.cn/post/7394790053136416802