前言
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中不存在再读取文件的思想为主。