Node.js源碼解析-require背后
歡迎來我的博客閱讀:《Node.js源碼解析-require背后》
在編寫 Node.js 應用的過程中,我們或多或少的都寫過類似 const xxx = require('xxx')
的代碼,其作用是引入模塊。不知大家有沒有想過,這段代碼是如何確定我們要引入的模塊?又是以怎樣的上下文來執行模塊代碼的呢?
讓我們來翻開 Node.js 源碼,先找到 lib/module.js
中的 Module.prototype.require()
函數
// lib/module.js
Module.prototype.require = function(path) {
assert(path, 'missing path');
assert(typeof path === 'string', 'path must be a string');
return Module._load(path, this, /* isMain */ false);
};
// 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.require()` with the
// filename and return the result.
// 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) {
if (parent) {
debug('Module._load REQUEST %s parent: %s', request, parent.id);
}
var filename = Module._resolveFilename(request, parent, isMain);
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}
if (NativeModule.nonInternalExists(filename)) {
debug('load native module %s', request);
return NativeModule.require(filename);
}
var module = new Module(filename, parent);
if (isMain) {
process.mainModule = module;
module.id = '.';
}
Module._cache[filename] = module;
tryModuleLoad(module, filename);
return module.exports;
};
Module.prototype.require()
對傳入的 path 簡單斷言后調用 Module._load()
來導入模塊
Module._load()
的執行思路如下:
- 查找模塊:
Module._resolveFilename()
- 存在緩存: 返回
cachedModule.exports
- 是內置模塊: 見 NativeModule.require()
- 加載模塊:
tryModuleLoad()
因此,Module.prototype.require()
的源碼可以分為兩大塊: 查找模塊和加載模塊
查找模塊
查找模塊的關鍵在于定位模塊的具體路徑,這個功能由 Module._resolveFilename()
函數實現
// lib/module.js
Module._resolveFilename = function(request, parent, isMain) {
if (NativeModule.nonInternalExists(request)) {
return request;
}
// 可能存在要加載模塊的目錄
var paths = Module._resolveLookupPaths(request, parent, true);
// 具體的模塊路徑
var filename = Module._findPath(request, paths, isMain);
if (!filename) {
var err = new Error(`Cannot find module '${request}'`);
err.code = 'MODULE_NOT_FOUND';
throw err;
}
return filename;
};
Module._resolveFilename()
函數先調用 Module._resolveLookupPaths()
計算出模塊可能存在的目錄,然后調用 Module._findPath()
得到模塊路徑
Module._resolveLookupPaths
通過調用 Module._resolveLookupPaths()
函數可以計算出模塊可能存在的目錄,在調用時存在三種情況:
- 直接
require('xxx')
: 需要遞歸查詢路徑上的node_modules
目錄和全局node_modules
目錄 - 通過
Module.runMain()
或--eval
參數: 返回執行命令行的目錄 - 使用相對 / 絕對路徑導入: 這時,直接返回父模塊目錄即可
// lib/module.js
Module._resolveLookupPaths = function(request, parent, newReturn) {
if (NativeModule.nonInternalExists(request)) {
debug('looking for %j in []', request);
return (newReturn ? null : [request, []]);
}
// 類似 require('xxx')
if (request.length < 2 ||
request.charCodeAt(0) !== 46 || // 非 . 開頭
(request.charCodeAt(1) !== 46 // 非 .. 開頭
&& request.charCodeAt(1) !== 47)) { // 非 / 開頭
var paths = modulePaths;
if (parent) {
if (!parent.paths)
paths = parent.paths = [];
else
paths = parent.paths.concat(paths);
}
// Maintain backwards compat with certain broken uses of require('.')
// by putting the module's directory in front of the lookup paths.
if (request === '.') {
if (parent && parent.filename) {
paths.unshift(path.dirname(parent.filename));
} else {
paths.unshift(path.resolve(request));
}
}
debug('looking for %j in %j', request, paths);
return (newReturn ? (paths.length > 0 ? paths : null) : [request, paths]);
}
// 執行 Module.runMain() 進入,此時 request 是絕對路徑
// with --eval, parent.id is not set and parent.filename is null
if (!parent || !parent.id || !parent.filename) {
// make require('./path/to/foo') work - normally the path is taken
// from realpath(__filename) but with eval there is no filename
var mainPaths = ['.'].concat(Module._nodeModulePaths('.'), modulePaths);
debug('looking for %j in %j', request, mainPaths);
return (newReturn ? mainPaths : [request, mainPaths]);
}
// ...
var parentDir = [path.dirname(parent.filename)];
debug('looking for %j in %j', id, parentDir);
return (newReturn ? parentDir : [id, parentDir]);
};
Module._findPath
使用 Module._resolveLookupPaths()
函數找到模塊可能存在的目錄后,調用 Module._findPath()
函數,遞歸查找模塊
Module._findPath()
函數在查找模塊時,存在以下幾種情況:
-
require
的是文件 ==>.js
/.json
/.node
-
require
的文件夾 ==> 找index.js
/index.json
/index.node
-
require
的包 ==> 找package.json
// lib/module.js
Module._findPath = function(request, paths, isMain) {
if (path.isAbsolute(request)) {
paths = [''];
} else if (!paths || paths.length === 0) {
return false;
}
// 計算 cacheKey
// 對于同一模塊,每個目錄的 cacheKey 不同
var cacheKey = request + '\x00' +
(paths.length === 1 ? paths[0] : paths.join('\x00'));
var entry = Module._pathCache[cacheKey];
// 有緩存就走緩存
if (entry)
return entry;
var exts;
var trailingSlash = request.length > 0 &&
request.charCodeAt(request.length - 1) === 47; // /
// For each path
for (var i = 0; i < paths.length; i++) {
const curPath = paths[i];
// 不存在就跳過
if (curPath && stat(curPath) < 1) continue;
var basePath = path.resolve(curPath, request);
var filename;
var rc = stat(basePath);
if (!trailingSlash) {
if (rc === 0) { // 文件
if (preserveSymlinks && !isMain) {
filename = path.resolve(basePath);
} else {
filename = toRealPath(basePath);
}
} else if (rc === 1) { // 目錄或 package
if (exts === undefined)
exts = Object.keys(Module._extensions);
// 如果是目錄,則 filename 為 false
filename = tryPackage(basePath, exts, isMain);
}
// 對應是文件但未給出文件后綴的情況
if (!filename) {
// try it with each of the extensions
if (exts === undefined)
exts = Object.keys(Module._extensions);
filename = tryExtensions(basePath, exts, isMain);
}
}
if (!filename && rc === 1) { // 目錄或 package
if (exts === undefined)
exts = Object.keys(Module._extensions);
// 如果是目錄,則 filename 為 false
filename = tryPackage(basePath, exts, isMain);
}
if (!filename && rc === 1) { // 目錄
// try it with each of the extensions at "index"
if (exts === undefined)
exts = Object.keys(Module._extensions);
filename = tryExtensions(path.resolve(basePath, 'index'), exts, isMain);
}
if (filename) {
// Warn once if '.' resolved outside the module dir
if (request === '.' && i > 0) {
if (!warned) {
warned = true;
process.emitWarning(
'warning: require(\'.\') resolved outside the package ' +
'directory. This functionality is deprecated and will be removed ' +
'soon.',
'DeprecationWarning', 'DEP0019');
}
}
Module._pathCache[cacheKey] = filename;
return filename;
}
}
return false;
};
上面的 for 循環內有許多重復代碼,可以優化為:
var exts = Object.keys(Module._extensions);
var trailingSlash = request.length > 0 &&
request.charCodeAt(request.length - 1) === 47; // /
for (var i = 0; i < paths.length; i++) {
const curPath = paths[i];
// 不存在就跳過
if (curPath && stat(curPath) < 1) continue;
var basePath = path.resolve(curPath, request);
var filename;
var rc = stat(basePath);
if (!trailingSlash && rc !== 1) { // 文件或啥也沒有
if (rc === 0) { // 文件
if (preserveSymlinks && !isMain) {
filename = path.resolve(basePath);
} else {
filename = toRealPath(basePath);
}
} else { // 對應是文件但未給出文件后綴的情況
filename = tryExtensions(basePath, exts, isMain);
}
}
if(!filename && rc === 1){ // 目錄或 package
filename = tryPackage(basePath, exts, isMain);
// 如果是目錄,則 filename 為 false
if (!filename) {
// try it with each of the extensions at "index"
filename = tryExtensions(path.resolve(basePath, 'index'), exts, isMain);
}
}
if (filename) {
// Warn once if '.' resolved outside the module dir
if (request === '.' && i > 0) {
if (!warned) {
warned = true;
process.emitWarning(
'warning: require(\'.\') resolved outside the package ' +
'directory. This functionality is deprecated and will be removed ' +
'soon.',
'DeprecationWarning', 'DEP0019');
}
}
Module._pathCache[cacheKey] = filename;
return filename;
}
}
Module._findPath()
函數內部依賴下面幾個方法:
// lib/module.js
function tryPackage(requestPath, exts, isMain) {
// 讀取目錄下的 package.json 并返回 package.main,沒有則返回 false
var pkg = readPackage(requestPath);
if (!pkg) return false;
var filename = path.resolve(requestPath, pkg);
return tryFile(filename, isMain) || // package.main 有后綴
tryExtensions(filename, exts, isMain) || // package.main 沒有后綴
tryExtensions(path.resolve(filename, 'index'), exts, isMain); // package.main 不存在則默認加載 index.js / index.json / index.node
}
function readPackage(requestPath) {
const entry = packageMainCache[requestPath];
if (entry)
return entry;
const jsonPath = path.resolve(requestPath, 'package.json');
const json = internalModuleReadFile(path._makeLong(jsonPath));
// 沒有 package.json,說明不是一個包
if (json === undefined) {
return false;
}
try {
var pkg = packageMainCache[requestPath] = JSON.parse(json).main;
} catch (e) {
e.path = jsonPath;
e.message = 'Error parsing ' + jsonPath + ': ' + e.message;
throw e;
}
return pkg;
}
// given a path check a the file exists with any of the set extensions
function tryExtensions(p, exts, isMain) {
for (var i = 0; i < exts.length; i++) {
const filename = tryFile(p + exts[i], isMain);
if (filename) {
return filename;
}
}
return false;
}
// check if the file exists and is not a directory
// if using --preserve-symlinks and isMain is false,
// keep symlinks intact, otherwise resolve to the
// absolute realpath.
function tryFile(requestPath, isMain) {
const rc = stat(requestPath);
if (preserveSymlinks && !isMain) {
return rc === 0 && path.resolve(requestPath);
}
return rc === 0 && toRealPath(requestPath);
}
function toRealPath(requestPath) {
return fs.realpathSync(requestPath, {
[internalFS.realpathCacheKey]: realpathCache
});
}
通過以上步驟,我們找到了對應的模塊文件,下面開始加載模塊
加載模塊
通過 Module._resolveFilename()
函數找到具體的模塊文件路徑后,就可以開始加載模塊了
Module._load()
調用 tryModuleLoad()
函數來加載模塊。tryModuleLoad()
則將 module.load()
函數( 真正加載模塊的函數 )包裹在一個 try-finally 塊中
// lib/module.js
function tryModuleLoad(module, filename) {
var threw = true;
try {
module.load(filename);
threw = false;
} finally {
if (threw) {
delete Module._cache[filename];
}
}
}
Module.prototype.load()
對于 Module.prototype.load()
函數來說,模塊文件有三種類型:
-
.js
: 讀取文件然后調用module._compile()
編譯執行,這是默認情況 -
.json
: 作為json
文件讀取 -
.node
: 直接執行編譯后的二進制文件
// lib/module.js
// Given a file name, pass it to the proper extension handler.
Module.prototype.load = function(filename) {
debug('load %j for module %j', filename, this.id);
assert(!this.loaded);
this.filename = filename;
// 全局 node_modules 和文件路徑上所有的 node_modules
this.paths = Module._nodeModulePaths(path.dirname(filename));
// 通過文件擴展名確定加載方式,默認采用 .js 的方式加載
var extension = path.extname(filename) || '.js';
if (!Module._extensions[extension]) extension = '.js';
Module._extensions[extension](this, filename);
this.loaded = true;
};
// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8');
module._compile(internalModule.stripBOM(content), filename);
};
// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8');
try {
module.exports = JSON.parse(internalModule.stripBOM(content));
} catch (err) {
err.message = filename + ': ' + err.message;
throw err;
}
};
//Native extension for .node
Module._extensions['.node'] = function(module, filename) {
return process.dlopen(module, path._makeLong(filename));
};
// Run the file contents in the correct scope or sandbox. Expose
// the correct helper variables (require, module, exports) to
// the file.
// Returns exception, if any.
Module.prototype._compile = function(content, filename) {
content = internalModule.stripShebang(content);
// 使用下面這個結構包裹
// (function (exports, require, module, __filename, __dirname) {
// ...
// });
var wrapper = Module.wrap(content);
// 在沙箱內生成函數對象
var compiledWrapper = vm.runInThisContext(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true
});
// ...
var dirname = path.dirname(filename);
var require = internalModule.makeRequireFunction(this);
var depth = internalModule.requireDepth;
if (depth === 0) stat.cache = new Map();
var result;
if (inspectorWrapper) {
result = inspectorWrapper(compiledWrapper, this.exports, this.exports,
require, this, filename, dirname);
} else {
result = compiledWrapper.call(this.exports, this.exports, require, this,
filename, dirname);
}
if (depth === 0) stat.cache = null;
return result;
};
從上面的代碼可以看出,執行模塊時,模塊中的 require,不是 module.js
中的 require,而是由 internalModule.makeRequireFunction()
生成的一個新的函數對象,其隱藏了 module 的實現細節,方便使用
// lib/internal/module.js
// Invoke with makeRequireFunction(module) where |module| is the Module object
// to use as the context for the require() function.
function makeRequireFunction(mod) {
const Module = mod.constructor;
function require(path) {
try {
exports.requireDepth += 1;
return mod.require(path);
} finally {
exports.requireDepth -= 1;
}
}
function resolve(request) {
return Module._resolveFilename(request, mod);
}
require.resolve = resolve;
require.main = process.mainModule;
// Enable support to add extra extension types.
require.extensions = Module._extensions;
require.cache = Module._cache;
return require;
}
各個模塊文件中 require
的區別:
- 內置模塊(
module.js
/fs.js
等 ): 對應NativeModule.require
函數,僅供 node 內部使用 - 第三方模塊: 對應
internalModule.makeRequireFunction()
函數的執行結果,底層依賴Module.prototype.require()
,遵循 CommonJS 規范
總結
當執行 const xxx = require('xxx')
這段代碼時
- 先根據
'xxx'
和模塊所在目錄得出被 require 的模塊可能存在的目錄 - Module._resolveLookupPaths - 再根據
'xxx'
和 1 的結果得出被 require 的模塊的文件路徑 - Module._findPath - 然后根據其拓展名確定加載方式 - Module.prototype.load
- 最后將
module.exports
導出
參考: