node源碼筆記——模塊載入

由於這幾天在學習react伺服器端渲染是如何實現的,在學習的過程中遇到了一個問題:在寫react的時候是通過webpack進行構建的,在構建的過程中用到了css-loader提供的css modules功能和file-loader,因為用到了css-loader和file-loader,所以在寫的代碼時候就import了.css、.png、.jpg等後綴的文件,對於這些文件在node環境下import是會報錯的,為了解決這個問題,我引入了webpack-isomorphic這個庫,這個庫之所以能夠解決這個問題主要是因為它擴展了node本身require的功能,使得node能夠require .css、.png、.jpg等後綴的文件。那麼如何才能擴展require的功能呢?這篇文章就是來解析node的模塊載入原理和如何擴展require功能的。

模塊載入原理解析

node對於require的每個模塊都會生成一個Module對象,所以讓我們先看看Module的源碼(lib/module.js里):

// Module的構造函數nfunction Module(id, parent) {n // id一般為該模塊的路徑,對於通過node運行的主文件的id為.n this.id = id;n n // 模塊導出的內容,就是我們在寫模塊過程中遇到的module.exports和exportsn this.exports = {};n n // 父模塊n this.parent = parent;n n // 將本模塊添加到父模塊中去n if (parent && parent.children) {n parent.children.push(this);n }n n // 該模塊對應的文件名(絕對路徑)n this.filename = null;nn // 該模塊是否已經載入完n this.loaded = false;n n // 該模塊的子模塊(在該模塊中require的模塊)n this.children = []; n}nn// 用來緩存載入過的模塊,和require.cache一致nModule._cache = {};nn// 用來緩存文件路徑nModule._pathCache = {};nn// 保存對不同後綴文件的解析,對於node本身提供了n// Module._extensions[.js]、Module._extensions[.json]n// 和Module._extensions[.node]nModule._extensions = {};nn// request參數是請求的模塊名,比如require(http)中的httpn// path參數是該模塊可能在的目錄n// isMain參數表明該模塊是否是主模塊(node啟動的文件對應的模塊)n// 這個函數的作用就是根據參數來尋找模塊對應文件的絕對路徑,如果沒有找到就報錯nModule._findPath = function(request, paths, isMain) {...}nn// from參數是絕對路徑,比如/seg1/seg2/seg3n// 這個函數用來獲取node_modules路徑的,如[/seg1/seg2/seg3/node_modules,n// /seg1/seg2/node_modules, /seg1/node_modules, /node_modules]nModule._nodeModulePaths = function(from) {...}nn// request參數是請求的模塊名n// 該函數的作用是用來獲取請求模塊可能在的目錄的路徑nModule._resolveLookupPaths = function(request, parent) {...}nn// 載入指定的模塊(間接的調用Module.prototype.load)nModule._load = function(request, parent, isMain) {...}nn// 解析指定模塊的絕對路徑nModule._resolveFilename = function(request, parent, isMain) {...}nn// 載入指定模塊nModule.prototype.load = function(filename) {...}nn// 這個函數可以算是我們平常寫模塊是遇到的requirenModule.prototype.require = function(path) {...}nn// 解析編譯並運行的模塊內容nModule.prototype._compile = function(content, filename) {...}nn// 用來解析.js後綴的文件nModule._extensions[.js] = function(module, filename) {...}nn// 用來解析.json後綴的文件nModule._extensions[.json] = function(module, filename) {...}nn// 用來接卸.node後綴的文件nModule._extensions[.node] = function(module, filename) {...}nn// 運行主模塊,比如node index.js會運行node.js這個模塊nModule.runMain = function() {...}n

上面主要列出了Module的構造函數和一些主要方法,並通過注釋的形式進行了一定的解釋。由於模塊的載入和運行是通過require和node XXX.js進行的,所以我們先來看看Module.runMain和Module.prototype.require兩個方法:

Module.runMain = function() {n Module._load(process.argv[1], null, true)n ...n}nnModule.prototype.require = function(path) {n return Module._load(path, this, /* isMain */ false);n};n

可以看到都是通過Module._load方法載入並運行模塊的,那我們看看Module._load方法:

Module._load = function(request, parent, isMain) {n ...n n // 解析請求模塊對應文件的絕對路徑,如果不存在的話拋出異常n var filename = Module._resolveFilename(request, parent, isMain);nn // 查看對應的模塊是否之前被引用過並緩存了,如果是直接返回緩存n var cachedModule = Module._cache[filename];n if (cachedModule) {n return cachedModule.exports;n }n n // 如果是node的內置的模塊,通過NativeModule.require返回n if (NativeModule.nonInternalExists(filename)) {n debug(load native module %s, request);n return NativeModule.require(filename);n }n n // 創建一個新的Module對象n var module = new Module(filename, parent);n n // 如果是主模塊進行一些標記n if (isMain) {n process.mainModule = module;n module.id = .;n }nn // 這裡之所以在載入模塊之前把模塊緩存起來n // 我覺得主要是因為commonjs對循環依賴的處理n Module._cache[filename] = module;n n // 嘗試去載入模塊n tryModuleLoad(module, filename);nn return module.exports;n};n

這裡我對每一步加上了簡單的注釋說明了每一個步驟。這裡需要進一步的說明_resolveFilename和tryModuleLoad,因為這兩個方法對於擴展require處理更多後綴的文件是有用的。讓我們先來看看_resolveFilename方法:

Module._resolveFilename = function(request, parent, isMain) {n // 判斷請求的模塊是否是內置的模塊,如果是直接返回模塊名(request)n if (NativeModule.nonInternalExists(request)) {n return request;n }nn // 根據模塊名和父模塊來獲取該模塊可能在的目錄n // 對於絕對路徑的模塊,路徑就是該路徑的dirn // 對於相對路徑的模塊,路徑通過父模塊的filename來獲取n // 對於其他情況,可能的路徑就根據node本身的模塊載入規則來獲取的n var resolvedModule = Module._resolveLookupPaths(request, parent);n var id = resolvedModule[0];n var paths = resolvedModule[1];n n // 這裡通過請求的模塊名和模塊可能在的目錄,來獲取模塊文件的絕對路徑n // 比如請求模塊是require(./a),那麼需要判斷./a是不是目錄,n // 如果不是目錄,則需要添加指定的後綴(Module._extensions對象的key),n // 並看看文件是否存在;如果是目錄則需要判斷該目錄下是否有package.json等情況n var filename = Module._findPath(request, paths, isMain);n if (!filename) {n var err = new Error("Cannot find module " + request + "");n err.code = MODULE_NOT_FOUND;n throw err;n }n return filename;n};n

這裡我對每一步加上了簡單的注釋說明了每一個步驟,特別需要注意的是Module._findPath這一步需要值得注意,因為這個在webpack-isomorphic中用到了的。

接下來讓我們看看tryModuleLoad源碼:

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

這裡調用了module.load方法,讓我們繼續看看module.load方法:

Module.prototype.load = function(filename) {n this.filename = filename;n this.paths = Module._nodeModulePaths(path.dirname(filename));nn var extension = path.extname(filename) || .js;n if (!Module._extensions[extension]) extension = .js;n Module._extensions[extension](this, filename);n this.loaded = true;n};n

首先根據filename來確定後綴,如果沒有的話就為 .js,然後判斷後綴是否在Module._extensions中有對應,沒有的話將後綴設置為.js,然後利用Module._extensions中對應的解析模塊的方法進行解析,node本身提供了對.json、.node和.js的後綴提供了解析方法,所以我們來這個三個解析方法:

.json文件的解析

Module._extensions[.json] = function(module, filename) {n var content = fs.readFileSync(filename, utf8);n try {n module.exports = JSON.parse(internalModule.stripBOM(content));n } catch (err) {n err.message = filename + : + err.message;n throw err;n }n};n

同步的讀取文件,然後將解析的json保存在module.exports中。

.node文件的解析

Module._extensions[.node] = function(module, filename) {n return process.dlopen(module, path._makeLong(filename));n};n

直接調用了process.dlopen方法,這裡不進行進一步解釋。

.js文件的解析

Module._extensions[.js] = function(module, filename) {n var content = fs.readFileSync(filename, utf8);n module._compile(internalModule.stripBOM(content), filename);n};n

首先同步讀取文件內容,然後調用_compile方法對模塊進行編譯和運行,讓我們來看看_compile方法的源碼(為了方便閱讀,刪除了一些不影響理解的代碼):

Module.prototype._compile = function(content, filename) {n var contLen = content.length;n n ...nn var wrapper = Module.wrap(content);nn var compiledWrapper = vm.runInThisContext(wrapper, {n filename: filename,n lineOffset: 0,n displayErrors: truen });n n var dirname = path.dirname(filename);n var require = internalModule.makeRequireFunction.call(this);n var args = [this.exports, require, this, filename, dirname];n var depth = internalModule.requireDepth;n if (depth === 0) stat.cache = new Map();n var result = compiledWrapper.apply(this.exports, args);n if (depth === 0) stat.cache = null;n return result;n};n

首先通過Module.wrap對模塊內容進行包裹,具體的包括後的內容如下:

(function (exports, require, module, __filename, __dirname) {n // 下面的content就是模塊內的內容n contentn});n

然後用vm.runInThisContext返回上面包括的函數,然後在調用這個函數就行了,這裡有個值得注意的地方就是require是通過internalModule.makeRequireFunction生成的,這個其實就是對Module.prototype.require函數進行了簡單的包裝。

擴展require對後綴的處理

通過上面的解析,能夠比較清楚的了解到,如果想要擴展對不同後綴文件進行解析的話,一般情況下只要擴展Module._extensions,然後將對指定後綴的文件的內容進行解析,最後把結果放到module.export上就行。如果對於require模塊的路徑解析和node本身對於路徑解析不一樣的話,那麼還需要對_findPath進行重寫,不然的話會報錯,下面是兩個具體的例子:

.v後綴文件的解析的例子(路徑解析和node本身一致)

test.v內容:

運行結果:

.x後綴文件的解析的例子(路徑解析和node本身不一致)

require(XXX)的內容在test.json文件中,具體代碼如下:

test.json內容如下:

運行結果如下:

總結

初步的解析了node模塊載入是如何實現的,並解釋了如何擴展require對更多後綴名的文件進行解析,對於看到最後的讀者真的的非常的感激,如果您發現文章有哪裡有不對的地方,請指出,非常感謝。


推薦閱讀:

前端開發工作遇到瓶頸,不知何去何從?
參加 2017 年 8 月 26 日北京第三屆 FEDAY 是個什麼樣的體驗?
Webstorm 的 Tab 鍵怎樣調整縮進值?
在網頁上看到想要的顏色,如何知道這種顏色的顏色代碼和 RGB 顏色值?

TAG:Nodejs | 源码阅读 | 前端开发 |