Node.js 模块系统详解:从基础使用到加载机制源码解析

作者:岩茶

时间:2026-10-23

浏览:220

本文深入解析 Node.js 模块系统,涵盖模块的基本概念、创建与导出方式(exports 与 module.exports),以及内置模块与文件模块的加载策略。重点剖析 require 方法的文件查找优先级、路径解析算法及核心加载函数,帮助开发者理解模块加载的内部逻辑。

文章目录

Node.js 模块系统概述

为了实现 Node.js 中文件间的相互调用,Node.js 内置了一套简洁的模块系统。

模块是 Node.js 应用的核心构成单元,每个文件与一个模块严格对应。这意味着,无论是 JavaScript 代码、JSON 数据文件,还是编译后的 C/C++ 扩展文件,在 Node.js 中均被视为一个独立的模块。

模块的创建与导出

创建模块

在 Node.js 中创建模块十分便捷。以下示例展示了如何创建一个名为 main.js 的文件,其核心代码如下:


var hello = require('./hello');
hello.world();

上述代码中,require('./hello') 语句用于引入当前目录下的 hello.js 文件。其中 './' 代表当前目录,Node.js 默认文件后缀为 .js。

Node.js 提供了 exports 和 require 两个关键对象。exports 用于定义模块对外公开的接口,而 require 则用于从外部引入模块,即获取该模块的 exports 对象。

接下来我们创建 hello.js 文件,具体代码如下:


exports.world = function() {
 console.log('Hello World');
}

在示例中,hello.js 通过 exports 对象暴露 world 函数作为模块访问接口。在 main.js 中,通过 require('./hello') 加载该模块后,即可直接调用 hello.js 中 exports 对象所挂载的成员函数。

有时我们需要将整个对象封装为模块接口,其格式如下:


module.exports = function() {
 // ...
}

例如:


//hello.js 
function Hello() { 
  var name; 
  this.setName = function(thyName) { 
    name = thyName; 
  }; 
  this.sayHello = function() { 
    console.log('Hello ' + name); 
  }; 
}; 
module.exports = Hello;

此时可直接获取该对象:


//main.js 
var Hello = require('./hello'); 
hello = new Hello(); 
hello.setName('BYVoid'); 
hello.sayHello(); 

模块接口的关键变化在于使用 module.exports = Hello 替代了 exports.world = function(){}。在外部引用该模块时,其接口对象即为输出的 Hello 对象本身,而非默认的 exports 对象。

服务端模块的存放与引用

服务端模块的存放位置

你可能已在代码中使用了模块,例如:


var http = require("http");

...

http.createServer(...);

Node.js 内置了一个名为 http 的模块。我们在代码中通过 require 请求该模块,并将返回值赋给本地变量。

这使得本地变量成为一个包含 http 模块所有公共方法的对象。

模块加载机制与优先级

Node.js 的 require 方法在解析文件时遵循特定的查找策略:

由于 Node.js 包含 4 类模块(原生模块和 3 种文件模块),尽管 require 方法接口简单,但其内部加载逻辑复杂,且各类模块的加载优先级各不相同。具体流程如下图所示:

Node.js 模块系统概述

从文件模块缓存中加载

尽管原生模块与文件模块的优先级存在差异,但两者均会优先从文件模块的缓存中加载已存在的模块。

从原生模块加载

原生模块的优先级仅次于文件模块缓存。在 require 方法解析文件名后,会优先检查该模块是否位于原生模块列表中。以 http 模块为例,即使当前目录下存在 http/http.js、http.node 或 http.json 文件,require("http") 也不会加载这些文件,而是直接从原生模块中加载。

原生模块同样拥有缓存区,优先从缓存中加载。若缓存中未加载过,则调用原生模块的加载机制进行加载和执行。

从文件加载

当文件模块缓存中不存在目标,且非原生模块时,Node.js 会解析 require 传入的参数,并从文件系统中加载实际文件。加载过程中的包装和编译细节前文已介绍,此处重点详述文件模块的查找过程,其中包含若干值得注意的细节。

require 参数类型

require 方法接受以下几种参数传递:

  • http、fs、path 等,属于原生模块。
  • ./mod 或 ../mod,表示相对路径的文件模块。
  • /pathtomodule/mod,表示绝对路径的文件模块。
  • mod,非原生模块的文件模块。

模块查找算法详解

在路径 Y 下执行 require(X) 语句的执行顺序如下:

1. 如果 X 是内置模块 a. 返回内置模块 b. 停止执行 2. 如果 X 以 '/' 开头 a. 设置 Y 为文件根路径 3. 如果 X 以 './' 或 '/' or '../' 开头 a. LOAD_AS_FILE(Y + X) b. LOAD_AS_DIRECTORY(Y + X) 4. LOAD_NODE_MODULES(X, dirname(Y)) 5. 抛出异常 "not found"

LOAD_AS_FILE(X) 1. 如果 X 是一个文件, 将 X 作为 JavaScript 文本载入并停止执行。 2. 如果 X.js 是一个文件, 将 X.js 作为 JavaScript 文本载入并停止执行。 3. 如果 X.json 是一个文件, 解析 X.json 为 JavaScript 对象并停止执行。 4. 如果 X.node 是一个文件, 将 X.node 作为二进制插件载入并停止执行。

LOAD_INDEX(X) 1. 如果 X/index.js 是一个文件, 将 X/index.js 作为 JavaScript 文本载入并停止执行。 2. 如果 X/index.json 是一个文件, 解析 X/index.json 为 JavaScript 对象并停止执行。 3. 如果 X/index.node 是一个文件, 将 X/index.node 作为二进制插件载入并停止执行。

LOAD_AS_DIRECTORY(X) 1. 如果 X/package.json 是一个文件, a. 解析 X/package.json, 并查找 "main" 字段。 b. let M = X + (json main 字段) c. LOAD_AS_FILE(M) d. LOAD_INDEX(M) 2. LOAD_INDEX(X)

LOAD_NODE_MODULES(X, START) 1. let DIRS=NODE_MODULES_PATHS(START) 2. for each DIR in DIRS: a. LOAD_AS_FILE(DIR/X) b. LOAD_AS_DIRECTORY(DIR/X)

NODE_MODULES_PATHS(START) 1. let PARTS = path split(START) 2. let I = count of PARTS - 1 3. let DIRS = [] 4. while I >= 0, a. if PARTS[I] = "node_modules" CONTINUE b. DIR = path join(PARTS[0 .. I] + "node_modules") c. DIRS = DIRS + DIR d. let I = I - 1 5. return DIRS

以上为 Node.js 模块系统的详细解析,更多关于 Node.js 模块系统的资料请关注本站其他相关文章!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章