Node.js:module(模块)

在 CommonJS 里,每个文件都是独立模块。一部分模块编译进 Node 二进制,叫 核心模块,源码在 lib/require('http') 永远拿到内置 HTTP,即使当前目录有同名文件。现行也可用 require('node:http'),带 node: 前缀就不会和 node_modules 里的包撞名。

ESM("type": "module".mjs)用 importexport,解析规则另看 package.jsonexports,不要把下面的 require 算法原样套上去。

访问主模块

CommonJS 的 module.filename 通常等于 __filename。用 node file.js 直接跑某个文件时,require.main 指向那个文件的 module

因此可用 require.main === module 判断「当前文件是不是入口」:

console.log(require.main.filename);
console.log(require.main === module);

ESM 没有 require.main,用 import.meta.url 和进程入口比较,或看 process.argv[1]

缓存

模块第一次加载后按 解析得到的文件名 放进 require.cache。同一路径再 require 得到同一个对象,不会重新执行文件。循环依赖就靠这个:后一次 require 拿到尚未赋完的 exports

若要多次执行,应导出函数,由调用方反复调用。

注意:

  • 从不同目录的 node_modules 解析,会落到不同文件,require('foo') 就不保证是同一个对象。
  • 在不区分大小写的磁盘上,Foo.jsfoo.js 可以指向同一文件,缓存仍当成两个键,会加载两次。

循环

A.jsrequire B.jsB.js 里再 require A.js 时:为避免无限递归,返回给 B 的是 尚未完成A.exportsB 加载完后,再把完整的 B.exports 交给 A。不会死循环,此时读到的是半成品。

// a.js
console.log('a');
exports.str = 'a1';
const b = require('./b.js');
console.log('a:', b.str);
exports.str = 'a2';
console.log('a:结束');
// b.js
console.log('b');
exports.str = 'b1';
const a = require('./a.js');
console.log('b:', a.str);
exports.str = 'b2';
console.log('b:结束');
// main.js
console.log('main');
const a = require('./a.js');
const b = require('./b.js');
console.log('main:', a.str, b.str);

输出:

main
a
b
b: a1
b:结束
a: b2
a:结束
main: a2 b2

b 打印 a.str 时还是 'a1';等 b 结束,a 看到的已是 'b2'main 里两者都是最终值。ESM 的 import 是实时绑定,行为不同,见同系列 npm 文里的循环依赖一节。

模块加载

文件模块

按确切文件名找不到时,会依次试 .js.json.node(还有 .cjs 等现行扩展,视条件而定)。

  • .js:按 JavaScript 执行(包内 "type": "module".js 是 ESM)
  • .json:解析为 JSON
  • .nodedlopen 加载的原生插件
  • / 开头:绝对路径
  • ./../ 开头:相对当前模块
  • 不以这些开头:核心模块,或从 node_modules

找不到则 requireErrorcode'MODULE_NOT_FOUND'。ESM 的 import 失败常见 ERR_MODULE_NOT_FOUND

目录作为模块

目录可以当一个包,靠入口文件对外。

若该目录有 package.json 且写了 main

{
  "name": "some-library",
  "main": "./lib/some-library.js"
}

require('./some-library') 会加载 ./some-library/lib/some-library.js。现行包还应写 exports,Node 解析入口时 exports 优先于 main,未列出的路径默认不能从包外 require

main(或 exports)指向的文件不存在,报 Cannot find module 'some-library'

没有 package.json 时,再试目录下的 index.jsindex.jsonindex.node

从 node_modules 目录加载

标识符既不是核心模块、也不是路径时,从当前文件所在目录开始,在每一层的 node_modules 里找。已经以 node_modules 结尾的路径不会再拼一层 node_modules。找不到就上移,直到盘符根。

例如 /home/ry/projects/foo.jsrequire('bar.js')

/home/ry/projects/node_modules/bar.js
/home/ry/node_modules/bar.js
/home/node_modules/bar.js
/node_modules/bar.js

日常包名不带 .jsrequire('bar')),解析的是 node_modules/bar 目录或文件。

从全局目录加载

NODE_PATH 是绝对路径列表,Unix 用冒号分隔,Windows 用分号。别处找不到时会再搜这些路径。

这是早期设计,现在依赖应放在项目 node_modules。靠 NODE_PATH 的部署容易在不知情时加载到另一份版本。

此外还会搜(历史兼容,不要当正规依赖位置):

$HOME/.node_modules
$HOME/.node_libraries
$PREFIX/lib/node

$HOME 是用户主目录,$PREFIX 是 Node 配置的 node_prefix。本地 node_modules 更快、也更可预期。

模块作用域

