Skip to content

模块化

Node.js 的模块化系统是其核心特性之一,它允许开发者将代码组织成独立的、可复用的模块。每个模块都有自己的作用域,这意味着定义在模块内部的变量和函数不会污染全局命名空间。Node.js 支持两种主要的模块系统:CommonJS 和 ES 模块(ESM)。下面我们将详细介绍这两种模块化方式。

模块查找

  1. 核心模块:如果指定的模块名是内置模块(如 fs, path, http 等),Node.js 会优先尝试加载该核心模块。

  2. 文件模块:如果路径中包含 /, ./, 或 ../,则认为这是对文件或目录的引用。Node.js 将按照特定的顺序查找文件:

    • 如果路径指向一个 .js, .json, 或者 .node 文件,则直接加载它。
    • 如果路径指向一个目录,则 Node.js 会在该目录下查找 package.json 文件,并读取其中的 "main" 字段以确定入口文件;如果没有找到 package.json 或者 "main" 字段不存在,则默认查找名为 index.js 的文件。
  3. 节点模块(Node Modules):如果上述两种情况都不匹配,则 Node.js 会从当前文件所在目录开始,逐层向上查找 node_modules 目录,直到找到同名的模块。查找路径遵循如下模式:

    • ./node_modules
    • ../node_modules
    • ../../node_modules
    • 以此类推,直到根目录
  4. 全局安装的模块:如果你的环境变量设置了 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 来导出值:

javascript
// myModule.js
function greet(name) {
  return `Hello, ${name}!`;
}

module.exports = greet; // 导出函数

或者使用对象的方式导出多个成员:

javascript
// myModule.js
exports.greet = function (name) {
  return `Hello, ${name}!`;
};

exports.add = function (a, b) {
  return a + b;
};

引入模块

使用 require() 函数来引入其他模块:

javascript
// app.js
const greet = require("./myModule"); // 引入单个成员
console.log(greet("World")); // 输出: Hello, World!

// 或者
const { greet, add } = require("./myModule"); // 解构赋值引入多个成员
console.log(add(2, 3)); // 输出: 5

ES 模块 (ESM)

随着 ECMAScript 标准的发展,ES 模块成为了现代 JavaScript 的一部分,并且从 Node.js v12 开始得到了更好的支持。ESM 提供了更直观的语法来定义和导入模块。

定义模块

使用 export 关键字来导出值:

javascript
// myModule.mjs 或者在 package.json 中设置 "type": "module"
export function greet(name) {
  return `Hello, ${name}!`;
}

export function add(a, b) {
  return a + b;
}

你也可以使用默认导出:

javascript
export default function () {
  return "Default export";
}

引入模块

使用 import 语句来引入模块中的成员:

javascript
// 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 也是完全可以的。

Released under the MIT License.