引題
用過node的同學應該都知道require是用來加載模塊的,那你是否存在如下的疑問呢?
1. require(path)是如何依據path找到對應module呢?
2. 為何在模塊定義中,一定要通過module.exports暴漏出接口?module.exports與require存在什么關系
對上述問題進行概括可以抽象出如下兩個問題:
1. module的路徑分析
2. 文件加載
切入
首先來直觀地看看require是什么?
// node環境下執行: console.log(require.toString) //輸入結果為: 'function require(path) {\n return self.require(path);\n }'
上述代碼說明require函數僅僅是module.require的封裝,這樣就需要查看node中的module源代碼了。
加載模塊的方式
首先來直觀來認識一下node的模塊加載方式有哪些方式:
case 1:
// 'path'為node的核心模塊
var path = require('path')
case2:
// a.js,路徑為: basePath/a.js var myModule = require('./my-module')
// my-module的路徑為basePath/node_modules/myModule.js
case 3:
// a.js, 路徑: basePath/a.js var main = require('./')
// basePath下還包括package.json, index.js
路徑解析
在node的官方API中,我們可以找到這段描述:
To get the exact filename that will be loaded when require() is called, use the require.resolve() function.
Putting together all of the above, here is the high-level algorithm in pseudocode of what require.resolve does:
......
試試在node環境下用用require.resolve這個API:
require.resolve('./a.js') // 這樣就得到a.js的絕對路徑
為了探索緣由,就從node核心代碼中的mdoule.js找答案吧:
require.resolve = function(request) { return Module._resolveFilename(request, self); } Module._resolveFilename = function(request, parent) { // 判斷是否為node的核心模塊 if (NativeModule.exists(request)) { return request; } // 得到查詢路徑,格式為數組:[id, [paths]] var resolvedModule = Module._resolveLookupPaths(request, parent); var paths = resolvedModule[1]; // 根據path、fileName得到絕對路徑 var filename = Module._findPath(request, paths); return filename; }
那Module._resolveLookupPaths是如何得到所有查詢路徑的呢?
- 為node的核心模塊,stop
- 以./或../開頭,本地查找, stop
- 沿着文件樹,得到node_module的所有路徑,直到/node_modules,在node_module中查找,stop
- path為目錄,則檢查package.json文件是否存在main屬性,否則默認為index.js
- 最后返回new Error('Cannot find module"' + request + '"');
模塊加載
先看require的源代碼:
// 我們經常使用的require函數 function require(path) { return self.require(path); } // 調用_load函數,加載所需的模塊 Module.prototype.require = function(path) { return Module._load(path, this); }
這樣模塊函數的調用連接到了Module._load函數:
Module.cache = {}; Module._load = function() { // 檢測模塊是否已經加載過 var cachedModule = Module._cache[filename]; if (cachedModule) { return cachedModule.exports; } // 模塊還未加載,則為模塊創建module實例 var module = new Module(filename, parent); // 新創建的實例存儲於cache中 Module._cache[filename] = module; // 開始獲取模塊的內容 module.load(filename); // 對外提供接口 return module.exports; }
接下來問題的關鍵就變成了module.load,該方法用於獲取module的內容,然后進行解析:
Module.prototype.load = function(filename) { // 解析出文件的后綴, 存在['.js', '.json', 'node']三種后綴 var extension = path.extname(filename) || '.js'; // 根據后綴,獲取相關的模塊 Module._extensions[extension](this, filename); }
node會匹配按照.js、.json、.node三種格式進行模塊匹配,根據文件類型的不同采取不同的加載策略,但是以實際開發中以加載.js最多,該種策略最后需要調用Module.prototype._compile進行編譯處理:
Module._extensions['.js'] = function(module, filename) { var content = fs.readFileSync(filename, 'utf8'); module._compile(stripBOM(content), filename); }; Module.prototype._compile = function(content, filename) { //將內容放入到(function() { content }),形成閉包,創建私有作用域 var wrapper = Module.wrap(content); // bind新的執行上下文 var compiledWrapper = runInThisContext(wrapper, { filename: filename }); // 向外暴漏接口:module.exports, require, module,__filename, __dirname, var args = [self.exports, require, self, filename, dirname]; return compiledWrapper.apply(self.exports, args); }
這樣,我們就可以在require來獲取相應地module。
結論
node現在這么火,各種優勢鋪天蓋地涌來,會讓剛剛入行的人覺得深不可測,因而往往會讓人望而卻步。但是只要我們敢於突破第一步,深入下來仔細分析,就會發現其實沒有那么晦澀難懂,踏出第一步真的很關鍵!
參考資料
http://thenodeway.io/posts/get-fancy/how-require-actually-works/
https://github.com/joyent/node/blob/master/lib/module.js