node 的模块运行机制

3,000 阅读7分钟

node 的模块运行机制简单了解。 涉及大概流程,略过底层系统区别。

  1. CommonJS 的规范
  2. node 模块加载过程
  3. 小结

1.CommonJS 的规范

CommonJS 的规范,包括模块引用模块定义,模块标识,3个部分

模块引用: 模块通过require方法来同步加载所依赖的模块

模块定义: 在node中一个文件就是一个模块,提供exports对象导出当前模块的方法或变量

模块标识: 模块标识传递给require()方法的参数,可以是按小驼峰(camelCase)命名的字符串,也可以是文件路径。

1.1.node 模块中CommonJS 的应用

模块内容导出两种方式:

a.js的内容如下,

方式一:可将需要导出的变量或函数挂载到 exports 对象的属性上

// node.js 每一个文件都是一个单独模块
// Node对获取的Javascript文件的内容进行了包装,以传入如下变量
console.log(exports, require, module, __filename, __dirname);
// 可将需要导出的变量或函数挂载到 exports 对象的属性上,
exports.name = 'luoxiaobu';
exports.age = '18'

方式二:使用 module.exports 对象整体导出一个变量对象或者函数

// node.js 每一个文件都是一个单独模块
// Node对获取的Javascript文件的内容进行了包装,以传入如下变量
console.log(exports, require, module, __filename, __dirname);
let name = 'luoxiaobu';
let age = '18'
// 使用 module.exports 对象整体导出一个变量对象或者函数,
module.exports = {name,age};

模块的引用的方式: 按照引用模块的来源来区分

// 核心模块的引入 node自己的模块
let crypto = require('crypto')

// 用户自己编写的模块引入
let aModule = require('./a.js')
// 第三方,别人实现发布的模块(其实也是其他用户编写)
let proxy = require('http-proxy');

2.node 模块加载过程

node.js 每一个文件都是一个单独模块,每个模块都用一个module对象来表示自身。
非 node NativeModule
// 非 node NativeModule
function Module(id = '', parent) {
  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 = [];
}
NativeModule
// Set up NativeModule.
function NativeModule(id) {
  this.filename = `${id}.js`;
  this.id = id;
  this.exports = {};
  this.module = undefined;
  this.exportKeys = undefined;
  this.loaded = false;
  this.loading = false;
  this.canBeRequiredByUsers = !id.startsWith('internal/');
}

2.1 node 模块加载简述

加载过程大概流程:(Module._load 加载函数)
代码略微删减
// Check the cache for the requested file.
// 1. If a module already exists in the cache: return its exports object.
// 2. If the module is native: call
//    `NativeModule.prototype.compileForPublicLoader()` and return the exports.
// 3. Otherwise, create a new module for the file and save it to the cache.
//    Then have it load  the file contents before returning its exports
//    object.
Module._load = function(request, parent, isMain) {
    let relResolveCacheIdentifier;
    if (parent) {
      debug('Module._load REQUEST %s parent: %s', request, parent.id);
      ...
    }
    // 查找文件具体位置
    const filename = Module._resolveFilename(request, parent, isMain);
    // 存在缓存,则不需要再次执行 返回缓存
    const cachedModule = Module._cache[filename];
    if (cachedModule !== undefined) {
      updateChildren(parent, cachedModule, true);
      if (!cachedModule.loaded)
        return getExportsForCircularRequire(cachedModule);
      return cachedModule.exports;
    }
    // 加载node原生模块,原生模块loadNativeModule  
    // 如果有 且能被用户引用 返回 mod.exports(这包括node模块的编译创建module对象,将模块运行结果保存在module对象上)
    const mod = loadNativeModule(filename, request);
    if (mod && mod.canBeRequiredByUsers) return mod.exports;
  
    // 创建一个模块
    // Don't call updateChildren(), Module constructor already does.
    const module = new Module(filename, parent);
  
    if (isMain) {
      process.mainModule = module;
      module.id = '.';
    }
    // 缓存模块
    Module._cache[filename] = module;
    if (parent !== undefined) {
      relativeResolveCache[relResolveCacheIdentifier] = filename;
    }
    // 加载执行新的模块
    module.load(filename);  
      
    return module.exports;
  };
node 缓存的是编译和执行后的对象
相同:
node模块和非node模块经历的过程都是,有执行后的缓存对象,返回缓存对象
没有执行后的缓存对象,创建module对象,执行模块,存储执行后得到的对象,返回执行后的结果exports
不同:
缓存对象不同
加载模块文件方式不同

2.2 node 源码目录

大概源码结构:(只标注了部分感兴趣的)

我们可以看到 node 库的目录,其中:
deps:包含了node所依赖的库,如v8,libuv,zlib 等,
lib:包含了用 javascript 定义的函数和模块(可能会通过internalBinding调用c++模块,c++ 模块实现在目录src 下),
src:包括了lib 库对应的C++实现,其中很多 built-in(C++实现) 模块都在这里


会有所困惑,js跟c++ 之间的相互调用?
Node.js主要包括这几个部分,Node Standard Library,Node Bindings,V8,Libuv,架构图如下:

Node Bindings: 是沟通JS 和 C++的桥梁,将V8 引擎暴露的c++ 接口转换成JS API

V8: JavaScript的引擎,提供JavaScript运行环境

c++ 模块的引用大概流程

C++ 和 JS 交互 参考文章:(感兴趣可以了解一下)

2.3 node 模块分类

// This file creates the internal module & binding loaders used by built-in
// modules. In contrast, user land modules are loaded using
// lib/internal/modules/cjs/loader.js (CommonJS Modules) or
// lib/internal/modules/esm/* (ES Modules).
//
// This file is compiled and run by node.cc before bootstrap/node.js
// was called, therefore the loaders are bootstraped before we start to
// actually bootstrap Node.js. It creates the following objects:
node.cc编译和运行的node/lib/internal/bootstrap/loaders.js 文件的时候会,创建内部模块, 绑定内置模块使用的加载程序。
而用户的模块加载运行依靠lib/internal/modules/cjs/loader.js 或者 lib/internal/modules/esm/*
所以node的模块大致分为两类:
  • node的核心模块
    • node的核心模块js实现
    • node核心模块c++实现,js包裹调用c++模块
  • 第三方模块,或者用户自己编写模块
    • JavaScript 模块,我们开发写的JavaScript 模
    • JSON 模块,一个 JSON 文件
    • C/C++ 扩展模块,使用 C/C++ 编写,编译后后缀名为 .node(感兴趣可以了解动态链接库)

2.3.1 node的核心模块

⑴ C++ binding loaders:(对c++核心模块的引入,暴露的c++ 接口)
process.binding():旧版C ++绑定加载程序,可从用户空间访问,因为它是附加到全局流程对象的对象。这些C ++绑定是使用NODE_BUILTIN_MODULE_CONTEXT_AWARE()创建的,并且其nm_flags设置为NM_F_BUILTIN。我们无法确保这些绑定的稳定性,但是仍然必须时时解决由它们引起的兼容性问题。
process._linkedBinding():在应用程序中添加额外的其他C ++绑定。可以使用带有标志NM_F_LINKED 的 NODE_MODULE_CONTEXT_AWARE_CPP()创建这些C ++绑定。
internalBinding():私有内部C ++绑定加载程序,(除非通过`require('internal / test / binding')`,否则无法从用户区域访问)。 这些C ++绑定是使用NODE_MODULE_CONTEXT_AWARE_INTERNAL()创建的,其nm_flags设置为NM_F_INTERNAL。
⑵Internal JavaScript module loader:
该模块是用于加载 lib/**/*.js 和 deps/**/*.js 中的JavaScript核心模块的最小模块系统。
所有核心模块都通过由 js2c.py 生成的 node_javascript.cc 编译成 Node 二进制文件,这样可以更快地加载它们,而不需要I/O成本。
此类使lib / internal / *,deps / internal / *模块和internalBinding()在默认情况下对核心模块可,并允许核心模块通require('internal / bootstrap / loaders')来引用自身,即使此文件不是以CommonJS风格编写的。
核心模块的加载大概如下:(internalBinding是process.binding的替代可以简单这样理解)

Process.binding / InternalBinding 实际上是C++函数,是用于将Node标准库中C++端和Javascript端连接起来的桥梁。

2.3.2 node的非 核心模块

  • JavaScript 模块,我们开发写的JavaScript 模(或着第三方模块)
  • JSON 模块,一个 JSON 文件
  • C/C++ 扩展模块,使用 C/C++ 编写,编译后后缀名为 .node(感兴趣可以了解动态链接库)

此类模块的大概加载流程:


路径分析

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

是否有缓存

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

创建module对象

  const module = new Module(filename, parent);
// 缓存 module 对象
  Module._cache[filename] = module;

文件定位根据后缀编译执行

// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
  if (experimentalModules && filename.endsWith('.js')) {
    const pkg = readPackageScope(filename);
    if (pkg && pkg.type === 'module') {
      throw new ERR_REQUIRE_ESM(filename);
    }
  }
  const content = fs.readFileSync(filename, 'utf8');
  module._compile(stripBOM(content), filename);
};


// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
  const content = fs.readFileSync(filename, 'utf8');

  if (manifest) {
    const moduleURL = pathToFileURL(filename);
    manifest.assertIntegrity(moduleURL, content);
  }

  try {
    module.exports = JSON.parse(stripBOM(content));
  } catch (err) {
    err.message = filename + ': ' + err.message;
    throw err;
  }
};


// Native extension for .node
Module._extensions['.node'] = function(module, filename) {
  if (manifest) {
    const content = fs.readFileSync(filename);
    const moduleURL = pathToFileURL(filename);
    manifest.assertIntegrity(moduleURL, content);
  }
  // Be aware this doesn't use `content`
  return process.dlopen(module, path.toNamespacedPath(filename));
};

返回module.exports 对象。

3.总结

node 的模块运行机制简单了解。 涉及大概流程,略过的底层系统区别。

文章整理了相关资料,记录了部分实践和自己的理解,理解不准确之处,还请教正。欢迎一起讨论学习。

参考资料: