模組載入痛點
大家也或多或少的瞭解node模組的載入機制,最為粗淺的表述就是依次從當前目錄向上級查詢node_modules目錄,若發現依賴則載入。但是隨著應用規模的加大,目錄層級越來越深,若是在某個模組中想要通過 require 方式以依賴名稱或相對路徑的方式引用其他模組就非常麻煩,影響開發效率和美觀。
示例demo:
1 2 3 4 5 6 7 |
// 當前目錄: /usr/local/test/index.js // gulp模組所在路徑為 /usr/lib/node_modules var gulp = require('../../lib/gulp'); gulp.task('say',function(){ console.log('hello wolrd'); }); |
目前的條件下,只有採用上述中相對路徑的方式引用依賴模組,可以看出上述引用的缺點:
- 醜陋,十分繁雜
- 容易出錯,難以維護
第二個缺點是最難以接受的,在多次引用模組的情況下問題會被放大,因此急需尋找某種方案解決多層目錄依賴引用,本文將會討論筆者在開發過程中的一些嘗試,並歡迎大家一起討論其他可行性方案。
全域性變數法
由於目標是解決毫無美觀又難以理解的相對目錄層級,那麼可以嘗試使用變數完成目錄層級的替代。這種方案最為直接,且node載入該依賴的速度最快,無需遍歷其他各級目錄。但是為了更為通用,筆者常採用全域性變數的方式繫結目錄關係:
demo:
1 2 3 4 5 6 7 |
// 當前目錄: /usr/local/test/index.js // gulp模組所在路徑為 /usr/lib/node_modules global._root = '/usr/lib/node_modules'; var path = require('path'); var gulp = require(path.join(_root,'gulp')); ... |
這種方案最為直接,但是可擴充套件性並不強,而且在多人維護的情況下尤甚,因此建議在單人開發的小專案中採用。
直接引用模組名
直接引用模組名,說到底就是直接引用node_modules目錄中的依賴,類似引用node預設載入的那些模組,如http,event模組。
demo:
1 2 3 4 5 |
// 當前目錄: /usr/local/test/index.js // gulp模組所在路徑為 /usr/lib/node_modules var gulp = require('gulp'); ... |
在目錄/usr/local/test、/usr/local、/usr、/四個目錄下都沒有“node_modules”目錄或者“node_modules”目錄下都沒有gulp模組,那麼執行這個檔案,肯定會報錯“MODULE_NOT_FOUND”,這就是我們接下來需要解決的問題,即如何修改node載入依賴的層級關係。
修改依賴載入層級
相信大家學習node也都讀過一本書《深入淺出nodejs》,這本書的第二章第二節曾簡要介紹node載入依賴所遍歷的一些目錄,書中讓我們在某個測試檔案中輸出module.paths
,結果是一個陣列,類似於
1 |
['/usr/local/test/node_modules'、'/usr/local/node_modules'、'/usr/node_modules'、'/node_modules'] |
這給我們一個啟發,即載入某個模組的順序就是按照上述陣列項的順序依次判斷模組是否存在,若存在則載入,事實上node也確實是這樣做的(下文會針對原始碼分析猜想的正確性)。那麼,在猜想的基礎上我們可以嘗試修改該陣列下可否影響本模組載入依賴的順序,如果成功自然美麗,如若不成功需尋找更為恰當的解決方案。
嘗試1:
1 2 3 4 5 6 |
// 當前目錄: /usr/local/test/index.js // gulp模組所在路徑為 /usr/lib/node_modules module.paths.push('/usr/lib/node_modules'); console.log(module.paths); var gulp = require('gulp'); |
執行命令,一切正常,成功了。通過輸出資訊可看出
1 |
['/usr/local/test/node_modules'、'/usr/local/node_modules'、'/usr/node_modules'、'/node_modules','/usr/lib/node_modules'] |
確實修改了依賴查詢層級,不過可以看出設定的目錄是在陣列中的最後一位,這意味著node會在找到gulp依賴前遍歷4層目錄,最後才在第五層目錄中找到它。如果專案中只引用了gulp也還好,但是隨著其他依賴的數量增多,執行時載入依賴/usr/lib/node_modules下的依賴將會耗費不少時間。因此建議大家在專案中評估好依賴的位置,如果合適的話可以優先載入手動設定的依賴目錄:
1 2 3 4 5 6 |
// 當前目錄: /usr/local/test/index.js // gulp模組所在路徑為 /usr/lib/node_modules module.paths.unshift('/usr/lib/node_modules'); console.log(module.paths); var gulp = require('gulp'); |
這樣,我們在不知道node底層如何工作的前提下就實現了目標。哈哈,不過作為一名靠譜的前端(node)工程師,我們不會滿足這種程度吧?哈哈!
深入原始碼探究
筆者摘出了與模組(依賴)載入相關的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
// 初始化全域性的依賴載入路徑 Module._initPaths = function() { ... var paths = [path.resolve(process.execPath, '..', '..', 'lib', 'node')]; if (homeDir) { paths.unshift(path.resolve(homeDir, '.node_libraries')); paths.unshift(path.resolve(homeDir, '.node_modules')); } // 我們需要著重關注此處,獲取環境變數“NODE_PATH” var nodePath = process.env['NODE_PATH']; if (nodePath) { paths = nodePath.split(path.delimiter).concat(paths); } // modulePaths記錄了全域性載入依賴的根目錄,在Module._resolveLookupPaths中有使用 modulePaths = paths; // clone as a read-only copy, for introspection. Module.globalPaths = modulePaths.slice(0); }; // @params: request為載入的模組名 // @params: parent為當前模組(即載入依賴的模組) Module._resolveLookupPaths = function(request, parent) { ... var start = request.substring(0, 2); // 若為引用模組名的方式,即require('gulp') if (start !== './' && start !== '..') { // 此處的modulePaths即為Module._initPaths函式中賦值的變數 var paths = modulePaths; if (parent) { if (!parent.paths) parent.paths = []; paths = parent.paths.concat(paths); } return [request, paths]; } // 使用eval執行可執行字串的情況下,parent.id 和parent.filename為空 if (!parent || !parent.id || !parent.filename) { var mainPaths = ['.'].concat(modulePaths); mainPaths = Module._nodeModulePaths('.').concat(mainPaths); return [request, mainPaths]; } ... }; |
Module._initPaths函式在預設的生命週期內只執行一次,作用自然是設定全域性載入依賴的相對路徑。而當每次在檔案中執行require載入其他依賴時,Module._resolveLookupPaths函式都會執行,返回一個包含依賴名和可遍歷的目錄陣列(該陣列中的目錄項可以載入到依賴,也可以無法載入依賴)。最後的工作就是根據Module._resolveLookupPaths函式返回的結果,遍歷目錄陣列,載入依賴。如果遍歷結束後仍沒有找到依賴,則拋錯。
在分析完原始碼後,相信大家也都注意了幾點資訊:
- Module._initPaths函式內部檢查了NODE_PATH環境變數
- Module._initPaths函式只執行一次
- Module._initPaths函式初始化的全域性依賴載入路徑與module.paths有關係
那麼,我們可以從另一個角度解決依賴載入的問題。
環境變數法
通過上一節的原始碼分析,我們知道了NODE_PATH的作用,那麼如何使用或者優雅的使用NODE_PATH來解決依賴載入問題呢?
嘗試一
最為直接的是,修改系統的環境變數。在linux下,執行
1 |
export NODE_PATH=/usr/lib/node_modules |
即可解決。
但是,這種方案畢竟不優雅,因為我們的一個專案就修改了系統的環境變數,如果其他專案也採用這種方案,那麼相信系統的NODE_PATH將會變得很長,而且會由於NODE_PATH的子路徑順序問題出現意想不到的衝突,因此作為這種解決方案不建議使用。
嘗試二
我們希望只針對當前執行的程式設定環境變數,不影響其他程式;而且一旦當前程式退出,設定的環境變數也被恢復。滿足這種需求的實現,最為直觀的就是命令列配置。通過查閱node手冊可以這樣執行:
1 |
NODE_PATH=/usr/lib/node_modules node /usr/local/test/index.js |
這樣,仍可以成功載入gulp依賴,而不影響系統的環境變數。
但是,命令列的方式顯而易見,就是醜陋,麻煩。每次執行程式都需要提前輸入一系列的路徑,這種方式將程式碼的可維護性變為了程式的可維護性,在負責的專案中不適合使用。
嘗試三
node執行時給我們提供了一個變數,對,就是process。process是node預設載入的Process模組的一個屬性,通過process可獲取應用程式的相關資訊,同時包括設定的環境變數。
我們可以在應用的入口檔案設定環境變數:
1 2 3 4 |
// 當前目錄: /usr/local/test/index.js // gulp模組所在路徑為 /usr/lib/node_modules process.env.NODE_PATH='/usr/lib/node_modules'; var gulp = require('gulp'); |
這樣我們在執行檔案,意想不到的事情發生了,仍報出“MODULE_NOT_FOUND”錯誤。
這是為什麼呢?原因仍要追溯到原始碼。在原始碼分析小節中總結了三點,其中第二點提到了**Module._initPaths函式只執行一次,這意味著當我們在程式碼中設定了process.env.NODE_PATH=’/usr/lib/node_modules’;,可是由於此時Module._initPaths已執行完畢,因此設定的環境變數並沒有被使用。解決這個問題也比較簡單,即重新呼叫Module._initPaths**即可。
1 2 3 4 5 6 |
// 當前目錄: /usr/local/test/index.js // gulp模組所在路徑為 /usr/lib/node_modules process.env.NODE_PATH='/usr/lib/node_modules'; require('module').Module._initPaths(); // 或者 module.constructor._initPaths() var gulp = require('gulp'); |
這樣,安全無公害的解決了多基目錄下依賴呼叫的問題。
總結
本文從實際開發中遇到的問題出發,提出了幾種解決多基目錄下依賴的幾種方案:
- 全域性變數法
- 修改module.paths方法
- 環境變數法(三種實現)
當然,社群還有一些幫助解決這種問題的模組,如“app-module-path”,但思想也大同小異。在這裡和大家一起分享學習收穫,希望對各位有些啟發和感悟,不勝感激!
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!