Node模塊加載機制,一文帶你徹底理解
一.require()時發(fā)生了什么?
Node.js 中,模塊加載過程分為 5 步:

路徑解析(Resolution):根據(jù)模塊標識找出對應模塊(入口)文件的絕對路徑
加載(Loading):如果是 JSON 或 JS 文件,就把文件內(nèi)容讀入內(nèi)存。如果是內(nèi)置的原生模塊,將其共享庫動態(tài)鏈接到當前 Node.js 進程
包裝(Wrapping):將文件內(nèi)容(JS 代碼)包進一個函數(shù),建立模塊作用域,
exports, require, module等作為參數(shù)注入執(zhí)行(Evaluation):傳入?yún)?shù),執(zhí)行包裝得到的函數(shù)
緩存(Caching):函數(shù)執(zhí)行完畢后,將
module緩存起來,并把module.exports作為require()的返回值返回
其中,模塊標識(Module Identifiers)就是傳入require(id)的第一個字符串參數(shù)id,例如require('./myModule')中的'./myModule',無需指定后綴名(但帶上也無礙)
對于.、..、/開頭的文件路徑,嘗試當做文件、目錄來匹配,具體過程如下:
若路徑存在并且是個文件,就當做 JS 代碼來加載(無論文件后綴名是什么,
require(./myModule.abcd)完全正確)若不存在,依次嘗試拼上
.js、.json、.node(Node.js 支持的二進制擴展)后綴名如果路徑存在并且是個文件夾,就在該目錄下找
package.json,取其main字段,并加載指定的模塊(相當于一次重定向)如果沒有
package.json,就依次嘗試index.js、index.json、index.node
對于模塊標識不是文件路徑的,先看是不是 Node.js 原生模塊(fs、path等)。如果不是,就從當前目錄開始,逐級向上在各個node_modules下找,一直找到頂層的/node_modules,以及一些全局目錄:
NODE_PATH環(huán)境變量中指定的位置默認的全局目錄:
$HOME/.node_modules、$HOME/.node_libraries和$PREFIX/lib/node
P.S.關(guān)于全局目錄的更多信息,見Loading from the global folders
找到模塊文件后,讀取內(nèi)容,并包一層函數(shù):
(function(exports, require, module, __filename, __dirname) {
// Module code actually lives in here
});
(摘自The module wrapper)
執(zhí)行時從外部注入這些模塊變量(exports, require, module, __filename, __dirname),模塊導出的東西通過module.exports帶出來,并將整個module對象緩存起來,最后返回require()結(jié)果
循環(huán)依賴
特殊的,模塊之間可能會出現(xiàn)循環(huán)依賴,對此,Node.js 的處理策略非常簡單:
// module1.js
exports.a = 1;
require('./module2');
exports.b = 2;
exports.c = 3;
// module2.js
const module1 = require('./module1');
console.log('module1 is partially loaded here', module1);
module1.js執(zhí)行中引用了module2.js,module2又引了module1,此時module1尚未加載完(exports.b = 2; exports.c = 3;還沒執(zhí)行)。而在 Node.js 里,只加載了一部分的模塊也可以正常引用:
When there are circular require() calls, a module might not have finished executing when it is returned.
所以module1.js執(zhí)行結(jié)果是:
module1 is partially loaded here { a: 1 }
P.S.關(guān)于循環(huán)引用的更多信息,見Cycles
二.Node.js 內(nèi)部是怎么實現(xiàn)的?
實現(xiàn)上,模塊加載的絕大多數(shù)工作都是由module模塊來完成的:
const Module = require('module');
console.log(Module);
Module是個函數(shù)/類:
function Module(id = '', parent) {
this.id = id;
this.path = path.dirname(id);
// 即module.exports
this.exports = {};
this.parent = parent;
updateChildren(parent, this, false);
this.filename = null;
this.loaded = false;
this.children = [];
}
每加載一個模塊都創(chuàng)建一個Module實例,模塊文件執(zhí)行完后,該實例仍然保留,模塊導出的東西依附于Module實例存在
模塊加載的所有工作都是由module原生模塊來完成的,包括Module._load、Module.prototype._compile
Module._load
Module._load()負責加載新模塊、管理緩存,具體如下:
Module._load = function(request, parent, isMain) {
// 0.解析模塊路徑
const filename = Module._resolveFilename(request, parent, isMain);
// 1.優(yōu)先找緩存 Module._cache
const cachedModule = Module._cache[filename];
// 2.嘗試匹配原生模塊
const mod = loadNativeModule(filename, request, experimentalModules);
// 3.未命中緩存,也沒匹配到原生模塊,就創(chuàng)建一個新的 Module 實例
const module = new Module(filename, parent);
// 4.把新實例緩存起來
Module._cache[filename] = module;
// 5.加載模塊
module.load(filename);
// 6.如果加載/執(zhí)行出錯了,就刪掉緩存
if (threw) {
delete Module._cache[filename];
}
// 7.返回 module.exports
return module.exports;
};
Module.prototype.load = function(filename) {
// 0.判定模塊類型
const extension = findLongestRegisteredExtension(filename);
// 1.按類型加載模塊內(nèi)容
Module._extensions[extension](this, filename);
};
支持的類型有.js、.json、.node3 種:
// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
// 1.讀取JS文件內(nèi)容
const content = fs.readFileSync(filename, 'utf8');
// 2.包裝、執(zhí)行
module._compile(content, filename);
};
// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
// 1.讀取JSON文件內(nèi)容
const content = fs.readFileSync(filename, 'utf8');
// 2.直接JSON.parse()完事
module.exports = JSONParse(stripBOM(content));
};
// Native extension for .node
Module._extensions['.node'] = function(module, filename) {
// 動態(tài)加載共享庫
return process.dlopen(module, path.toNamespacedPath(filename));
};
P.S.process.dlopen具體見process.dlopen(module, filename[, flags])
Module.prototype._compile
Module.prototype._compile = function(content, filename) {
// 1.包一層函數(shù)
const compiledWrapper = wrapSafe(filename, content, this);
// 2.把要注入的參數(shù)準備好
const dirname = path.dirname(filename);
const require = makeRequireFunction(this, redirects);
const exports = this.exports;
const thisValue = exports;
const module = this;
// 3.注入?yún)?shù)、執(zhí)行
compiledWrapper.call(thisValue, exports, require, module, filename, dirname);
};
包裝部分的實現(xiàn)如下:
function wrapSafe(filename, content, cjsModuleInstance) {
let compiled = compileFunction(
content,
filename,
0,
0,
undefined,
false,
undefined,
[],
[
'exports',
'require',
'module',
'__filename',
'__dirname',
]
);
return compiled.function;
}
P.S.模塊加載的完整實現(xiàn)見node/lib/internal/modules/cjs/loader.js
三.知道這些有什么用?
知道了模塊的加載機制,在一些需要擴展篡改加載邏輯的場景很有用,比如用來實現(xiàn)虛擬模塊、模塊別名等
虛擬模塊
比如,VS Code 插件通過require('vscode')來訪問插件 API:
// The module 'vscode' contains the VS Code extensibility API
import * as vscode from 'vscode';
而vscode模塊實際上是不存在的,是個運行時擴展出來的虛擬模塊:
// ref: src/vs/workbench/api/node/extHost.api.impl.ts
function defineAPI() {
const node_module = <any>require.__$__nodeRequire('module');
const original = node_module._load;
// 1.劫持 Module._load
node_module._load = function load(request, parent, isMain) {
if (request !== 'vscode') {
return original.apply(this, arguments);
}
// 2.注入虛擬模塊 vscode
// get extension id from filename and api for extension
const ext = extensionPaths.findSubstr(parent.filename);
let apiImpl = extApiImpl.get(ext.id);
if (!apiImpl) {
apiImpl = factory(ext);
extApiImpl.set(ext.id, apiImpl);
}
return apiImpl;
};
}
具體見API 注入機制及插件啟動流程_VSCode 插件開發(fā)筆記 2,這里不再贅述
模塊別名
類似的,可以通過重寫Module._resolveFilename來實現(xiàn)模塊別名,比如把proj/src中的@lib/my-module模塊引用映射到proj/lib/my-module:
// src/index.js
require('./patchModule');
const myModule = require('@lib/my-module');
console.log(myModule);
patchModule具體實現(xiàn)如下:
const Module = require('module');
const path = require('path');
const _resolveFilename = Module._resolveFilename;
Module._resolveFilename = function(request) {
const args = Array.from(arguments);
// 別名映射
const LIB_PREFIX = '@lib/';
if (request.startsWith(LIB_PREFIX)) {
console.log(request);
request = path.resolve(__dirname, '../' + request.slice(1));
args[0] = request;
console.log(` => ${request}`);
}
return _resolveFilename.apply(null, args);
}
P.S.當然,一般不需要這樣做,可以通過Webpack等構(gòu)建工具來完成
清掉緩存
默認 Node.js 模塊加載過就有緩存,而有些時候可能想要禁掉緩存,強制重新加載一個模塊,比如想要讀取能被用戶頻繁修改的 JS 文件(如webpack.config.js)
此時可以手動刪掉掛在require.cache身上的module.exports緩存:
delete require.cache[require.resolve('./b.js')]
然而,如果b.js還引用了其它外部(非原生)模塊,也需要一并刪除:
const mod = require.cache[require.resolve('./b.js')];
// 把引用樹上所有模塊緩存全都刪掉
(function traverse(mod) {
mod.children.forEach((child) => {
traverse(child);
});
console.log('decache ' + mod.id);
delete require.cache[mod.id];
}(mod));
P.S.或者采用decache模塊
參考資料
Node.js, TC-39, and Modules:以及譯文
The Node.js Way – How
require()Actually WorksRequiring modules in Node.js: Everything you need to know
Deep Dive Into Node.js Module Architecture
node.js require() cache – possible to invalidate?
