likes
comments
collection
share

JavaScript 模块规范对比:ESM v.s. CommonJSJavaScript 模块规范对比:ES Modu

作者站长头像
站长
· 阅读数 36

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 的拷贝 -> 一旦输出一个值,模块内部的变化就影响不到这个值

阮一峰 - CommonJS 规范

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

// 动态 import() 表达式
const loadModule = async () => {
	try {
		await import('some-package') // 调用该函数时动态加载 some-package
	} catch (e) {
		console.log(e); // error handling
	}
}

阮一峰 - import() 表达式

Node.js import

Node.js 环境中使用 import 必须带文件后缀;index 文件必须显示声明。

Mandatory file extensions

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

  1. package.json 添加字段 { "type": "module" }
  2. 文件后缀指定为 .mjs \ .mts
  3. tsconfig 指定 compilerOptions.modulemodule

CommonJS

  1. package.json 添加字段 { "type": "commonjs" }
  2. 文件后缀指定为 .cjs \ .cts
  3. tsconfig 指定 compilerOptions.modulecommonjs

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');
})();

阮一峰 - Node.js 如何处理 ES6 模块

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; // ✅

Node.js 文档 - Interoperability with CommonJS

转载自:https://juejin.cn/post/7394790053136416802
评论
请登录