jsernews 1.0.1

<~>

ts 224 days ago.

原文链接:https://blog.safia.rocks/post/169267758290/node-module-deep-dive-module

哦,你好! 好久不见。

我从这篇系列博客中稍微休息了一下,享受假期并编写新版本的 Zarf。但我回来了,并准备继续推出这个系列博客。

我想先看看 module 模块(快速说三遍!)。Node.js 文档的模块页面包含了模块系统的大量信息。在深入 module 代码库之前,我做了一个快速阅读。我将避免总结该文档页面的内容,因为我专注于代码,但如果在阅读本博文的其余部分之前仔细阅读,可能会有所帮助。

像往常一样,我通过阅读 module.js 文件中的最后几行开始读取代码。

Module._initPaths();

// backwards compatibility
Module.Module = Module;

所以看起来好像在底部有一个变量映射,并且注释指出它是为了“向后兼容”。通过查看此文件上的提交历史,我深入了解了这部分。看起来这种变化在2011年初出现了(当时的Node 还只是一个 baby),并且是 CommonJS 模块系统某种重构的一部分。重构涉及一些文件的移动,所以变量映射实际上早于2011年初的日期。我似乎开始走入一个考古虫洞,所以我停下来回到代码。

module.js 始于调用 _initPaths 函数。如果我不得不尽力猜测,我会说它将负责初始化一些路径 - 呵呵呵。_initPaths 函数的前几行非常标准化,在我阅读过的 Node 代码库中很常见。逻辑很简单:如果我们在 Windows 上,请执行此操作;否则,那样做。在这部分,条件逻辑负责设置代表用户主目录的变量。

Module._initPaths = function() {
  const isWindows = process.platform === 'win32';

  var homeDir;
  if (isWindows) {
    homeDir = process.env.USERPROFILE;
  } else {
    homeDir = process.env.HOME;
  }

接下来的一部分代码还使用了类似的条件逻辑来确定相关机器上 Node.js 安装的位置。

  // $PREFIX/lib/node, where $PREFIX is the root of the Node.js installation.
  var prefixDir;
  // process.execPath is $PREFIX/bin/node except on Windows where it is
  // $PREFIX\node.exe.
  if (isWindows) {
    prefixDir = path.resolve(process.execPath, '..');
  } else {
    prefixDir = path.resolve(process.execPath, '..', '..');
  }

然后该函数将创建一个 paths 数组,它将存储可能存在 Node.js 的安装目录和用户的主目录中的 Node 模块位置。

下一段代码获取 NODE_PATH 环境变量的值。我对这个环境变量的名字有点好奇,因为我以前没有遇到过。更彻底地重新阅读模块文档显示,这是一个“遗留”变量,来自于还没有一个如何在 Node 中加载模块的规范的时候。

注:是否有人不太喜欢 process.env['NODE_PATH'] 形式的属性访问器,而不是 process.env.NODE_PATH ?没有?只有我?好吧。

不管怎样。接下来的代码有趣一点了。nodePath 是分号(或冒号,如果你在 Windows上)分隔的一串路径。下一部分代码通过冒号或分号将字符串分解,迭代该列表中的每个元素(即路径),并检查它们是否为真值,然后将它们添加到已有的 paths 列表中。因此,现在我们的路径列表包含 Node.js 安装中的模块集合,我们的主目录以及NODE_PATH 环境变量中引用的目录。

  var nodePath = process.env['NODE_PATH'];
  if (nodePath) {
    paths = nodePath.split(path.delimiter).filter(function(path) {
      return !!path;
    }).concat(paths);
  }

最后,该函数将 Module.globalPaths 的值设置为我们刚刚创建的 paths 列表的副本。

接下来我有兴趣看看的一个函数,就是 require 函数。

Module.prototype.require = function(path) {
  assert(path, 'missing path');
  assert(typeof path === 'string', 'path must be a string');
  return Module._load(path, this, /* isMain */ false);
};

所以看起来 require 需要做一些基本的断言来确认传递的 path 的有效性,然后调用 _load 函数。

if (isMain && experimentalModules) {
  (async () => {
    // loader setup
    if (!ESMLoader) {
      ESMLoader = new Loader();
      const userLoader = process.binding('config').userLoader;
      if (userLoader) {
        const hooks = await ESMLoader.import(userLoader);
        ESMLoader = new Loader();
        ESMLoader.hook(hooks);
      }
    }
    Loader.registerImportDynamicallyCallback(ESMLoader);
    await ESMLoader.import(getURLFromFilePath(request).pathname);
  })()

所以看起来我们正在检查 isMain 变量和 experimentalModules 变量是否为真。

isMain 是指(我猜测)require 是否来自主模块(译注:_load 函数通过 isMain 的值来设定主模块),而 experimentaModules 是指 Node 是否配置为支持 ES模块。看起来如果情况如此,函数调用 ES 模块加载器。我会在另一篇博客文章中多看看 ES 模块加载器,因此我目前放弃深入这部分。取而代之,我查看了 _load 函数的其余部分。

所以看起来 _load 所做的第一件事就是获取与我们要导入的模块相关的文件名。

var filename = Module._resolveFilename(request, parent, isMain);

它通过调用 _resolveFilename 函数基于以下优先级搜索具有该模块名称的文件。

// require("a.<ext>")
//   -> a.<ext>
//
// require("a")
//   -> a
//   -> a.<ext>
//   -> a/index.<ext>

如果您通过上面的链接阅读了模块文档,您会记得 Node 缓存它需要的模块。这使得代码中的下几行易于理解。

var cachedModule = Module._cache[filename];
if (cachedModule) {
  updateChildren(parent, cachedModule, true);
  return cachedModule.exports;
}

所以基本上,一旦它具有与模块相关联的文件名,它将检查该模块是否已经被加载并且在缓存中。如果是,它会调用 updateChildren 函数(我认为)更新了一些内部数据结构,以存储缓存模块现在已被新模块 required 的事实(译注:更新了 module.children 数组)。然后它返回该模块导出的 exports。

下一段代码将检查所请求的模块是否是 NativeModule。如果是这样,它将调用原生模块加载器中的 require 函数。

最后,如果模块尚未被 required ,并且它不是 Native 模块,则该函数调用 Module 构造函数为我们尝试加载的特定模块创建一个新的 Module 对象,并将其存储在缓存中。

var module = new Module(filename, parent);
...
Module._cache[filename] = module;

接下来它调用 module 上的 tryModuleLoad 函数。tryModuleLoad 函数基本上是 Module 对象中的 load 原型函数的一个 try-catch 小包装。load 函数的功能是检查模块文件名上的文件扩展名,然后将其传递给另一个加载器。例如,以 .js 结尾的文件名由一个加载程序处理,以 .json 结尾的文件名由另一个加载。快速浏览一看,我们可以导入以 .js.json.node.mjs(新ES模块系统的文件扩展名)结尾的文件。每个扩展加载程序最终负责填充 module 对象中的 exports 字段,作为 require 的结果返回。

return module.exports;

我将在这里停下代码阅读以保持文章的精简,简而言之,模块加载器负责管理我们系统上的模块(可能不同类型的模块)的缓存和查找。