模块化
Node.js 的模块化系统是其核心特性之一,它允许开发者将代码组织成独立的、可复用的模块。每个模块都有自己的作用域,这意味着定义在模块内部的变量和函数不会污染全局命名空间。Node.js 支持两种主要的模块系统:CommonJS 和 ES 模块(ESM)。下面我们将详细介绍这两种模块化方式。
模块查找
核心模块:如果指定的模块名是内置模块(如
fs,path,http等),Node.js 会优先尝试加载该核心模块。文件模块:如果路径中包含
/,./, 或../,则认为这是对文件或目录的引用。Node.js 将按照特定的顺序查找文件:- 如果路径指向一个
.js,.json, 或者.node文件,则直接加载它。 - 如果路径指向一个目录,则 Node.js 会在该目录下查找
package.json文件,并读取其中的"main"字段以确定入口文件;如果没有找到package.json或者"main"字段不存在,则默认查找名为index.js的文件。
- 如果路径指向一个
节点模块(Node Modules):如果上述两种情况都不匹配,则 Node.js 会从当前文件所在目录开始,逐层向上查找
node_modules目录,直到找到同名的模块。查找路径遵循如下模式:./node_modules../node_modules../../node_modules- 以此类推,直到根目录
全局安装的模块:如果你的环境变量设置了
NODE_PATH,那么 Node.js 还会在这些路径中查找模块。此外,在某些情况下,全局安装的模块也可以被加载(但这不是推荐的做法)。
路径解析算法
对于每个给定的模块请求,Node.js 使用以下算法来确定确切的文件路径:
- 绝对路径:如果路径是以斜杠开头(例如
/home/user/project/mymodule.js),那么就直接使用这个路径。 - 相对路径:如果路径是以
./或../开头,那么它是相对于调用require()的那个文件的位置来解析的。 - 裸模块标识符:如果既没有斜杠也没有点(例如
express),那么它被认为是裸模块标识符,Node.js 会按照上面提到的规则在node_modules中查找。 - 模块标识符:如果路径不是以上任何一种情况,那么它是一个模块标识符,Node.js 会在
node_modules目录中查找该模块。 - package.json:如果路径指向一个目录,Node.js 会在该目录下查找
package.json文件,并读取其中的"main"字段以确定入口文件。
缓存行为
Node.js 在第一次加载模块时会将其缓存起来。这意味着如果你多次 require() 同一个模块,实际上只会执行一次初始化代码,并且后续的调用会返回缓存的对象。这有助于提高性能,但也需要注意避免意外的状态共享。
module 对象
记录当前模块的信息,如模块的加载路径、导出的成员等。
require
Node.js 的 require() 函数用于加载模块。它接收一个参数,即模块的标识符或路径。
CommonJS
CommonJS 是 Node.js 早期采用的模块化标准,主要用于服务器端 JavaScript。它基于文件的概念,每个文件被视为一个独立的模块。
定义模块
你可以通过 module.exports 或者简化的 exports 来导出值:
// myModule.js
function greet(name) {
return `Hello, ${name}!`;
}
module.exports = greet; // 导出函数或者使用对象的方式导出多个成员:
// myModule.js
exports.greet = function (name) {
return `Hello, ${name}!`;
};
exports.add = function (a, b) {
return a + b;
};引入模块
使用 require() 函数来引入其他模块:
// app.js
const greet = require("./myModule"); // 引入单个成员
console.log(greet("World")); // 输出: Hello, World!
// 或者
const { greet, add } = require("./myModule"); // 解构赋值引入多个成员
console.log(add(2, 3)); // 输出: 5ES 模块 (ESM)
随着 ECMAScript 标准的发展,ES 模块成为了现代 JavaScript 的一部分,并且从 Node.js v12 开始得到了更好的支持。ESM 提供了更直观的语法来定义和导入模块。
定义模块
使用 export 关键字来导出值:
// myModule.mjs 或者在 package.json 中设置 "type": "module"
export function greet(name) {
return `Hello, ${name}!`;
}
export function add(a, b) {
return a + b;
}你也可以使用默认导出:
export default function () {
return "Default export";
}引入模块
使用 import 语句来引入模块中的成员:
// app.mjs 或者在 package.json 中设置 "type": "module"
import { greet, add } from "./myModule.js";
console.log(greet("ES Modules")); // 输出: Hello, ES Modules!
console.log(add(4, 5)); // 输出: 9
// 引入默认导出
import defaultExport from "./myModule.js";
console.log(defaultExport()); // 输出: Default export注意事项
- 文件扩展名:对于 ESM,通常需要使用
.mjs扩展名,除非你在项目的package.json文件中设置了"type": "module"。 - 互操作性:虽然 CommonJS 和 ESM 可以一起工作,但它们之间存在一些差异。例如,ESM 默认是严格模式,并且不能动态加载模块。
- 性能:ESM 在解析时会进行静态分析,这可能有助于优化和工具链的支持。
- 运行: Node.js 默认支持 ESM,但需要使用
--experimental-modules标志来启用。 - 绝对路径:ESM 模块支持绝对路径引入,不支持以目录方式引入,也不能省略后缀名。
总结
Node.js 的模块化系统使得代码更加结构化和易于维护。选择 CommonJS 还是 ESM 主要取决于你的项目需求和个人偏好。如果你是从零开始的新项目,推荐使用 ESM,因为它遵循最新的 JavaScript 标准并且具有更好的特性和工具支持。如果你正在维护一个旧项目,那么继续使用 CommonJS 也是完全可以的。