下列名字只在 当前 CommonJS 文件 里有,不是 global 上的属性。它们是模块包装函数的五个形参,机制见同系列 global(全局变量)。ESM 没有 __dirname__filenameexportsmodulerequire

__dirname

当前模块所在目录。等于 path.dirname(__filename)。ESM 没有这个名字,用 import.meta.dirname(Node 20.11+)或从 import.meta.url 自己算,写法见 global(全局变量)

__filename

当前模块文件的绝对路径(符号链接会先解析)。入口脚本的这个值也不等于你在命令行里敲的那个相对名:node main.js 再加载 a.js 时,a.js 里打印的是 a.js 自己的路径。

console.log(__filename);

exports

模块执行前被设成与 module.exports 同一个引用,所以 exports.f = ... 等于 module.exports.f = ...。给 exports 换新对象会断开绑定:

module.exports.hello = true; // 会导出
exports = { hello: false };  // 只在本文件有效

整体替换导出时两边一起赋值:

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

module

指向当前模块对象,不是全局变量。

require()

加载模块。

require.cache

已加载模块的缓存。删掉某个键,下次 require 会重新执行该文件。

不能靠删缓存重载原生 addon,再加载会出错。

require.resolve(request[, options])

只解析路径,不执行模块。

  • request:要解析的标识符
  • options.paths:搜索起点数组;提供时不再用默认起点,每一项都会当作算法起点去找 node_modules

require.resolve.paths(request)

返回解析过程中查过的路径数组。若 request 是核心模块(httpfs 等),返回 null

module 对象

每个 CommonJS 文件里的 module 指向「当前这份模块」。require() 返回的是 module.exports

module.children

本模块 require 过的模块对象。

module.exports

模块系统创建的导出对象。给别的文件用,就把值赋给它。

// c.js
module.exports = {
  a: 1,
  b: 2
};
// main.js
const ObjC = require('./c.js');
console.log(ObjC); // { a: 1, b: 2 }

只给 exports 换对象导出不了:

// 有效
exports.a = 1;
exports.b = 2;

// 无效,外面拿到 {}
exports = {
  a: 1,
  b: 2
};

module.exports 的赋值必须在模块顶层同步完成,不能放到回调里再赋(回调跑的时候,别人已经 require 结束了)。

module.filename

解析后的完整文件名。

module.id

模块标识,通常就是完整文件名。

module.loaded

是否已加载完。循环依赖时,被卡住的那份这里仍是 false

module.parent

最先 require 本模块的那个模块。已不鼓励依赖此属性(入口为 null)。

module.paths

本模块的搜索路径。

module.require(id)

从「这个模块所在位置」出发做一次 require。因为普通 require() 只返回 exports,要用它必须先把 module 自己导出。

module 核心模块

文件里的 module 对象,和 require('module') 拿到的 核心模块 不是一回事。后者是模块系统的 API:

const Module = require('module');
for (const key in Module) {
  console.log(key);
}

某版本上 for...in 能扫到的键(随版本增减,以本机打印为准):

_cache
_pathCache
_extensions
_debug
_findPath
_nodeModulePaths
_resolveLookupPaths
_load
_resolveFilename
_initPaths
_preloadModules

builtinModules
globalPaths
createRequireFromPath
createRequire
syncBuiltinESMExports
register
Module
runMain
findSourceMap
SourceMap

_ 的是内部实现,不要当公共 API。createRequireFromPath 已废弃。register 用来注册 ESM 加载钩子。

module.builtinModules

内置模块名列表,用来区分是核心模块还是第三方包。

const { builtinModules } = require('module');
console.log(builtinModules);

module.createRequire(filename)

构造一个带指定解析起点的 requirefilename 必须是文件 URL、文件 URL 字符串或绝对路径。

// main.mjs(ESM)
import { createRequire } from 'node:module';

const myRequire = createRequire(import.meta.url);
const objC = myRequire('./c.js');
console.log(objC);
// main.js
const { createRequire } = require('module');
const myRequire = createRequire(__filename);
const objC = myRequire('./c.js');
console.log(objC);

module.createRequireFromPath(filename)(已废弃)

旧 API,改用 createRequire()。参数必须是绝对路径一类,不能只给目录还指望和现在 createRequire 行为完全一样。

const { createRequireFromPath } = require('module');
const myRequire = createRequireFromPath('/Users/xxx/Documents/xxx/demo-lizh/node/nodejs/file.js');
const objC = myRequire('./c.js');
console.log(objC);

参考资料

Modules: CommonJS

Modules: node:module API

Modules: ECMAScript

© lizhao all right reserved,powered by Gitbook文件修订时间: 2026-09-02 01:39:06

results matching ""

    No results matching ""