溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Node.js模塊系統(tǒng)源碼分析

發(fā)布時(shí)間:2021-11-05 16:46:59 來(lái)源:億速云 閱讀:110 作者:iii 欄目:web開(kāi)發(fā)

這篇文章主要介紹“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)題:

  1.  模塊的緩存策略是什么?

  分析上述代碼我們可以看到, _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,具體流程如下圖所示:

Node.js模塊系統(tǒng)源碼分析

模塊文件處理

定位到對(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í)用的文章!

向AI問(wèn)一下細(xì)節(jié)

免責(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)容。

AI