调试理解 NodeJS 模块机制

前言

通过断点调试理解 NodeJS & CommonJS 的模块机制,先说结论:

  1. NodeJS 中每个文件视作一个模块,每个模块默认可以访问 moduleexportsrequire__filename__dirname 变量
  2. NodeJS 中通过将模块源码包裹在 Wrapper 函数中,并通过调用函数传递参数的方式传递默认变量
  3. 通过 vm 模块 的 runInThisContext 方法 生成 Wrapper 函数,使用 evalnew Function 的方式生成都会出现作用域问题,eval 的作用域为当前上下文,new Function 的作用域为全局上下文

示例代码:

const path = require("path");
const fs = require("fs");
const vm = require("vm");function myRequire(_path) {// 计算决定路径const absolatePath = /* ... */;// 计算文件名const filename = /* ... */;// 读取文件源码const sourceCode = fs.readFileSync(absolatePath, 'utf-8');// 生成 Wrapper 函数const fn = vm.runInThisContext(`(function (exports, module, require, __filename, __dirname) { ${sourceCode} });`);// 定义 moduleconst module = { exports: {} };// 调用 Wrapper 函数,此时 module 代码执行,并为 module.exports 赋值fn.apply(module.exports, [module.exports, module, myRequire, filename, absolatePath]);return module.exports;
}

大致的流程如此,后面是调试流程,长文警告。

准备调试

调试流程:

新建目录,在目录根路径执行:

npm init -y

新建 index.js 文件,写点代码,并在首行代码处打上断点:

image

新建 /.vscode/launch.json vscode 调试配置文件:

image

简单说下调试配置项:

  • type:调试类型,node 表示 node 程序
  • request:请求类型,这里是启动
  • name:调试配置名称
  • skipFiles:配置调试过程中跳过的文件,默认跳过 node 内部代码文件,我们需要调试 node 的启动流程,所以这里将他注释
  • program:程序所在的位置

在 vscode 左侧调试面板中,选择我们的配置并运行调试,默认会跳过 node 的启动流程,直接断到我们编写的程序中:

image

但是可以看到左下角列出来经过的全部程序,我们直接在首先启动的程序文件中打上断点:

image

image

接下来就可以开始愉快的调试了。

流程调试

查看最先执行的程序代码:

/*** other code in here...*/if (getOptionValue('--experimental-default-type') === 'module') {require('internal/modules/run_main').executeUserEntryPoint(mainEntry);
} else {require('internal/modules/cjs/loader').Module.runMain(mainEntry);
}

getOptionValue 获取到 nodejs 对应选项的配置值,这里判断 --experimental-default-type 属性值是否是 module,如果是则使用 esm 加载器来执行,可以通过 node --experimental-default-type=module 来进入这个条件。

在调试时可以在调试配置中添加 "runtimeArgs": ["--experimental-default-type=module"] 来配置运行时参数。

我们不关注 type=module 的情况,所以这里直接忽略,进入 else 的判断。

接下来进入 runMain 方法,代码如下:

/*** other code in here...*/function executeUserEntryPoint(main = process.argv[1]) {/** 获取入口代码的路径 */const resolvedMain = resolveMainPath(main);/** 判断是否应该使用 esm loader */const useESMLoader = shouldUseESMLoader(resolvedMain);let mainURL;if (!useESMLoader) {/** 使用 commonjs loader */const cjsLoader = require('internal/modules/cjs/loader');const { wrapModuleLoad } = cjsLoader;wrapModuleLoad(main, null, true);} else {/** 使用 esm loader */const mainPath = resolvedMain || main;if (mainURL === undefined) {mainURL = pathToFileURL(mainPath).href;}runEntryPointWithESMLoader((cascadedLoader) => {return cascadedLoader.import(mainURL, undefined, { __proto__: null }, true);});}
}

resolveMainPath 函数比较简单,就是用一堆工具函数来找到真实的入口文件路径,这里不进行说明,来看看 shouldUseESMLoader 的函数的代码:

/** 判断是否应该使用 esm loader */
function shouldUseESMLoader(mainPath) {if (getOptionValue('--experimental-default-type') === 'module') {return true;}/** 用户注册的自定义 loader 列表 */const userLoaders = getOptionValue('--experimental-loader');/** 用户注册的预加载模块列表 */const userImports = getOptionValue('--import');if (userLoaders.length > 0 || userImports.length > 0) {return true;}/** 解析入口文件的文件后缀,如果为 mjs 则是 esm,如果是 cjs 则是 commonjs */if (mainPath && StringPrototypeEndsWith(mainPath, '.mjs')) {return true;}if (!mainPath || StringPrototypeEndsWith(mainPath, '.cjs')) {return false;}/** 这里是获取到 package.json 中的 type 定义 */const type = getNearestParentPackageJSONType(mainPath);if (type === undefined || type === 'none') {return false;}/** type 为 module 则为 esm 模块 */return type === 'module';
}

这里可以看到满足以下几个条件之一则使用 esm loader:

  • 运行时设置 --experimental-default-type=module
  • 运行时指定了自定义 loader
  • 运行时指定了预加载的模块
  • 入口文件后缀为 .mjs
  • 当前项目的 package.json 中 type 指定为 module

我们依然不关心 esm loader 的相关逻辑,走到 wrapModuleLoad 方法,代码如下:

/*** other code in here...*//** 这是对内部方法 Module._load 的包装,request 是加载的模块,parent 是父模块,isMain 表示入口模块 */
function wrapModuleLoad(request, parent, isMain) {/* ... */try {return onRequire().traceSync(Module._load,{__proto__: null,parentFilename: parent?.filename,id: request},Module,request,parent,isMain);} finally {/* ... */}
}

traceSync 方法就是对函数的一层封装,在执行函数前后添加了 hook 代码,主要用于跟踪函数执行,这里执行的是 Module._load 方法,我们继续查看他的代码:

Module._load = function (request, parent, isMain) {/** TIPS: 省略了大部分代码,完整的代码建议自己调试查看 *//** 判断请求的模块路径是否以 node: 开头,node: 开头的是 nodejs 内置模块 */if (StringPrototypeStartsWith(request, 'node:')) {const id = StringPrototypeSlice(request, 5);if (!BuiltinModule.canBeRequiredByUsers(id)) {throw new ERR_UNKNOWN_BUILTIN_MODULE(request);}const module = loadBuiltinModule(id, request);return module.exports;}/** 获取模块的决定路径 */const filename = Module._resolveFilename(request, parent, isMain);/** 获取缓存 */const cachedModule = Module._cache[filename];/** 返回被缓存且已加载完成的模块 */if (cachedModule !== undefined) {if (cachedModule.loaded) {return cachedModule.exports;}}/** 获取缓存模块或构建新模块 */const module = cachedModule || new Module(filename, parent);/** 标识模块信息 */if (!cachedModule) {if (isMain) {setOwnProperty(process, 'mainModule', module);setOwnProperty(module.require, 'main', process.mainModule);module.id = '.';module[kIsMainSymbol] = true;} else {module[kIsMainSymbol] = false;}/** 缓存模块 */Module._cache[filename] = module;}let threw = true;try {/** 尝试加载模块 */module.load(filename);threw = false;} finally {/** 模块加载失败需要清理残留 */if (threw) {delete Module._cache[filename];}}/** 返回模块 */return module.exports;
};

核心的加载代码在 module.load 方法中,代码如下:

Module.prototype.load = function (filename) {this.filename = filename;this.paths = Module._nodeModulePaths(path.dirname(filename));/** 通过文件后缀查找已注册的扩展 */const extension = findLongestRegisteredExtension(filename);/** 如果文件以 .mjs 为后缀,且没有注册 .mjs 的扩展,则抛出错误 */if (StringPrototypeEndsWith(filename, '.mjs') && !Module._extensions['.mjs']) {throw new ERR_REQUIRE_ESM(filename, true);}/** 调用对应文件后缀的扩展,将 module 实例和模块路径传递 */Module._extensions[extension](this, filename);/** 标识已被加载 */this.loaded = true;const exports = this.exports;this[kModuleExport] = exports;
};

load 方法内部通过 findLongestRegisteredExtension 方法查找对应的扩展名,最后调用指定扩展来加载模块,这里的扩展名为 .js,默认在 Module._extension 中注册,他的代码如下:

Module._extensions['.js'] = function (module, filename) {/** 获取模块源码 */const content = getMaybeCachedSource(module, filename);let format;if (StringPrototypeEndsWith(filename, '.js')) {/** 获取 package.json 配置 */const pkg = packageJsonReader.getNearestParentPackageJSON(filename);if (pkg?.data.type === 'module') {/** 如果启用了 --experimental-require-module,则允许在 type=module 的情况下使用 require */if (getOptionValue('--experimental-require-module')) {/** 以 esm 模式编译模块 */module._compile(content, filename, 'module');return;}/** 构造错误信息 */const parent = module[kModuleParent];const parentPath = parent?.filename;const packageJsonPath = path.resolve(pkg.path, 'package.json');const usesEsm = containsModuleSyntax(content, filename);const err = new ERR_REQUIRE_ESM(filename, usesEsm, parentPath, packageJsonPath);// Attempt to reconstruct the parent require frame.if (Module._cache[parentPath]) {let parentSource;try {parentSource = fs.readFileSync(parentPath, 'utf8');} catch {// Continue regardless of error.}if (parentSource) {const errLine = StringPrototypeSplit(StringPrototypeSlice(err.stack, StringPrototypeIndexOf(err.stack, '    at ')),'\n',1)[0];const { 1: line, 2: col } = RegExpPrototypeExec(/(\d+):(\d+)\)/, errLine) || [];if (line && col) {const srcLine = StringPrototypeSplit(parentSource, '\n')[line - 1];const frame = `${parentPath}:${line}\n${srcLine}\n${StringPrototypeRepeat(' ',col - 1)}^\n`;setArrowMessage(err, frame);}}}throw err;} else if (pkg?.data.type === 'commonjs') {format = 'commonjs';}} else if (StringPrototypeEndsWith(filename, '.cjs')) {format = 'commonjs';}/** 以 commonjs 模式编译源码 */module._compile(content, filename, format);
};

继续查看 module._compile 方法,代码如下:

Module.prototype._compile = function (content, filename, format) {/** 删除大部分代码,建议自行调试查看 */let redirects;let compiledWrapper;if (format !== 'module') {/** wrapSafe 方法包装模块源码并生成函数,函数作用域与外界隔离 */const result = wrapSafe(filename, content, this, format);/** compiledWrapper 即是生成的函数 */compiledWrapper = result.function;}/** 获取模块所在目录 */const dirname = path.dirname(filename);/**** 构建 require 函数,对 module.require 方法的包装, module.require 方法又是对开头介绍的 wrapModuleLoad 函数的包装* */const require = makeRequireFunction(this, redirects);let result;const exports = this.exports;const thisValue = exports;const module = this;/** 执行生成的包装函数并传递参数 */result = ReflectApply(compiledWrapper, thisValue, [exports, require, module, filename, dirname]);return result;
};

当执行到 ReflectApply 函数内部时,会执行生成的包装函数,继续向下执行会发现断到了我们编写的模块内部:

image

此时流程基本已经结束,可以看到核心的逻辑基本与 Module 类相关,下面我用 ts 给出 Module 类的主要定义(部分内容忽略)。

Module 类型定义

declare type Exports = { [key: string | symbol]: any };declare class Module {constructor(id: string, parent: Module);/** 模块 id,一般为模块绝对路径 */id: string;/** 模块路径 */path: string;/** module.exports,模块中通过给这个对象新增属性来达到导出目的 */exports: Exports;/** 文件名 */filename: string | null;/** 是否加载完成 */loaded: boolean;/** 外层包裹函数字符串的代理 ["function(module, exports, require, __filename, __dirname) {", "};"] */wrapper: [string, string];/** 父模块 */parent?: Module;/** 子模块 */children?: Module[];/** 缓存模块 */static _cache: Record<string, Module>;/** 缓存路径 */static _pathCache: Record<string, Module>;/** 注册扩展 */static _extensions: Record<string, (module: Module, filename: string) => void>;/** 全局路径 */static globalPaths: string[];/** 生成包裹函数 */wrap(script: string): string;/** 加载模块 */load(filename: string): void;/** 请求模块 */require(id: string): Exports;private _compile(content: string,filename: string,format: 'module' | 'commonjs' | undefined): Exports;/** 创建 require 函数 */static createRequire(filename: string | URL): (id: string) => Exports;/*** 如果用户覆盖了内置模块的导出,此函数可以确保覆盖用于 CommonJS 和 ES 模块上下文*/static syncBuiltinESMExports(): void;/** 查找路径 */private static _findPath(request: string, paths: string[], isMain: boolean): string | false;/** 根据给定路径查找 node_modules 路径 */private static _nodeModulePaths(from: string): string[];/** 获取模块解析路径 */private static _resolveLookupPaths(request: string, parent: Module): string[];/** 加载模块 */private static _load(request: string, parent: Module, isMain: boolean): Exports;/** 解析模块绝对路径 */private static _resolveFilename(request: string,parent: Module,isMain: boolean,options: object,paths: string[]): string;/** 定义用于解析模块的路径 */private static _initPaths(): void;/** 处理通过 “--require” 加载的模块 */private static _preloadModules(requests: string[]): void;
}

–end

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/52416.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【每日一题】【素数筛板子题】又是一年毕业季 牛客小白月赛99 D题 C++

牛客小白月赛99 D题 又是一年毕业季 题目背景 牛客小白月赛99 题目描述 样例 #1 样例输入 #1 3 4 2 4 6 5 5 6 2 5 3 2333333 8 11 4 5 14 19 19 8 10样例输出 #1 3 7 2做题思路 首先观察到 即需要保证拍照的时刻 大于等于 2 那么就从2开始往上走&#xff0c;如果有人…

红黑树、B+Tree、B—Tree

红黑树 B-Tree 这三个通常都是把内存全部加载到内存里&#xff0c;然后再内存中进行处理的&#xff0c;数据量通常不会很大。 内存一般容量都在GB级别&#xff0c;比如说现在常见的4G、8G或者16G。 如果要处理的数据规模非常大&#xff0c;大到内存根本存不下的时候。这个时候…

Spring Boot 集成 swagger 3.0 指南

Spring Boot 集成 swagger 3.0 指南 一、Swagger介绍1.springfox-swagger 22.SpringFox 3.0.0 发布 二、Spring Boot 集成 swagger 3.01. 添加Maven依赖2. 创建配置类配置Swagger2.1 创建SwaggerConfig 配置类2.1 创建TestInfoConfig信息配置类 3. 在你的Controller上添加swagg…

【思源笔记】思源笔记配置S3同步

本文首发于 ❄️慕雪的寒舍 文章目录 1. 写在前面2. 什么是思源笔记的S3/WEBDAV同步&#xff1f;2.1. 说明2.2. 思源的同步配置和工作空间2.3. 什么是S3协议&#xff1f; 3. 配置思源S3同步3.1. 初始化数据仓库密钥3.2. 思源S3同步界面3.3. 配置七牛云KODO3.4. 如何将同步配置导…

以GD32F103C8T6为例的核心板原理图PCB绘制学习笔记简单总结

目录 GD32F103C8T6核心板 设计流程 基础知识 部分原理图解析 排针连接 (H1 - PZ254V-12-8P): 晶振 封装 基础知识 C0603封装 C0805 F1210封装 保险丝 L0603 贴片电感 LED-0603 R0603 HDR-TH_8P-P2.54-V-M-R2-C4-S2.54 排针 按键&#xff08;SW-SMD-T6X…

Python(PyTorch)物理变化可微分神经算法

&#x1f3af;要点 &#x1f3af;使用受控物理变换序列实现可训练分层物理计算 | &#x1f3af;多模机械振荡、非线性电子振荡器和光学二次谐波生成神经算法验证 | &#x1f3af;训练输入数据&#xff0c;物理系统变换产生输出和可微分数字模型估计损失的梯度 | &#x1f3af;…

Nacos微服务注册管理中心与服务通信

参照springboot-alibaba-ribbon项目学习 E:\Codes\Idea_java_works\apesource\springboot\微服务\springboot_alibaba_ribbon Nacos 微服务注册中心-discover Nacos 是⼀个更易于构建云原⽣应⽤的动态服务发现、配置管理和服务管理平台。简单来说 Nacos 就是 注册中⼼ 配置…

Java入门:06.Java中的方法--进阶02.03

2 可变参数 方法调用时&#xff0c; 传递的实参数量&#xff0c;是由被调用方法的参数列表数列决定的。 一般来讲&#xff0c;传递的实参数量必须与形参变量数量相同&#xff0c;但是也有一种特殊的参数&#xff0c;允许调用时传递的实参数量是可变&#xff0c;这种参数就称为…

CSS3多行多栏布局

当前布局由6个等宽行组成&#xff0c;其中第四行有三栏&#xff0c;第五行有四栏。 重点第四行设置&#xff1a; 代码&#xff1a; <!DOCTYPE html> <html><head><meta charset"utf-8"><title></title><style>img {hei…

AI 时代的编程革命:如何在挑战中抓住机遇?

AI 发展对软件开发的挑战与机遇&#xff1a;程序员应对策略 随着人工智能&#xff08;AI&#xff09;技术的快速进步&#xff0c;软件开发领域正经历深刻的变革。AI 不仅改变了编程的方式&#xff0c;也对程序员的职业发展产生了重要影响。在这个背景下&#xff0c;我们既看到…

HTML5休闲小游戏《砖块破坏者》源码,引流、刷广告利器

HTML5休闲小游戏《砖块破坏者》源码&#xff0c;直接把源码上传到服务器就能使用了&#xff01; 下载链接&#xff1a;https://www.huzhan.com/code/goods468802.html

Linux:Bash中的命令介绍(简单命令、管道以及命令列表)

相关阅读 Linuxhttps://blog.csdn.net/weixin_45791458/category_12234591.html?spm1001.2014.3001.5482 在Bash中&#xff0c;命令执行的方式可以分为简单命令、管道和命令列表组成。这些结构提供了强大的工具&#xff0c;允许用户组合命令并精确控制其执行方式。以下是对这…

2024年【电气试验】找解析及电气试验模拟考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 电气试验找解析根据新电气试验考试大纲要求&#xff0c;安全生产模拟考试一点通将电气试验模拟考试试题进行汇编&#xff0c;组成一套电气试验全真模拟考试试题&#xff0c;学员可通过电气试验模拟考试全真模拟&#…

文件IO和多路复用IO

目录 前言 一、文件 I/O 1.基本文件 I/O 操作 1.1打开文件 1.2读取文件内容 (read) 1.3写入文件 (write) 1.4关闭文件 (close) 2.文件指针 二、多路复用 I/O 1.常用的多路复用 I/O 模型 1.1select 1.2poll 1.3epoll 2.使用 select、poll 和 epoll 进行简单的 I/O…

C++观察者模式Observer

组件协作 –(都是晚绑定的&#xff09; ----观察者模式 为某些对象建立一种通知依赖的关系&#xff0c; 只要这个对象状态发生改变&#xff0c;观察者对象都能得到通知。 但是依赖关系要松耦合&#xff0c;不要太依赖。 eg&#xff1a;做一个文件分割器&#xff0c;需要一个…

css实现水滴效果图

效果图&#xff1a; <template><div style"width: 100%;height:500px;padding:20px;"><div class"water"></div></div> </template> <script> export default {data() {return {};},watch: {},created() {},me…

B/S架构和C/S架构的区别

B/S架构、C/S架构区别 1. B/S架构 1.1 什么是B/S架构 B/S架构的全称为Browser/Server&#xff0c;即浏览器/服务器结构。Browser指的是Web浏览器&#xff0c;极少数事务逻辑在前端实现&#xff0c;但主要事务逻辑在服务器端实现。B/S架构的系统无须特别安装&#xff0c;只需要…

动态内存管理-经典笔试题

目录 题目一&#xff1a; 题目二&#xff1a; 题目三&#xff1a; 题目四&#xff1a; 题目一&#xff1a; 结果&#xff1a;程序崩溃 原因&#xff1a; 1、函数是传值调用&#xff0c;出了函数p不存在&#xff0c;str未改变&#xff0c;依旧为空指针&#xff0c;运行时发…

【CTF Web】CTFShow 版本控制泄露源码2 Writeup(目录扫描+.svn泄漏)

版本控制泄露源码2 10 版本控制很重要&#xff0c;但不要部署到生产环境更重要。 解法 用 dirsearch 扫描。 dirsearch -u https://8d22223d-dc2c-419c-b82d-a1d781eda427.challenge.ctf.show/找到 .svn 仓库。 访问&#xff1a; https://8d22223d-dc2c-419c-b82d-a1d781eda…

ubuntu安装minio

# 下载MinIO的可执行文件 curl -O https://dl.min.io/server/minio/release/linux-amd64/minio # 添加执行权限 chmod x minio # 运行MinIO (需要先创建存储数据和存储存储桶的目录) nohup ./minio server /home/lighthouse/minioDir/data /home/lighthouse/minioDir/bucke…