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