淺析當下的 Node.js CommonJS 模塊系統

這裡就是在模塊代碼中常用到的 module, exports, require ,__dirname, __filename 的來源。

經過 Module.wrap() 包裝之後的代碼使用 vm.runInThisContext() 在當前上下文中編譯執行。

// content 來自模塊的代碼var wrapper = Module.wrap(content);var compiledWrapper = vm.runInThisContext(wrapper, { filename: filename, lineOffset: 0, displayErrors: true});

完成上面的鋪墊,迴歸主線,開始解析 makeRequireFunction 做了什麼。

2.2 makeRequireFunction 的實現

// internal/modules/cjs/helpers.js:20// 調用 makeRequireFunction(module) 這裡的 | module | 是 Module對象// 生成當前模塊上下文的 require()function makeRequireFunction(mod) { const Module = mod.constructor;  // 創建一個模塊相關的 require // 依賴深度機制實現十分巧妙 function require(path) { try { exports.requireDepth += 1; return mod.require(path); } finally { exports.requireDepth -= 1; } } // resolve 是對 Module._resolveFilename 的封裝 function resolve(request, options) { if (typeof request !== 'string') { throw new ERR_INVALID_ARG_TYPE('request', 'string', request); } return Module._resolveFilename(request, mod, false, options); } require.resolve = resolve;  // resolve.paths 是對 Module._resolveLookupPaths 的封裝 function paths(request) { if (typeof request !== 'string') { throw new ERR_INVALID_ARG_TYPE('request', 'string', request); } return Module._resolveLookupPaths(request, mod, true); } resolve.paths = paths;  // process.mainModule 入口模塊 require.main = process.mainModule; // 啟用支持以添加額外的擴展類型 // 我們可以實現 require.extensions['.xxx'] // 來實現對自定義文件類型模塊的支持 require.extensions = Module._extensions;  // 模塊的緩存 // key: 模塊路徑 // value: 模塊實例 require.cache = Module._cache; return require;}

通過上面的代碼我們完整了解了 makeRequireFunction 的實現,它主要實現了:

  • 生成對應 module 上下文的 require 方法,內部調用 module 實例上 的 require 方法通過 require.resolve 添加類型校驗,封裝了 Module._resolveFilename通過 require.resolve.paths 封裝了 Module._resolveLookupPaths在 require.main 添加入口模塊 process.mainModule通過 require.extensions 暴露 Module._extensions,提供了擴展能力在 require.cache 暴露了 Module._cache,我們可以通過 require.cache操作模塊緩存。

2.3 require 來自於 module.require

// Loads a module at the given file path. Returns that module's// `exports` property.Module.prototype.require = function(id) { // 參數校驗  // 必須為 非空字符 if (typeof id !== 'string') { throw new ERR_INVALID_ARG_TYPE('id', 'string', id); } if (id === '') { throw new ERR_INVALID_ARG_VALUE('id', id, 'must be a non-empty string'); } // 調用 Module._load return Module._load(id, this, /* isMain */ false);};

可以看到 module.require 參數校驗完成之後,就調用 Module._load

2.4 模塊的加載之前的工作 Module._load

Module._load 的主要邏輯:

  • 如果模塊已經在緩存中存在,直接返回緩存中的模塊如果是是原生模塊,直接調用 NativeModule.require()其他情況,創建一個新的模塊實例,加入緩存,調用模塊實例的加載方法
Module._load = function(request, parent, isMain) { if (parent) { debug('Module._load REQUEST %s parent: %s', request, parent.id); } if (experimentalModules && isMain) { asyncESM.loaderPromise.then((loader) => { return loader.import(getURLFromFilePath(request).pathname); }) .catch((e) => { decorateErrorStack(e); console.error(e); process.exit(1); }); return; } var filename = Module._resolveFilename(request, parent, isMain); var cachedModule = Module._cache[filename]; if (cachedModule) { updateChildren(parent, cachedModule, true); return cachedModule.exports; } if (NativeModule.nonInternalExists(filename)) { debug('load native module %s', request); return NativeModule.require(filename); } // Don't call updateChildren(), Module constructor already does. var module = new Module(filename, parent); if (isMain) { process.mainModule = module; module.id = '.'; } Module._cache[filename] = module; tryModuleLoad(module, filename); return module.exports;};

可以看到,上面代碼中的 Module._cache[filename] = module , 對還沒有開始加載的模塊就寫入緩存可能是不安全的。

tryModuleLoad 這這裡做了檢測,如果模塊加載失敗,會清理模塊的緩存。

function tryModuleLoad(module, filename) { var threw = true; try { module.load(filename); threw = false; } finally { if (threw) { delete Module._cache[filename]; } }}

2.5 模塊需要真實加載

module.load 的主要邏輯:

  • 寫入 module.filename生成當前模塊的模塊路徑, 並緩存在 module.paths根據 filename 文件後綴,選擇不同的處理方式如果啟用了 ES Module, 調用 ESMLoader 加載模塊
// 設置文件名,並選擇相應的文件處理// 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; this.paths = Module._nodeModulePaths(path.dirname(filename)); var extension = path.extname(filename) || '.js'; if (!Module._extensions[extension]) extension = '.js'; Module._extensions[extension](this, filename); this.loaded = true; if (experimentalModules) { // ES Module 相關邏輯,下篇文章分析 ... }};

同步讀取文件,清除文件中的 BOM 編碼字符,然後調用 module._compile 編譯

// Native extension for .jsModule._extensions['.js'] = function(module, filename) { var content = fs.readFileSync(filename, 'utf8'); module._compile(stripBOM(content), filename);};

2.6 模塊的編譯 module._compile

module._compile 的主要工作:

  • 在當前的作用域或沙箱中運行文件內容,暴露當前模塊上下文的的工具變量(require,module,exports)到模塊文件中。如果有的任何異常,返回異常

以下的代碼是 module._compile 的部分代碼,去掉了 調試相關和文件 stat 緩存的代碼。

Module.prototype._compile = function(content, filename) {  // 去除 Shebang 比如:#!/bin/sh content = stripShebang(content); // create wrapper function // 創建封裝函數 var wrapper = Module.wrap(content);  // 在當前上下文編譯模塊的封裝函數代碼 // 傳入當前模塊的文件名,用於定義堆棧跟蹤信息 // 如在解析代碼的時候發生錯誤Error,引起錯誤的行將會被加入堆棧跟蹤信息 var compiledWrapper = vm.runInThisContext(wrapper, { filename: filename, lineOffset: 0, displayErrors: true }); ...  var dirname = path.dirname(filename); var require = makeRequireFunction(this); var depth = requireDepth;  ...   // 運行模塊的封裝函數 // 並傳入 `exports` , `require`, `module` `filename`, `dirname` var result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname);  return result;};

2.7 小結

模塊的代碼最終在 Module.prototype._compile 中完成了封裝、編譯、執行。最後我們在回看 Module._load ,之前我們看到的 Module.load , Module.prototype._compile 都在 tryModuleLoad() 中被調用, 當這些都執行完成之後,Module._load 返回 module.exports

Module._load = function(request, parent, isMain) { ...  tryModuleLoad(module, filename); return module.exports;};

所以我們在模塊可以使用下面的方式暴露模塊 API

function add () {}module.exports.add = addexports.add = addthis.add = addmodule.exports = { add}

3.模塊系統中的一些細節

3.1 各種緩存

  • Module._pathCacheModule._cachepackageMainCache

模塊請求 -> 路徑的緩存

Module._findPath = function(request, paths, isMain) { if (path.isAbsolute(request)) { paths = ['']; } else if (!paths || paths.length === 0) { return false; } var cacheKey = request + '\\x00' + (paths.length === 1 ? paths[0] : paths.join('\\x00')); var entry = Module._pathCache[cacheKey]; if (entry) return entry;  ... }

模塊路徑 -> 模塊實例的緩存

Module._load = function(request, parent, isMain) {  ...  var filename = Module._resolveFilename(request, parent, isMain); var cachedModule = Module._cache[filename]; if (cachedModule) { updateChildren(parent, cachedModule, true); return cachedModule.exports; } ... }

3.2 啟動

當我們啟動 Node.js 時,入口文件也是作為一個模塊被加載執行。

// bootstrap main module.Module.runMain = function() { // Load the main module--the command line argument. Module._load(process.argv[1], null, true); // Handle any nextTicks added in the first tick of the program process._tickCallback();};

3.3 相對路徑模塊、絕對路徑的模塊與 node_modules 中的模塊

我們之前看到 require.resolve() 封裝了 Module._resolveFilename。從最初調用 require()輸入的字符 request 通過_resolveFilename 匹配到了模塊文件路徑 ,這在繼續深入會發現 Module._resolveLookupPaths ,。

Module._resolveLookupPaths() 會返回一個數組,第一個元素是我們的 request ,第二個元素

console.log(Module._resolveLookupPaths('app', module))[ "app", [ "/Users/awe/Desktop/code/test/node-module/node_modules", "/Users/awe/Desktop/code/test/node_modules", "/Users/awe/Desktop/code/node_modules", "/Users/awe/Desktop/node_modules", "/Users/awe/node_modules", "/Users/node_modules", "/node_modules", "/Users/awe/Desktop/code/test/node-module", "/Users/awe/.node_modules", "/Users/awe/.node_libraries", "/Users/awe/.nvm/versions/node/v10.0.0/lib/node" ]]

/src/node.cc#L3212

// --inspect-brk if (debug_options.wait_for_connect()) { READONLY_DONT_ENUM_PROPERTY(process, "_breakFirstLine", True(env->isolate())); }

回想最開頭提到的找不到模塊的錯誤是在 Module._resolveFilename ,我們可以在 node 源碼中看看它的實現 lib/internal/modules/cjs/loader.js

Module._resolveFilename 的功能是根據我們在 require 輸入的參數,嘗試查找可以引入的模塊。

Module._resolveFilename = function(request, parent, isMain, options) { ... // look up the filename first, since that's the cache key. var filename = Module._findPath(request, paths, isMain); if (!filename) { // 可以看到,找不到模塊的報錯來自於這裡 // eslint-disable-next-line no-restricted-syntax var err = new Error(`Cannot find module '${request}'`); err.code = 'MODULE_NOT_FOUND'; throw err; } return filename;};

4 小結

通過這篇文章,我們從代碼實現瞭解了 Node.js 的 CommonJS 模塊系統的實現,再次回顧模塊系統的流程:

  • 路徑解析文件加載模塊封裝編譯執行緩存
淺析當下的 Node.js CommonJS 模塊系統


分享到:


相關文章: