Node.js 模块化规范详解
在 Node.js 中,模块化是开发应用程序的核心概念,它使得代码可以按照功能模块进行分割,易于维护、复用和扩展。Node.js 支持两种模块化规范:
CommonJS(CJS):这是 Node.js 最初使用的模块化规范。
ECMAScript Modules(ESM):这是现代 JavaScript 的官方模块化规范,自 ECMAScript 2015(ES6)引入。
1. CommonJS 模块化规范
CommonJS 是 Node.js 早期就支持的模块化标准,允许在服务端使用模块。在 CommonJS 中,每个文件都被视为一个独立的模块。你可以通过 module.exports 导出模块内容,并使用 require() 函数引入模块。
1.1. CommonJS 基本用法
导出模块:使用 module.exports 或 exports。
引入模块:使用 require()。
定义模块:
// math.js - 定义模块
function add(a, b) {return a + b;
}function subtract(a, b) {return a - b;
}// 使用 module.exports 导出模块
module.exports = {add,subtract,
};// 或者也可以使用 exports(两者作用相同)
exports.add = add;
exports.subtract = subtract;
引入模块:
// main.js - 引入并使用模块
const math = require('./math');console.log(math.add(5, 3)); // 输出:8
console.log(math.subtract(5, 3)); // 输出:2
1.2. CommonJS 特点
同步加载:require() 是同步的,这意味着模块会在需要时同步加载。这在服务端是可以接受的,但在浏览器中不够高效。
模块缓存:加载的模块会被缓存,因此多次 require() 同一个模块时,模块只会被加载一次。
2. ECMAScript Modules 规范
随着 JavaScript 语言的发展,ECMAScript Modules(ESM)被引入,成为 JavaScript 官方标准的模块系统。Node.js 从版本 12.17 开始支持 ESM,Node.js 通过引入 .mjs 文件扩展名和 package.json 中的 "type": "module" 字段来实现对 ESM 的支持。
2.1. ECMAScript Modules 基本用法
导出模块:使用 export 关键字。
引入模块:使用 import 关键字。
定义模块:
// math.mjs - 定义模块
export function add(a, b) {return a + b;
}export function subtract(a, b) {return a - b;
}
导入模块:
// main.mjs - 引入并使用模块
import { add, subtract } from './math.mjs';console.log(add(5, 3)); // 输出:8
console.log(subtract(5, 3)); // 输出:2
2.2. ESM 特点
静态解析:ESM 是静态的,这意味着在代码编译时就能确定模块的依赖关系(相较于 CommonJS 的动态加载)。
异步加载:import 支持异步加载,这在浏览器中更高效,特别是当你需要懒加载模块时。
严格模式:所有的 ECMAScript 模块都默认处于严格模式("use strict")。
不允许动态导入和导出:ESM 不支持像 CommonJS 那样的动态 require,必须在顶层进行 import/export。
3. 文件后缀与模块加载规则
3.1. CommonJS 文件后缀
对于 CommonJS 模块,文件通常使用 .js 后缀。如果你在 Node.js 中编写 CommonJS 模块,直接使用 .js 文件即可。
3.2. ECMAScript 模块的文件后缀
在 Node.js 中,可以通过以下两种方式启用 ESM:
使用 .mjs 文件后缀:如果文件扩展名是 .mjs,Node.js 会将其视为 ESM。
配置 package.json:在项目的 package.json 中设置 "type": "module",这样即使文件是 .js 后缀,Node.js 也会将其作为 ESM 处理。
3.3. 如何区分 CommonJS 和 ESM
Node.js 根据以下规则来确定模块系统:
CommonJS:默认情况下,Node.js 中所有 .js 文件都被视为 CommonJS 模块,除非另有指定。
ESM:当你使用 .mjs 扩展名,或者在 package.json 中指定 "type": "module",所有的 .js 文件都会被视为 ESM 模块。
4. require 和 import 互操作性
尽管 Node.js 支持 CommonJS 和 ESM 两种规范,但两者在一起使用时需要注意一些限制:
4.1. 从 CommonJS 中引入 ESM
从 CommonJS 文件中引入 ESM 模块是有一定限制的。require() 无法直接加载 ESM 模块,必须使用 import() 函数(它是一个异步函数)。
// 在 CommonJS 模块中动态引入 ESM 模块
(async () => {const math = await import('./math.mjs');console.log(math.add(5, 3));
})();
4.2. 从 ESM 中引入 CommonJS
ESM 模块可以直接通过 import 引入 CommonJS 模块,因为 Node.js 会将 CommonJS 模块包装成 ESM 格式以供使用。
// 在 ESM 中引入 CommonJS 模块
import math from './math.js'; // 假设 math.js 是一个 CommonJS 模块
console.log(math.add(5, 3));
5. 选择 CommonJS 还是 ESM?
在实际开发中,选择使用 CommonJS 还是 ESM 取决于几个因素:
兼容性:如果你要支持旧的 Node.js 版本(12 之前)或使用大量依赖的第三方库(特别是历史遗留的),CommonJS 可能更合适。
未来发展:ESM 是 JavaScript 官方标准,未来会有更多的支持,因此如果是新项目,推荐使用 ESM。
生态系统:目前 Node.js 生态中的大多数库仍然使用 CommonJS,但越来越多的库开始迁移到 ESM。