nodejs模塊系統(tǒng)源碼分析
概述
Node.js的出現(xiàn)使得前端工程師可以跨端工作在服務(wù)器上,當(dāng)然,一個(gè)新的運(yùn)行環(huán)境的誕生亦會(huì)帶來(lái)新的模塊、功能、抑或是思想上的革新,本文將帶領(lǐng)讀者領(lǐng)略 Node.js(以下簡(jiǎn)稱 Node) 的模塊設(shè)計(jì)思想以及剖析部分核心源碼實(shí)現(xiàn)。
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ì)去使用緩存而不是重新加載。
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; cursor = cursor.parent) { requireStack.push(cursor.filename || cursor.id); } // 未找到模塊,拋出異常(是不是很熟悉的錯(cuò)誤) let message = `Cannot find module '${request}'`; if (requireStack.length > 0) { message = 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)置 APIfs.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)行模塊。
以上就是nodejs模塊系統(tǒng)源碼分析的詳細(xì)內(nèi)容,更多關(guān)于nodejs模塊系統(tǒng)源碼分析的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
npm install -g 遇到權(quán)限問(wèn)題解析
這篇文章主要為大家介紹了npm install -g 遇到權(quán)限問(wèn)題解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06解決node終端下運(yùn)行js文件不支持ES6語(yǔ)法
這篇文章主要介紹了解決node終端下運(yùn)行js文件不支持ES6語(yǔ)法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04基于NodeJS的前后端分離的思考與實(shí)踐(六)Nginx + Node.js + Java 的軟件棧部署實(shí)踐
關(guān)于前后端分享的思考,我們已經(jīng)有五篇文章闡述思路與設(shè)計(jì)。本文介紹淘寶網(wǎng)收藏夾將 Node.js 引入傳統(tǒng)技術(shù)棧的具體實(shí)踐。2014-09-09webpack創(chuàng)建項(xiàng)目并打包的詳細(xì)流程記錄
webpack在前端工程領(lǐng)域起到了中流砥柱的作用,理解它的內(nèi)部實(shí)現(xiàn)機(jī)制會(huì)對(duì)你的工程建設(shè)提供很大的幫助(不論是定制功能還是優(yōu)化打包),下面這篇文章主要給大家介紹了關(guān)于webpack創(chuàng)建項(xiàng)目并打包的詳細(xì)流程,需要的朋友可以參考下2023-03-03詳解NodeJs支付寶移動(dòng)支付簽名及驗(yàn)簽
本文主要介紹了NodeJs支付寶移動(dòng)支付簽名及驗(yàn)簽的方法,具有一定的參考價(jià)值,下面跟著小編一起來(lái)看下吧2017-01-01一文學(xué)會(huì)搭建HTTP服務(wù)器調(diào)用DLL庫(kù)
這篇文章主要為大家介紹了一文學(xué)會(huì)搭建HTTP服務(wù)器調(diào)用DLL庫(kù),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06在Node.js中使用Swagger自動(dòng)生成API接口文檔
這篇文章主要給大家介紹了如何在Node.js項(xiàng)目中使用 Swagger 來(lái)自動(dòng)生成 API接口文檔,使用生成方式有很多種,本文基于swagger-jsdoc+swagger-ui-express快速實(shí)現(xiàn),文中通過(guò)代碼示例介紹的非常詳細(xì),需要的朋友可以參考下2024-01-01詳解node如何讓一個(gè)端口同時(shí)支持https與http
眾所周知node是一個(gè)高性能的web服務(wù)器,使用它可以很簡(jiǎn)單的創(chuàng)建一個(gè)http或https的服務(wù)器。這篇文章主要介紹了詳解node如何讓一個(gè)端口同時(shí)支持https與http2017-07-07