您好,登錄后才能下訂單哦!
這篇文章主要介紹“Node.js模塊系統(tǒng)源碼分析”,在日常操作中,相信很多人在Node.js模塊系統(tǒng)源碼分析問(wèn)題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”Node.js模塊系統(tǒng)源碼分析”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!
CommonJS 規(guī)范
Node 最初遵循 CommonJS 規(guī)范來(lái)實(shí)現(xiàn)自己的模塊系統(tǒng),同時(shí)做了一部分區(qū)別于規(guī)范的定制。CommonJS 規(guī)范是為了解決 JavaScript 的作用域問(wèn)題而定義的模塊形式,它可以使每個(gè)模塊在它自身的命名空間中執(zhí)行。
該規(guī)范強(qiáng)調(diào)模塊必須通過(guò) module.exports 導(dǎo)出對(duì)外的變量或函數(shù),通過(guò) require() 來(lái)導(dǎo)入其他模塊的輸出到當(dāng)前模塊作用域中,同時(shí),遵循以下約定:
在模塊中,必須暴露一個(gè) require 變量,它是一個(gè)函數(shù),require 函數(shù)接受一個(gè)模塊標(biāo)識(shí)符,require 返回外部模塊的導(dǎo)出的 API。如果要求的模塊不能被返回則 require 必須拋出一個(gè)錯(cuò)誤。
在模塊中,必須有一個(gè)自由變量叫做 exports,它是一個(gè)對(duì)象,模塊在執(zhí)行時(shí)可以在 exports 上掛載模塊的屬性。模塊必須使用 exports 對(duì)象作為唯一的導(dǎo)出方式。
在模塊中,必須有一個(gè)自由變量 module,它也是一個(gè)對(duì)象。module 對(duì)象必須有一個(gè) id 屬性,它是這個(gè)模塊的頂層 id。id 屬性必須是這樣的,require(module.id) 會(huì)從源出 module.id 的那個(gè)模塊返回 exports 對(duì)象(就是說(shuō) module.id 可以被傳遞到另一個(gè)模塊,而且在要求它時(shí)必須返回最初的模塊)。
Node 對(duì) CommonJS 規(guī)范的實(shí)現(xiàn)
定義了模塊內(nèi)部的 module.require 函數(shù)和全局的 require 函數(shù),用來(lái)加載模塊。
在 Node 模塊系統(tǒng)中,每個(gè)文件都被視為一個(gè)獨(dú)立的模塊。模塊被加載時(shí),都會(huì)初始化為 Module 對(duì)象的實(shí)例,Module 對(duì)象的基本實(shí)現(xiàn)和屬性如下所示:
function Module(id = "", parent) { // 模塊 id,通常為模塊的絕對(duì)路徑 this.id = id; this.path = path.dirname(id); this.exports = {}; // 當(dāng)前模塊調(diào)用者 this.parent = parent; updateChildren(parent, this, false); this.filename = null; // 模塊是否加載完成 this.loaded = false; // 當(dāng)前模塊所引用的模塊 this.children = []; }
每一個(gè)模塊都對(duì)外暴露自己的 exports 屬性作為使用接口。
模塊導(dǎo)出以及引用
在 Node 中,可使用 module.exports 對(duì)象整體導(dǎo)出一個(gè)變量或者函數(shù),也可將需要導(dǎo)出的變量或函數(shù)掛載到 exports 對(duì)象的屬性上,代碼如下所示:
// 1. 使用 exports: 筆者習(xí)慣通常用作對(duì)工具庫(kù)函數(shù)或常量的導(dǎo)出 exports.name = 'xiaoxiang'; exports.add = (a, b) => a + b; // 2. 使用 module.exports:導(dǎo)出一整個(gè)對(duì)象或者單一函數(shù) ... module.exports = { add, minus }
通過(guò)全局 require 函數(shù)引用模塊,可傳入模塊名稱、相對(duì)路徑或者絕對(duì)路徑,當(dāng)模塊文件后綴為 js / json / node 時(shí),可省略后綴,如下代碼所示:
// 引用模塊 const { add, minus } = require('./module'); const a = require('/usr/app/module'); const http = require('http');
注意事項(xiàng):
exports 變量是在模塊的文件級(jí)作用域內(nèi)可用的,且在模塊執(zhí)行之前賦值給 module.exports。
exports.name = 'test'; console.log(module.exports.name); // test module.export.name = 'test'; console.log(exports.name); // test
如果為 exports 賦予了新值,則它將不再綁定到 module.exports,反之亦然:
exports = { name: 'test' }; console.log(module.exports.name, exports.name); // undefined, test
當(dāng) module.exports 屬性被新對(duì)象完全替換時(shí),通常也需要重新賦值 exports:
module.exports = exports = { name: 'test' }; console.log(module.exports.name, exports.name) // test, test
模塊系統(tǒng)實(shí)現(xiàn)分析
模塊定位
以下是 require 函數(shù)的代碼實(shí)現(xiàn):
// require 入口函數(shù) Module.prototype.require = function(id) { //... requireDepth++; try { return Module._load(id, this, /* isMain */ false); // 加載模塊 } finally { requireDepth--; } };
上述代碼接收給定的模塊路徑,其中的 requireDepth 用來(lái)記載模塊加載的深度。其中 Module 的類方法 _load 實(shí)現(xiàn)了 Node 加載模塊的主要邏輯,下面我們來(lái)解析 Module._load 函數(shù)的源碼實(shí)現(xiàn),為了方便大家理解,我把注釋加在了文中。
Module._load = function(request, parent, isMain) { // 步驟一:解析出模塊的全路徑 const filename = Module._resolveFilename(request, parent, isMain); // 步驟二:加載模塊,具體分三種情況處理 // 情況一:存在緩存的模塊,直接返回模塊的 exports 屬性 const cachedModule = Module._cache[filename]; if (cachedModule !== undefined) return cachedModule.exports; // 情況二:加載內(nèi)建模塊 const mod = loadNativeModule(filename, request); if (mod && mod.canBeRequiredByUsers) return mod.exports; // 情況三:構(gòu)建模塊加載 const module = new Module(filename, parent); // 加載過(guò)之后就進(jìn)行模塊實(shí)例緩存 Module._cache[filename] = module; // 步驟三:加載模塊文件 module.load(filename); // 步驟四:返回導(dǎo)出對(duì)象 return module.exports; };
加載策略
上面的代碼信息量比較大,我們主要看以下幾個(gè)問(wèn)題:
模塊的緩存策略是什么?
分析上述代碼我們可以看到, _load 加載函數(shù)針對(duì)三種情況給出了不同的加載策略,分別是:
情況一:緩存命中,直接返回。
情況二:內(nèi)建模塊,返回暴露出來(lái)的 exports 屬性,也就是 module.exports 的別名。
情況三:使用文件或第三方代碼生成模塊,最后返回,并且緩存,這樣下次同樣的訪問(wèn)就會(huì)去使用緩存而不是重新加載。
2. Module._resolveFilename(request, parent, isMain) 是怎么解析出文件名稱的?
我們看如下定義的類方法:
Module._resolveFilename = function(request, parent, isMain, options) { if (NativeModule.canBeRequiredByUsers(request)) { // 優(yōu)先加載內(nèi)建模塊 return request; } let paths; // node require.resolve 函數(shù)使用的 options,options.paths 用于指定查找路徑 if (typeof options === "object" && options !== null) { if (ArrayIsArray(options.paths)) { const isRelative = request.startsWith("./") || request.startsWith("../") || (isWindows && request.startsWith(".\\")) || request.startsWith("..\\"); if (isRelative) { paths = options.paths; } else { const fakeParent = new Module("", null); paths = []; for (let i = 0; i < options.paths.length; i++) { const path = options.paths[i]; fakeParent.paths = Module._nodeModulePaths(path); const lookupPaths = Module._resolveLookupPaths(request, fakeParent); for (let j = 0; j < lookupPaths.length; j++) { if (!paths.includes(lookupPaths[j])) paths.push(lookupPaths[j]); } } } } else if (options.paths === undefined) { paths = Module._resolveLookupPaths(request, parent); } else { //... } } else { // 查找模塊存在路徑 paths = Module._resolveLookupPaths(request, parent); } // 依據(jù)給出的模塊和遍歷地址數(shù)組,以及是否為入口模塊來(lái)查找模塊路徑 const filename = Module._findPath(request, paths, isMain); if (!filename) { const requireStack = []; for (let cursor = parent; cursor; cursorcursor = cursor.parent) { requireStack.push(cursor.filename || cursor.id); } // 未找到模塊,拋出異常(是不是很熟悉的錯(cuò)誤) let message = `Cannot find module '${request}'`; if (requireStack.length > 0) { messagemessage = message + "\nRequire stack:\n- " + requireStack.join("\n- "); } const err = new Error(message); err.code = "MODULE_NOT_FOUND"; err.requireStack = requireStack; throw err; } // 最終返回包含文件名的完整路徑 return filename; };
上面的代碼中比較突出的是使用了 _resolveLookupPaths 和 _findPath 兩個(gè)方法。
_resolveLookupPaths: 通過(guò)接受模塊名稱和模塊調(diào)用者,返回提供 _findPath 使用的遍歷范圍數(shù)組。
// 模塊文件尋址的地址數(shù)組方法 Module._resolveLookupPaths = function(request, parent) { if (NativeModule.canBeRequiredByUsers(request)) { debug("looking for %j in []", request); return null; } // 如果不是相對(duì)路徑 if ( request.charAt(0) !== "." || (request.length > 1 && request.charAt(1) !== "." && request.charAt(1) !== "/" && (!isWindows || request.charAt(1) !== "\\")) ) { /** * 檢查 node_modules 文件夾 * modulePaths 為用戶目錄,node_path 環(huán)境變量指定目錄、全局 node 安裝目錄 */ let paths = modulePaths; if (parent != null && parent.paths && parent.paths.length) { // 父模塊的 modulePath 也要加到子模塊的 modulePath 里面,往上回溯查找 paths = parent.paths.concat(paths); } return paths.length > 0 ? paths : null; } // 使用 repl 交互時(shí),依次查找 ./ ./node_modules 以及 modulePaths if (!parent || !parent.id || !parent.filename) { const mainPaths = ["."].concat(Module._nodeModulePaths("."), modulePaths); return mainPaths; } // 如果是相對(duì)路徑引入,則將父級(jí)文件夾路徑加入查找路徑 const parentDir = [path.dirname(parent.filename)]; return parentDir; };
_findPath: 依據(jù)目標(biāo)模塊和上述函數(shù)查找到的范圍,找到對(duì)應(yīng)的 filename 并返回。
// 依據(jù)給出的模塊和遍歷地址數(shù)組,以及是否頂層模塊來(lái)尋找模塊真實(shí)路徑 Module._findPath = function(request, paths, isMain) { const absoluteRequest = path.isAbsolute(request); if (absoluteRequest) { // 絕對(duì)路徑,直接定位到具體模塊 paths = [""]; } else if (!paths || paths.length === 0) { return false; } const cacheKey = request + "\x00" + (paths.length === 1 ? paths[0] : paths.join("\x00")); // 緩存路徑 const entry = Module._pathCache[cacheKey]; if (entry) return entry; let exts; let trailingSlash = request.length > 0 && request.charCodeAt(request.length - 1) === CHAR_FORWARD_SLASH; // '/' if (!trailingSlash) { trailingSlash = /(?:^|\/)\.?\.$/.test(request); } // For each path for (let i = 0; i < paths.length; i++) { const curPath = paths[i]; if (curPath && stat(curPath) < 1) continue; const basePath = resolveExports(curPath, request, absoluteRequest); let filename; const rc = stat(basePath); if (!trailingSlash) { if (rc === 0) { // stat 狀態(tài)返回 0,則為文件 // File. if (!isMain) { if (preserveSymlinks) { // 當(dāng)解析和緩存模塊時(shí),命令模塊加載器保持符號(hào)連接。 filename = path.resolve(basePath); } else { // 不保持符號(hào)鏈接 filename = toRealPath(basePath); } } else if (preserveSymlinksMain) { filename = path.resolve(basePath); } else { filename = toRealPath(basePath); } } if (!filename) { if (exts === undefined) exts = ObjectKeys(Module._extensions); // 解析后綴名 filename = tryExtensions(basePath, exts, isMain); } } if (!filename && rc === 1) { /** * stat 狀態(tài)返回 1 且文件名不存在,則認(rèn)為是文件夾 * 如果文件后綴不存在,則嘗試加載該目錄下的 package.json 中 main 入口指定的文件 * 如果不存在,然后嘗試 index[.js, .node, .json] 文件 */ if (exts === undefined) exts = ObjectKeys(Module._extensions); filename = tryPackage(basePath, exts, isMain, request); } if (filename) { // 如果存在該文件,將文件名則加入緩存 Module._pathCache[cacheKey] = filename; return filename; } } const selfFilename = trySelf(paths, exts, isMain, trailingSlash, request); if (selfFilename) { // 設(shè)置路徑的緩存 Module._pathCache[cacheKey] = selfFilename; return selfFilename; } return false; };
模塊加載
標(biāo)準(zhǔn)模塊處理
閱讀完上面的代碼,我們發(fā)現(xiàn),當(dāng)遇到模塊是一個(gè)文件夾的時(shí)候會(huì)執(zhí)行 tryPackage 函數(shù)的邏輯,下面簡(jiǎn)要分析一下具體實(shí)現(xiàn)。
// 嘗試加載標(biāo)準(zhǔn)模塊 function tryPackage(requestPath, exts, isMain, originalPath) { const pkg = readPackageMain(requestPath); if (!pkg) { // 如果沒(méi)有 package.json 這直接使用 index 作為默認(rèn)入口文件 return tryExtensions(path.resolve(requestPath, "index"), exts, isMain); } const filename = path.resolve(requestPath, pkg); let actual = tryFile(filename, isMain) || tryExtensions(filename, exts, isMain) || tryExtensions(path.resolve(filename, "index"), exts, isMain); //... return actual; } // 讀取 package.json 中的 main 字段 function readPackageMain(requestPath) { const pkg = readPackage(requestPath); return pkg ? pkg.main : undefined; }
readPackage 函數(shù)負(fù)責(zé)讀取和解析 package.json 文件中的內(nèi)容,具體描述如下:
function readPackage(requestPath) { const jsonPath = path.resolve(requestPath, "package.json"); const existing = packageJsonCache.get(jsonPath); if (existing !== undefined) return existing; // 調(diào)用 libuv uv_fs_open 的執(zhí)行邏輯,讀取 package.json 文件,并且緩存 const json = internalModuleReadJSON(path.toNamespacedPath(jsonPath)); if (json === undefined) { // 接著緩存文件 packageJsonCache.set(jsonPath, false); return false; } //... try { const parsed = JSONParse(json); const filtered = { name: parsed.name, main: parsed.main, exports: parsed.exports, type: parsed.type }; packageJsonCache.set(jsonPath, filtered); return filtered; } catch (e) { //... } }
上面的兩段代碼完美地解釋 package.json 文件的作用,模塊的配置入口( package.json 中的 main 字段)以及模塊的默認(rèn)文件為什么是 index,具體流程如下圖所示:
模塊文件處理
定位到對(duì)應(yīng)模塊之后,該如何加載和解析呢?以下是具體代碼分析:
Module.prototype.load = function(filename) { // 保證模塊沒(méi)有加載過(guò) assert(!this.loaded); this.filename = filename; // 找到當(dāng)前文件夾的 node_modules this.paths = Module._nodeModulePaths(path.dirname(filename)); const extension = findLongestRegisteredExtension(filename); //... // 執(zhí)行特定文件后綴名解析函數(shù) 如 js / json / node Module._extensions[extension](this, filename); // 表示該模塊加載成功 this.loaded = true; // ... 省略 esm 模塊的支持 };
后綴處理
可以看出,針對(duì)不同的文件后綴,Node.js 的加載方式是不同的,一下針對(duì) .js, .json, .node 簡(jiǎn)單進(jìn)行分析。
.js 后綴 js 文件讀取主要通過(guò) Node 內(nèi)置 API fs.readFileSync 實(shí)現(xiàn)。
Module._extensions[".js"] = function(module, filename) { // 讀取文件內(nèi)容 const content = fs.readFileSync(filename, "utf8"); // 編譯執(zhí)行代碼 module._compile(content, filename); };
.json 后綴 JSON 文件的處理邏輯比較簡(jiǎn)單,讀取文件內(nèi)容后執(zhí)行 JSONParse 即可拿到結(jié)果。
Module._extensions[".json"] = function(module, filename) { // 直接按照 utf-8 格式加載文件 const content = fs.readFileSync(filename, "utf8"); //... try { // 以 JSON 對(duì)象格式導(dǎo)出文件內(nèi)容 module.exports = JSONParse(stripBOM(content)); } catch (err) { //... } };
.node 后綴 .node 文件是一種由 C / C++ 實(shí)現(xiàn)的原生模塊,通過(guò) process.dlopen 函數(shù)讀取,而 process.dlopen 函數(shù)實(shí)際上調(diào)用了 C++ 代碼中的 DLOpen 函數(shù),而 DLOpen 中又調(diào)用了 uv_dlopen, 后者加載 .node 文件,類似 OS 加載系統(tǒng)類庫(kù)文件。
Module._extensions[".node"] = function(module, filename) { //... return process.dlopen(module, path.toNamespacedPath(filename)); };
從上面的三段源碼,我們看出來(lái)并且可以理解,只有 JS 后綴最后會(huì)執(zhí)行實(shí)例方法 _compile,我們?nèi)コ恍?shí)驗(yàn)特性和調(diào)試相關(guān)的邏輯來(lái)簡(jiǎn)要的分析一下這段代碼。
編譯執(zhí)行
模塊加載完成后,Node 使用 V8 引擎提供的方法構(gòu)建運(yùn)行沙箱,并執(zhí)行函數(shù)代碼,代碼如下所示:
Module.prototype._compile = function(content, filename) { let moduleURL; let redirects; // 向模塊內(nèi)部注入公共變量 __dirname / __filename / module / exports / require,并且編譯函數(shù) const compiledWrapper = wrapSafe(filename, content, this); const dirname = path.dirname(filename); const require = makeRequireFunction(this, redirects); let result; const exports = this.exports; const thisValue = exports; const module = this; if (requireDepth === 0) statCache = new Map(); //... // 執(zhí)行模塊中的函數(shù) result = compiledWrapper.call( thisValue, exports, require, module, filename, dirname ); hasLoadedAnyUserCJSModule = true; if (requireDepth === 0) statCache = null; return result; }; // 注入變量的核心邏輯 function wrapSafe(filename, content, cjsModuleInstance) { if (patched) { const wrapper = Module.wrap(content); // vm 沙箱運(yùn)行 ,直接返回運(yùn)行結(jié)果,env -> SetProtoMethod(script_tmpl, "runInThisContext", RunInThisContext); return vm.runInThisContext(wrapper, { filename, lineOffset: 0, displayErrors: true, // 動(dòng)態(tài)加載 importModuleDynamically: async specifier => { const loader = asyncESM.ESMLoader; return loader.import(specifier, normalizeReferrerURL(filename)); } }); } let compiled; try { compiled = compileFunction( content, filename, 0, 0, undefined, false, undefined, [], ["exports", "require", "module", "__filename", "__dirname"] ); } catch (err) { //... } const { callbackMap } = internalBinding("module_wrap"); callbackMap.set(compiled.cacheKey, { importModuleDynamically: async specifier => { const loader = asyncESM.ESMLoader; return loader.import(specifier, normalizeReferrerURL(filename)); } }); return compiled.function; }
上述代碼中,我們可以看到在 _compile 函數(shù)中調(diào)用了 wrapwrapSafe 函數(shù),執(zhí)行了 __dirname / __filename / module / exports / require 公共變量的注入,并且調(diào)用了 C++ 的 runInThisContext 方法(位于 src/node_contextify.cc 文件)構(gòu)建了模塊代碼運(yùn)行的沙箱環(huán)境,并返回了 compiledWrapper 對(duì)象,最終通過(guò) compiledWrapper.call 方法運(yùn)行模塊。
到此,關(guān)于“Node.js模塊系統(tǒng)源碼分析”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注億速云網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)?lái)更多實(shí)用的文章!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。