前言

require是Node.js中非常重要的一个方法,我们可以使用它在文件中加载其它文件中定义的模块。本文主要分析了Node.js中使用require加载模块的主要实现过程,为了方便理解,对其源码进行了一定的删减,去除了部分诸如debug、加载实验性模块的代码。文中选用了当前最新的Node LTS版本(2019年2月) 10.15.1源码。

源码分析

在前几个版本中,require的实现主要位于lib/module.js中,但自从9.11.0开始对其进行了重构,具体的实现移至lib/internal/modules/cjs/loader.js文件中。

require方法即Module模块的require方法,它主要做了参数的检查并调用Module模块的_load方法。Module模块中存在_load及load两个方法,需要进行区分。

Module.prototype.require = function(id) {
  return Module._load(id, this, false);
};

Module模块的_load方法的主要内容为检查cache中是否已经存在该模块,若已存在则直接从cache中获取;若cache中不存在,则判断该模块是否为Native模块,并进行加载。当模块为Native模块,则调用NativeModule.require方法加载,否则将创建一个新的Module实例,调用tryModuleLoad方法加载该模块内容并将其保存至cache中。在tryModuleLoad方法中,将会调用Module模块的load方法加载模块,并根据是否存在错误进行相应的处理。

我们将在加载Native模块章节中具体分析Native模块的加载过程,本节中继续以普通的模块为主。

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

  // 检查cache中是否已存在该模块,若已存在则直接从cache中读取
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    updateChildren(parent, cachedModule, true);
    return cachedModule.exports;
  }

  // 加载Native模块
  if (NativeModule.nonInternalExists(filename)) {
    return NativeModule.require(filename);
  }

  // 创建一个新的Module,加载并保存至cache中
  var module = new Module(filename, parent);

  Module._cache[filename] = module;

  tryModuleLoad(module, filename);

  return module.exports;
};

function tryModuleLoad(module, filename) {
  var threw = true;
  try {
    module.load(filename);
    threw = false;
  } finally {
    if (threw) {
      delete Module._cache[filename];
    }
  }
}

Module模块的load方法主要根据需要加载的文件的扩展名判断并选取对应的加载方法,除此以外它还包括了部分实验性模块的处理,本文中不作为主要内容所以进行了省略。文件的加载方法存放在Module模块的_extensions数据中,在该文件中主要定义了.js.json.node.mjs四种文件的加载方法。.js.json文件的加载皆为调用fs模块的readFileSync方法读取文件内容后进行对应的处理,而对于.node文件,则使用了process的dlopen方法加载C++扩展。

Module.prototype.load = function(filename) {
  this.filename = filename;
  this.paths = Module._nodeModulePaths(path.dirname(filename));

  var extension = path.extname(filename) || '.js';
  if (!Module._extensions[extension]) extension = '.js';
  Module._extensions[extension](this, filename);
  this.loaded = true;

  // ...
};

Module._extensions['.js'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  module._compile(stripBOM(content), filename);
};

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

在加载.js文件时,读取文件内容后将调用Module模块的_compile方法,对读取到的文件内容进行一定的处理,然后使用vm.runInThisContext方法运行脚本,本文中将不再深入探讨VM模块的实现。Module模块的_compile方法还进行例如设置断点等处理,有兴趣的朋友可自己根据源码进行深入的学习。

Module.prototype._compile = function(content, filename) {
  content = stripShebang(content);

  var wrapper = Module.wrap(content);

  var compiledWrapper = vm.runInThisContext(wrapper, {
    filename: filename,
    lineOffset: 0,
    displayErrors: true
  });

  // ...
};

Module.wrap = function(script) {
  return Module.wrapper[0] + script + Module.wrapper[1];
};

Module.wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
];

至此,便完成了使用require加载模块的过程,其主要内容就是在判断缓存后读取文件内容,然后使用VM模块运行。接下来我们将继续看看Native模块的加载。

读取Native模块

Native模块的加载位于lib/internal/bootstrap/loaders.js文件中。我们在加载Native模块时,将调用NativeModule模块的require方法。它的主要过程也和上文中普通模块的过程相似,先判断是否已经cache,若未cache则调用NativeModule模块的compile方法加载模块并进行cache。

NativeModule.require = function(id) {
  if (id === loaderId) {
    return loaderExports;
  }

  const cached = NativeModule.getCached(id);
  if (cached && (cached.loaded || cached.loading)) {
    return cached.exports;
  }

  if (!NativeModule.exists(id)) {
    // ...
  }

  moduleLoadList.push(`NativeModule ${id}`);

  const nativeModule = new NativeModule(id);

  nativeModule.cache();
  nativeModule.compile();

  return nativeModule.exports;
};

本文也不再继续深入探索Native模块的加载过程,有兴趣的朋友可根据源码进行深入的学习。

加载JSON

在Node.js中,可以直接使用require读取JSON文件的内容,其实现与读取.js文件时的区别为读取后直接调用了JSON.parse方法。

Module._extensions['.json'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  try {
    module.exports = JSON.parse(stripBOM(content));
  } catch (err) {
    err.message = filename + ': ' + err.message;
    throw err;
  }
};

结束语

Node几个大版本中,require的具体实现都有或多或少的改变,但总体仍是以先从cache中获取,若cache中不存在再读取文件的思想为主。