做者:肖磊javascript
我的主頁:githubjava
最近一直在用node.js
寫一些相關的工具,對於node.js
的模塊如何去加載,以及所遵循的模塊加載規範的具體細節又是如何並非瞭解。這篇文件也是看過node.js
源碼及部分文章總結而來:node
在es2015
標準之前,js
並無成熟的模塊系統的規範。Node.js
爲了彌補這樣一個缺陷,採用了CommonJS
規範中所定義的模塊規範,它包括:git
1.requiregithub
require
是一個函數,它接收一個模塊的標識符,用以引用其餘模塊暴露出來的API
。json
2.module context緩存
module context
規定了一個模塊當中,存在一個require變量,它聽從上面對於這個require
函數的定義,一個exports
對象,模塊若是須要向外暴露API,即在一個exports
的對象上添加屬性。以及一個module object
。app
3.module Identifierside
module Identifiers
定義了require
函數所接受的參數規則,好比說必須是小駝峯命名的字符串,能夠沒有文件後綴名,.
或者..
代表文件路徑是相對路徑等等。函數
具體關於commonJS
中定義的module
規範,能夠參見wiki文檔
在咱們的node.js
程序當中,咱們使用require
這個看起來是全局(後面會解釋爲何看起來是全局的)的方法去加載其餘模塊。
const util = require('./util')
複製代碼
首先咱們來看下關於這個方法,node.js
內部是如何定義的:
Module.prototype.require = function () {
assert(path, 'missing path');
assert(typeof path === 'string', 'path must be a string');
// 其實是調用Module._load方法
return Module._load(path, this, /* isMain */ false);
}
Module._load = function (request, parent, isMain) {
.....
// 獲取文件名
var filename = Module._resolveFilename(request, parent, isMain);
// _cache緩存的模塊
var cachedModule = Module._cache[filename];
if (cachedModule) {
updateChildren(parent, cachedModule, true);
return cachedModule.exports;
}
// 若是是nativeModule模塊
if (NativeModule.nonInternalExists(filename)) {
debug('load native module %s', request);
return NativeModule.require(filename);
}
// Don't call updateChildren(), Module constructor already does.
// 初始化一個新的module
var module = new Module(filename, parent);
if (isMain) {
process.mainModule = module;
module.id = '.';
}
// 加載模塊前,就將這個模塊緩存起來。注意node.js的模塊加載系統是如何避免循環依賴的
Module._cache[filename] = module;
// 加載module
tryModuleLoad(module, filename);
// 將module.exports導出的內容返回
return module.exports;
}
複製代碼
Module._load
方法是一個內部的方法,主要是:
接下來咱們來看下node.js
是如何根據傳入的模塊路徑字符串來查找對應的模塊的:
Module._resolveFilename = function (request, parent, isMain, options) {
if (NativeModule.nonInternalExists(request)) {
return request;
}
var paths;
if (typeof options === 'object' && options !== null &&
Array.isArray(options.paths)) {
...
} else {
// 獲取模塊的大體路徑 [parentDir] | [id, [parentDir]]
paths = Module._resolveLookupPaths(request, parent, true);
}
// look up the filename first, since that's the cache key.
// node index.js
// request = index.js
// paths = ['/root/foo/bar/index.js', '/root/foo/bar']
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._resolveLookupPaths
,這個方法會依據父模塊的路徑獲取全部這個模塊可能的路徑:
Module._resolveLookupPaths = function (request, parent, newReturn) {
...
}
複製代碼
這個方法內部有如下幾種狀況的處理:
node xxx
啓動的模塊這個時候node.js
會直接獲取到你這個程序執行路徑,並在這個方法當中返回
require(xxx)
require一個存在於node_modules
中的模塊這個時候會對執行路徑上全部可能存在node_modules
的路徑進行遍歷一遍
require(./)
require一個相對路徑或者絕對路徑的模塊直接返回父路徑
當拿到須要找尋的路徑後,調用Module._findPath
方法去查找對應的文件路徑。
Module._findPath = function (request, paths, isMain) {
if (path.isAbsolute(request)) {
paths = [''];
} else if (!paths || paths.length === 0) {
return false;
}
// \x00 -> null,至關於空字符串
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++) {
// Don't search further if path doesn't exist
const curPath = paths[i]; // 當前路徑
if (curPath && stat(curPath) < 1) continue;
var basePath = path.resolve(curPath, request);
var filename;
// 調用internalModuleStat方法來判斷文件類型
var rc = stat(basePath);
// 若是路徑不以/結尾,那麼多是文件,也多是文件夾
if (!trailingSlash) {
if (rc === 0) { // File. 文件
if (preserveSymlinks && !isMain) {
filename = path.resolve(basePath);
} else {
filename = toRealPath(basePath);
}
} else if (rc === 1) { // Directory. 當提供的路徑是文件夾的狀況下會去這個路徑下找package.json中的main字段對應的模塊的入口文件
if (exts === undefined)
// '.js' '.json' '.node' '.ms'
exts = Object.keys(Module._extensions);
// 獲取pkg內部的main字段對應的值
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); // ${basePath}.(js|json|node)等文件後綴,看是否文件存在
}
}
// 若是路徑以/結尾,那麼就是文件夾
if (!filename && rc === 1) { // Directory.
if (exts === undefined)
exts = Object.keys(Module._extensions);
filename = tryPackage(basePath, exts, isMain) ||
// try it with each of the extensions at "index"
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;
}
function tryPackage(requestPath, exts, isMain) {
var pkg = readPackage(requestPath); // 獲取package.json當中的main字段
if (!pkg) return false;
var filename = path.resolve(requestPath, pkg); // 解析路徑
return tryFile(filename, isMain) || // 直接判斷這個文件是否存在
tryExtensions(filename, exts, isMain) || // 判斷這個分別以js,json,node等後綴結尾的文件是否存在
tryExtensions(path.resolve(filename, 'index'), exts, isMain); // 判斷這個分別以 ${filename}/index.(js|json|node)等後綴結尾的文件是否存在
}
複製代碼
梳理下上面查詢模塊時的一個策略:
require
模塊的時候,傳入的字符串最後一個字符不是/
時:若是是個文件,那麼直接返回這個文件的路徑
若是是個文件夾,那麼會找個這個文件夾下是否有package.json
文件,以及這個文件當中的main
字段對應的路徑(對應源碼當中的方法爲tryPackage
):
.js
,.json
,.node
,.ms
後綴去加載對應文件index.js
,index.json
,index.node
文件若是以上2個方法都沒有找到對應文件路徑,那麼就對文件路徑後添加分別添加.js
,.json
,.node
,.ms
後綴去加載對應的文件(對應源碼當中的方法爲tryExtensions
)
require
模塊的時候,傳入的字符串最後一個字符是/
時,即require
的是一個文件夾時:index.js
,index.json
,index.node
等文件當找到文件的路徑後就調用tryModuleLoad
開始加載模塊了,這個方法內部其實是調用了模塊實例的load
方法:
Module.prototype.load = function () {
...
this.filename = filename;
// 定義module的paths。獲取這個module路徑上全部可能的node_modules路徑
this.paths = Module._nodeModulePaths(path.dirname(filename));
var extension = path.extname(filename) || '.js';
if (!Module._extensions[extension]) extension = '.js';
// 開始load這個文件
Module._extensions[extension](this, filename);
this.loaded = true;
...
}
複製代碼
調用Module._extension
方法去加載不一樣格式的文件,就拿js
文件來講:
Module._extensions['.js'] = function(module, filename) {
// 首先讀取文件的文本內容
var content = fs.readFileSync(filename, 'utf8');
module._compile(internalModule.stripBOM(content), filename);
};
複製代碼
內部調用了Module.prototype._compile
這個方法:
Module.prototype._compile = function (content, filename)) {
content = internalModule.stripShebang(content);
// create wrapper function
// 將源碼的文本包裹一層
var wrapper = Module.wrap(content);
// vm.runInThisContext在一個v8的虛擬機內部執行wrapper後的代碼
var compiledWrapper = vm.runInThisContext(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true
});
var inspectorWrapper = null;
if (process._breakFirstLine && process._eval == null) {
if (!resolvedArgv) {
// we enter the repl if we're not given a filename argument.
if (process.argv[1]) {
resolvedArgv = Module._resolveFilename(process.argv[1], null, false);
} else {
resolvedArgv = 'repl';
}
}
// Set breakpoint on module start
if (filename === resolvedArgv) {
delete process._breakFirstLine;
inspectorWrapper = process.binding('inspector').callAndPauseOnStart;
}
}
var dirname = path.dirname(filename);
// 構造require函數
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 {
// 開始執行這個函數
// 傳入的參數依次是 module.exports / require / module / filename / dirname
result = compiledWrapper.call(this.exports, this.exports, require, this,
filename, dirname);
}
if (depth === 0) stat.cache = null;
return result;
}
Module.wrap = function(script) {
return Module.wrapper[0] + script + Module.wrapper[1];
};
Module.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
複製代碼
Module.wrap
將源碼包裹一層(遵循commonJS
規範)vm
v8虛擬機暴露出來的方法來構造一個新的函數經過源碼發現,Module.wrapper
在對源碼文本進行包裹的時候,傳入了5個參數:
是對於第三個參數module
的exports
屬性的引用
這個require
並不是是Module.prototype.require
方法,而是經過internalModule.makeRequireFunction
從新構造出來的,這個方法內部仍是依賴Module.prototype.require
方法去加載模塊的,同時還對這個require
方法作了一些拓展。
module
對象,若是須要向外暴露API
供其餘模塊來使用,須要在module.exports
屬性上定義
當前文件的絕對路徑
當前文件的父文件夾的絕對路徑
特別注意第一個參數和第三參數的聯繫:第一參數是對於第三個參數的exports
屬性的引用。一旦將某個模塊exports
賦值給另一個新的對象,那麼就斷開了exports
屬性和module.exports
之間的引用關係,同時在其餘模塊當中也沒法引用在當前模塊中經過exports
暴露出去的API
,對於模塊的引用始終是獲取module.exports
屬性。
官方示例:
a.js
console.log('a 開始');
exports.done = false;
const b = require('./b.js');
console.log('在 a 中,b.done = %j', b.done);
exports.done = true;
console.log('a 結束');
複製代碼
b.js
console.log('b 開始');
exports.done = false;
const a = require('./a.js');
console.log('在 b 中,a.done = %j', a.done);
exports.done = true;
console.log('b 結束');
複製代碼
main.js
console.log('main 開始');
const a = require('./a.js');
const b = require('./b.js');
console.log('在 main 中,a.done=%j,b.done=%j', a.done, b.done);
複製代碼
$ node main.js
main 開始
a 開始
b 開始
在 b 中,a.done = false
b 結束
在 a 中,b.done = true
a 結束
在 main 中,a.done=true,b.done=true
複製代碼
在a
模塊加載時,須要加載b
模塊,可是在實際加載a
模塊以前,就已經將a
模塊進行的緩存,具體參見Module._load
方法:
Module._cache[filename] = module;
tryModuleLoad(module, filename);
複製代碼
由於在加載b
模塊的過程當中再次去加載a
模塊的時候,這時是直接從緩存中獲取a
模塊導出的API
,此時exports.done
的屬性仍是false
,未被設置爲true
,只有當b
模塊被徹底加載後,a
模塊exports
屬性才被設置爲true
。