我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Node.js模块系统源码分析

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Node.js模块系统源码分析

这篇文章主要介绍“Node.js模块系统源码分析”,在日常操作中,相信很多人在Node.js模块系统源码分析问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Node.js模块系统源码分析”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

CommonJS 规范

Node 最初遵循 CommonJS 规范来实现自己的模块系统,同时做了一部分区别于规范的定制。CommonJS 规范是为了解决 JavaScript 的作用域问题而定义的模块形式,它可以使每个模块在它自身的命名空间中执行。

该规范强调模块必须通过 module.exports 导出对外的变量或函数,通过 require() 来导入其他模块的输出到当前模块作用域中,同时,遵循以下约定:

  •  在模块中,必须暴露一个 require 变量,它是一个函数,require 函数接受一个模块标识符,require 返回外部模块的导出的 API。如果要求的模块不能被返回则 require 必须抛出一个错误。

  •  在模块中,必须有一个自由变量叫做 exports,它是一个对象,模块在执行时可以在 exports 上挂载模块的属性。模块必须使用 exports 对象作为唯一的导出方式。

  •  在模块中,必须有一个自由变量 module,它也是一个对象。module 对象必须有一个 id 属性,它是这个模块的顶层 id。id 属性必须是这样的,require(module.id) 会从源出 module.id 的那个模块返回 exports 对象(就是说 module.id 可以被传递到另一个模块,而且在要求它时必须返回最初的模块)。

Node 对 CommonJS 规范的实现

  •  定义了模块内部的 module.require 函数和全局的 require 函数,用来加载模块。

  •  在 Node 模块系统中,每个文件都被视为一个独立的模块。模块被加载时,都会初始化为 Module 对象的实例,Module 对象的基本实现和属性如下所示: 

function Module(id = "", parent) {    // 模块 id,通常为模块的绝对路径    this.id = id;    this.path = path.dirname(id);    this.exports = {};    // 当前模块调用者    this.parent = parent;    updateChildren(parent, this, false);    this.filename = null;    // 模块是否加载完成     this.loaded = false;    // 当前模块所引用的模块    this.children = [];  }
  •  每一个模块都对外暴露自己的 exports 属性作为使用接口。

模块导出以及引用

在 Node 中,可使用 module.exports 对象整体导出一个变量或者函数,也可将需要导出的变量或函数挂载到 exports 对象的属性上,代码如下所示:

// 1. 使用 exports: 笔者习惯通常用作对工具库函数或常量的导出  exports.name = 'xiaoxiang';  exports.add = (a, b) => a + b;  // 2. 使用 module.exports:导出一整个对象或者单一函数  ...  module.exports = {    add,    minus  }

通过全局 require 函数引用模块,可传入模块名称、相对路径或者绝对路径,当模块文件后缀为 js / json / node 时,可省略后缀,如下代码所示:

// 引用模块  const { add, minus } = require('./module');  const a = require('/usr/app/module');  const http = require('http');

注意事项:

  •  exports 变量是在模块的文件级作用域内可用的,且在模块执行之前赋值给 module.exports。 

exports.name = 'test';  console.log(module.exports.name); // test  module.export.name = 'test';  console.log(exports.name); // test
  •  如果为 exports 赋予了新值,则它将不再绑定到 module.exports,反之亦然: 

exports = { name: 'test' };  console.log(module.exports.name, exports.name); // undefined, test
  •  当 module.exports 属性被新对象完全替换时,通常也需要重新赋值 exports: 

module.exports = exports = { name: 'test' };  console.log(module.exports.name, exports.name) // test, test

模块系统实现分析

模块定位

以下是 require 函数的代码实现:

// require 入口函数  Module.prototype.require = function(id) {    //...    requireDepth++;    try {      return Module._load(id, this,  false); // 加载模块    } finally {      requireDepth--;    }  };

上述代码接收给定的模块路径,其中的 requireDepth 用来记载模块加载的深度。其中 Module 的类方法 _load 实现了 Node 加载模块的主要逻辑,下面我们来解析 Module._load 函数的源码实现,为了方便大家理解,我把注释加在了文中。

Module._load = function(request, parent, isMain) {    // 步骤一:解析出模块的全路径    const filename = Module._resolveFilename(request, parent, isMain);     // 步骤二:加载模块,具体分三种情况处理    // 情况一:存在缓存的模块,直接返回模块的 exports 属性    const cachedModule = Module._cache[filename];    if (cachedModule !== undefined)       return cachedModule.exports;    // 情况二:加载内建模块    const mod = loadNativeModule(filename, request);    if (mod && mod.canBeRequiredByUsers) return mod.exports;    // 情况三:构建模块加载    const module = new Module(filename, parent);    // 加载过之后就进行模块实例缓存    Module._cache[filename] = module;    // 步骤三:加载模块文件    module.load(filename);    // 步骤四:返回导出对象    return module.exports;  };

加载策略

上面的代码信息量比较大,我们主要看以下几个问题:

  1.  模块的缓存策略是什么?

  分析上述代码我们可以看到, _load 加载函数针对三种情况给出了不同的加载策略,分别是:

  •   情况一:缓存命中,直接返回。

  •   情况二:内建模块,返回暴露出来的 exports 属性,也就是 module.exports 的别名。

  •   情况三:使用文件或第三方代码生成模块,最后返回,并且缓存,这样下次同样的访问就会去使用缓存而不是重新加载。         

     2.  Module._resolveFilename(request, parent, isMain) 是怎么解析出文件名称的?

我们看如下定义的类方法:

Module._resolveFilename = function(request, parent, isMain, options) {   if (NativeModule.canBeRequiredByUsers(request)) {        // 优先加载内建模块     return request;   }   let paths;   // node require.resolve 函数使用的 options,options.paths 用于指定查找路径   if (typeof options === "object" && options !== null) {     if (ArrayIsArray(options.paths)) {       const isRelative =         request.startsWith("./") ||         request.startsWith("../") ||         (isWindows && request.startsWith(".\\")) ||         request.startsWith("..\\");       if (isRelative) {         paths = options.paths;       } else {         const fakeParent = new Module("", null);         paths = [];         for (let i = 0; i < options.paths.length; i++) {           const path = options.paths[i];           fakeParent.paths = Module._nodeModulePaths(path);           const lookupPaths = Module._resolveLookupPaths(request, fakeParent);           for (let j = 0; j < lookupPaths.length; j++) {             if (!paths.includes(lookupPaths[j])) paths.push(lookupPaths[j]);           }         }       }     } else if (options.paths === undefined) {       paths = Module._resolveLookupPaths(request, parent);     } else {          //...     }   } else {     // 查找模块存在路径     paths = Module._resolveLookupPaths(request, parent);   }   // 依据给出的模块和遍历地址数组,以及是否为入口模块来查找模块路径   const filename = Module._findPath(request, paths, isMain);   if (!filename) {     const requireStack = [];     for (let cursor = parent; cursor; cursorcursor = cursor.parent) {       requireStack.push(cursor.filename || cursor.id);     }     // 未找到模块,抛出异常(是不是很熟悉的错误)     let message = `Cannot find module '${request}'`;     if (requireStack.length > 0) {       messagemessage = message + "\nRequire stack:\n- " + requireStack.join("\n- ");     }     const err = new Error(message);     err.code = "MODULE_NOT_FOUND";     err.requireStack = requireStack;     throw err;   }   // 最终返回包含文件名的完整路径   return filename;  };

上面的代码中比较突出的是使用了 _resolveLookupPaths 和 _findPath 两个方法。

  •  _resolveLookupPaths: 通过接受模块名称和模块调用者,返回提供 _findPath 使用的遍历范围数组。 

// 模块文件寻址的地址数组方法    Module._resolveLookupPaths = function(request, parent) {     if (NativeModule.canBeRequiredByUsers(request)) {       debug("looking for %j in []", request);       return null;     }     // 如果不是相对路径     if (       request.charAt(0) !== "." ||       (request.length > 1 &&         request.charAt(1) !== "." &&         request.charAt(1) !== "/" &&         (!isWindows || request.charAt(1) !== "\\"))     ) {              let paths = modulePaths;       if (parent != null && parent.paths && parent.paths.length) {         // 父模块的 modulePath 也要加到子模块的 modulePath 里面,往上回溯查找         paths = parent.paths.concat(paths);       }       return paths.length > 0 ? paths : null;     }     // 使用 repl 交互时,依次查找 ./ ./node_modules 以及 modulePaths     if (!parent || !parent.id || !parent.filename) {       const mainPaths = ["."].concat(Module._nodeModulePaths("."), modulePaths);           return mainPaths;     }     // 如果是相对路径引入,则将父级文件夹路径加入查找路径     const parentDir = [path.dirname(parent.filename)];     return parentDir;    };
  •  _findPath: 依据目标模块和上述函数查找到的范围,找到对应的 filename 并返回。 

// 依据给出的模块和遍历地址数组,以及是否顶层模块来寻找模块真实路径  Module._findPath = function(request, paths, isMain) {   const absoluteRequest = path.isAbsolute(request);   if (absoluteRequest) {    // 绝对路径,直接定位到具体模块     paths = [""];   } else if (!paths || paths.length === 0) {     return false;   }   const cacheKey =     request + "\x00" + (paths.length === 1 ? paths[0] : paths.join("\x00"));   // 缓存路径   const entry = Module._pathCache[cacheKey];   if (entry) return entry;   let exts;   let trailingSlash =     request.length > 0 &&     request.charCodeAt(request.length - 1) === CHAR_FORWARD_SLASH; // '/'   if (!trailingSlash) {     trailingSlash = /(?:^|\/)\.?\.$/.test(request);   }   // For each path   for (let i = 0; i < paths.length; i++) {     const curPath = paths[i];     if (curPath && stat(curPath) < 1) continue;     const basePath = resolveExports(curPath, request, absoluteRequest);     let filename;     const rc = stat(basePath);     if (!trailingSlash) {       if (rc === 0) { // stat 状态返回 0,则为文件         // File.         if (!isMain) {           if (preserveSymlinks) {             // 当解析和缓存模块时,命令模块加载器保持符号连接。             filename = path.resolve(basePath);           } else {             // 不保持符号链接             filename = toRealPath(basePath);           }         } else if (preserveSymlinksMain) {           filename = path.resolve(basePath);         } else {           filename = toRealPath(basePath);         }       }       if (!filename) {         if (exts === undefined) exts = ObjectKeys(Module._extensions);         // 解析后缀名         filename = tryExtensions(basePath, exts, isMain);       }     }     if (!filename && rc === 1) {               if (exts === undefined) exts = ObjectKeys(Module._extensions);       filename = tryPackage(basePath, exts, isMain, request);     }     if (filename) { // 如果存在该文件,将文件名则加入缓存       Module._pathCache[cacheKey] = filename;       return filename;     }   }   const selfFilename = trySelf(paths, exts, isMain, trailingSlash, request);   if (selfFilename) {     // 设置路径的缓存     Module._pathCache[cacheKey] = selfFilename;     return selfFilename;   }   return false;  };

模块加载

标准模块处理

阅读完上面的代码,我们发现,当遇到模块是一个文件夹的时候会执行 tryPackage 函数的逻辑,下面简要分析一下具体实现。

// 尝试加载标准模块  function tryPackage(requestPath, exts, isMain, originalPath) {    const pkg = readPackageMain(requestPath);    if (!pkg) {      // 如果没有 package.json 这直接使用 index 作为默认入口文件      return tryExtensions(path.resolve(requestPath, "index"), exts, isMain);    }    const filename = path.resolve(requestPath, pkg);    let actual =      tryFile(filename, isMain) ||      tryExtensions(filename, exts, isMain) ||      tryExtensions(path.resolve(filename, "index"), exts, isMain);    //...    return actual;  }  // 读取 package.json 中的 main 字段  function readPackageMain(requestPath) {    const pkg = readPackage(requestPath);    return pkg ? pkg.main : undefined;  }

readPackage 函数负责读取和解析 package.json 文件中的内容,具体描述如下:

function readPackage(requestPath) {    const jsonPath = path.resolve(requestPath, "package.json");    const existing = packageJsonCache.get(jsonPath);    if (existing !== undefined) return existing;    // 调用 libuv uv_fs_open 的执行逻辑,读取 package.json 文件,并且缓存    const json = internalModuleReadJSON(path.toNamespacedPath(jsonPath));    if (json === undefined) {      // 接着缓存文件      packageJsonCache.set(jsonPath, false);      return false;    }    //...    try {      const parsed = JSONParse(json);      const filtered = {        name: parsed.name,        main: parsed.main,        exports: parsed.exports,        type: parsed.type      };      packageJsonCache.set(jsonPath, filtered);      return filtered;    } catch (e) {      //...    }  }

上面的两段代码完美地解释 package.json 文件的作用,模块的配置入口( package.json 中的 main 字段)以及模块的默认文件为什么是 index,具体流程如下图所示:

Node.js模块系统源码分析

模块文件处理

定位到对应模块之后,该如何加载和解析呢?以下是具体代码分析:

Module.prototype.load = function(filename) {    // 保证模块没有加载过    assert(!this.loaded);    this.filename = filename;    // 找到当前文件夹的 node_modules    this.paths = Module._nodeModulePaths(path.dirname(filename));    const extension = findLongestRegisteredExtension(filename);    //...    // 执行特定文件后缀名解析函数 如 js / json / node    Module._extensions[extension](this, filename);    // 表示该模块加载成功    this.loaded = true;    // ... 省略 esm 模块的支持  };

后缀处理

可以看出,针对不同的文件后缀,Node.js 的加载方式是不同的,一下针对 .js, .json, .node 简单进行分析。

  •  .js 后缀 js 文件读取主要通过 Node 内置 API fs.readFileSync 实现。 

Module._extensions[".js"] = function(module, filename) {    // 读取文件内容    const content = fs.readFileSync(filename, "utf8");    // 编译执行代码    module._compile(content, filename);  };
  •  .json 后缀 JSON 文件的处理逻辑比较简单,读取文件内容后执行 JSONParse 即可拿到结果。 

Module._extensions[".json"] = function(module, filename) {    // 直接按照 utf-8 格式加载文件    const content = fs.readFileSync(filename, "utf8");    //...    try {      // 以 JSON 对象格式导出文件内容      module.exports = JSONParse(stripBOM(content));    } catch (err) {      //...    }  };
  •  .node 后缀 .node 文件是一种由 C / C++ 实现的原生模块,通过 process.dlopen 函数读取,而 process.dlopen 函数实际上调用了 C++ 代码中的 DLOpen 函数,而 DLOpen 中又调用了 uv_dlopen, 后者加载 .node 文件,类似 OS 加载系统类库文件。 

Module._extensions[".node"] = function(module, filename) {    //...    return process.dlopen(module, path.toNamespacedPath(filename));  };

从上面的三段源码,我们看出来并且可以理解,只有 JS 后缀最后会执行实例方法 _compile,我们去除一些实验特性和调试相关的逻辑来简要的分析一下这段代码。

编译执行

模块加载完成后,Node 使用 V8 引擎提供的方法构建运行沙箱,并执行函数代码,代码如下所示:

Module.prototype._compile = function(content, filename) {    let moduleURL;    let redirects;    // 向模块内部注入公共变量 __dirname / __filename / module / exports / require,并且编译函数    const compiledWrapper = wrapSafe(filename, content, this);    const dirname = path.dirname(filename);    const require = makeRequireFunction(this, redirects);    let result;    const exports = this.exports;    const thisValue = exports;    const module = this;    if (requireDepth === 0) statCache = new Map();        //...     // 执行模块中的函数      result = compiledWrapper.call(        thisValue,        exports,        require,        module,        filename,        dirname      );    hasLoadedAnyUserCJSModule = true;    if (requireDepth === 0) statCache = null;    return result;  };  // 注入变量的核心逻辑  function wrapSafe(filename, content, cjsModuleInstance) {    if (patched) {      const wrapper = Module.wrap(content);      // vm 沙箱运行 ,直接返回运行结果,env -> SetProtoMethod(script_tmpl, "runInThisContext", RunInThisContext);      return vm.runInThisContext(wrapper, {        filename,        lineOffset: 0,        displayErrors: true,        // 动态加载        importModuleDynamically: async specifier => {          const loader = asyncESM.ESMLoader;          return loader.import(specifier, normalizeReferrerURL(filename));        }      });    }    let compiled;    try {      compiled = compileFunction(        content,        filename,        0,        0,        undefined,        false,        undefined,        [],        ["exports", "require", "module", "__filename", "__dirname"]      );    } catch (err) {      //...    }    const { callbackMap } = internalBinding("module_wrap");    callbackMap.set(compiled.cacheKey, {      importModuleDynamically: async specifier => {        const loader = asyncESM.ESMLoader;        return loader.import(specifier, normalizeReferrerURL(filename));      }    });    return compiled.function;  }

上述代码中,我们可以看到在 _compile 函数中调用了 wrapwrapSafe 函数,执行了 __dirname / __filename / module / exports / require 公共变量的注入,并且调用了 C++ 的 runInThisContext 方法(位于 class="lazy" data-src/node_contextify.cc 文件)构建了模块代码运行的沙箱环境,并返回了 compiledWrapper 对象,最终通过 compiledWrapper.call 方法运行模块。

到此,关于“Node.js模块系统源码分析”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注编程网网站,小编会继续努力为大家带来更多实用的文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Node.js模块系统源码分析

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

node.js文件系统模块实例分析

这篇文章主要讲解了“node.js文件系统模块实例分析”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“node.js文件系统模块实例分析”吧!一、node.js文件系统模块node.jsjav
2023-06-30

Node.js深入分析Koa源码

本文主要从源码的角度来讲述Koa,尤其是其中间件系统是如何实现的。跟Express相比,Koa的源码异常简洁,Express因为把路由相关的代码嵌入到了主要逻辑中,因此读Express的源码可能长时间不得要领,而直接读Koa的源码几乎没有什么障碍
2022-11-13

nginx slice模块的使用和源码分析小结

Nginxslice模块用于分片并按需流传输大型文件。通过配置sliceon和slice_max_size,可以启用此模块。该模块包含ngx_http_slice_filter_module(分片文件)和ngx_http_slice_stream_module(流式传输分片)。其性能优化包括并发传输、范围请求和分片缓存。slice模块通过分片文件、支持范围请求和并发传输,有效地优化了流化性能,使其成为流媒体应用程序的理想选择。
nginx slice模块的使用和源码分析小结
2024-04-02

ASP.NET Core Zero模块系统的示例分析

这篇文章主要为大家展示了“ASP.NET Core Zero模块系统的示例分析”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“ASP.NET Core Zero模块系统的示例分析”这篇文章吧。简介
2023-06-29

Python数据分析模块Numpy切片、索引和广播源码分析

这篇文章主要讲解了“Python数据分析模块Numpy切片、索引和广播源码分析”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Python数据分析模块Numpy切片、索引和广播源码分析”吧!N
2023-07-06

编程热搜

目录