Node.js 中的 require 是如何工作的?
作者:FESKY 鏈接:https://juejin.im/post/6844903957752463374
作為前端開發(fā)者,不可避免每天都要跟 Node.js 打交道。Node 遵循 Commonjs 規(guī)范,規(guī)范的核心是通過 require 來加載依賴的其他模塊。我們已經(jīng)常習慣于使用社區(qū)提供的各種庫,但對于模塊引用的背后原理知之甚少。這篇文章通過源碼閱讀,淺析在 commonjs 規(guī)范中 require 背后的工作原理。
require 從哪里來?
大家都知道,在 node js 的模塊/文件中,有些“全局”變量是可以直接使用的,比如 require, module, __dirname, __filename, exports。其實這些變量或方法并不是“全局”的,而是在 commonjs 模塊加載中, 通過包裹的形式,提供的局部變量。
module.exports?=?function?()?{
????console.log(__dirname);
}
經(jīng)過 compile 之后,就有了 module,__dirname 等變量可以直接使用。
(function?(exports,?require,?module,?__filename,?__dirname)?{
????module.exports?=?function?()?{
????????console.log(__dirname);
????}
})
這也可以很好解答初學者常常會困惑的問題,為什么給 exports 賦值,require 之后得到的結果是 undefined?
//?直接給?exports?賦值是不會生效的
(function?(exports,?module)?{
????exports?=?function?()?{
????}
})(m.exports,?m)
return?m.exports;
直接賦值只是修改了局部變臉 exports 的值。最終 export 出去的 module.exports 沒有被賦值。
require 的查找過程
文檔中描述得非常清楚,簡化版 require 模塊的查找過程如下:在 Y 路徑下,require(X)
如果X是內置模塊(http, fs, path 等), 直接返回內置模塊,不再執(zhí)行 如果 X 以 '/' 開頭,把 Y 設置為文件系統(tǒng)根目錄 如果 X 以 './', '/', '../' 開頭 a. 按照文件的形式加載(Y + X),根據(jù) extensions 依次嘗試加載文件 [X, X.js, X.json, X.node] 如果存在就返回該文件,不再繼續(xù)執(zhí)行。b. 按照文件夾的形式加載(Y + X),如果存在就返回該文件,不再繼續(xù)執(zhí)行,若找不到將拋出錯誤 a. 嘗試解析路徑下 package.json main 字段 b. 嘗試加載路徑下的 index 文件(index.js, index.json, index.node) 搜索 NODE_MODULE,若存在就返回模塊 a. 從路徑 Y 開始,一層層往上找,嘗試加載(路徑 + 'node_modules/' + X) b. 在 GLOBAL_FOLDERS node_modules 目錄中查找 X 拋出 "Not Found" Error 復制代碼例如在 /Users/helkyle/projects/learning-module/foo.js` 中 require('bar') 將會從`/Users/helkyle/projects/learning-module/開始逐層往上查找bar模塊(不是以'./', '/', '../'開頭)。
'/Users/helkyle/projects/learning-module/node_modules',
'/Users/helkyle/projects/node_modules',
'/Users/helkyle/node_modules',
'/Users/node_modules',
'/node_modules'
需要注意的是,在使用 npm link 功能的時候,被 link 模塊內的 require 會以被 link 模塊在文件系統(tǒng)中的絕對路徑進行查找,而不是 main module 所在的路徑。舉個例子,假設有兩個模塊。
/usr/lib/foo
/usr/lib/bar
通過 link 形式在 foo 模塊中 link bar,會產生軟連 /usr/lib/foo/node_modules/bar 指向 /usr/lib/bar,這種情況下 bar 模塊下 require('quux') 的查找路徑是 /usr/lib/bar/node_modules/而不是 /usr/lib/foo/node_modules我之前踩過的坑
Cache 機制
在實踐過程中能了解到,實際上 Node module require 的過程會有緩存。也就是兩次 require 同一個 module會得到一樣的結果。
//?a.js
module.exports?=?{
????foo:?1,
};
//?b.js
const?a1?=?require('./a.js');
a1.foo?=?2;
const?a2?=?require('./a.js');
console.log(a2.foo);?//?2
console.log(a1?===?a2);?//?true
執(zhí)行 node b.js,可以看到,第二次 require a.js 跟第一次 require 得到的是相同的模塊引用。從源碼上看,require 是對 module 常用方法的封裝。
function?makeRequireFunction(mod,?redirects)?{
??const?Module?=?mod.constructor;
??let?require;
??//?簡化其他代碼
??require?=?function?require(path)?{
????return?mod.require(path);
??};
??function?resolve(request,?options)?{
????validateString(request,?'request');
????return?Module._resolveFilename(request,?mod,?false,?options);
??}
??require.resolve?=?resolve;
??function?paths(request)?{
????validateString(request,?'request');
????return?Module._resolveLookupPaths(request,?mod);
??}
??resolve.paths?=?paths;
??require.main?=?process.mainModule;
??require.extensions?=?Module._extensions;
??require.cache?=?Module._cache;
??return?require;
}
跟蹤代碼看到,require() 最終調用的是 Module._load 方法:// 忽略代碼,看看 load 的過程發(fā)生了什么?
Module._load?=?function(request,?parent,?isMain)?{
??//?調用?_resolveFilename?獲得模塊絕對路徑
??const?filename?=?Module._resolveFilename(request,?parent,?isMain);
??const?cachedModule?=?Module._cache[filename];
??if?(cachedModule?!==?undefined)?{
????//?如果存在緩存,直接返回緩存的?exports?對象
????return?cachedModule.exports;
??}
??//?內建模塊直接返回
??const?mod?=?loadNativeModule(filename,?request,?experimentalModules);
??if?(mod?&&?mod.canBeRequiredByUsers)?return?mod.exports;
??//?創(chuàng)建新的?module?對象
??const?module?=?new?Module(filename,?parent);
??//?main?module?特殊處理
??if?(isMain)?{
????process.mainModule?=?module;
????module.id?=?'.';
??}
??//?緩存?module
??Module._cache[filename]?=?module;
??
??//?返回?module?exports?對象
??return?module.exports;
};
到這里,module cache 的原理也很清晰,模塊在首次加載后,會以模塊絕對路徑為 key 緩存在 Module._cache屬性上,再次 require 時會直接返回已緩存的結果以提高 效率。在控制臺打印 require.cache 看看。
//?b.js
require('./a.js');
require('./a.js');
console.log(require.cache);
緩存中有兩個key,分別是 a.js, b.js 文件在系統(tǒng)中的絕對路徑。value 則是對應模塊 load 之后的 module 對象。所以第二次 require('./a.js') 的結果是 require.cache['/Users/helkyle/projects/learning-module/a.js'].exports 和第一次 require 指向的是同一個 Object。
{?
????'/Users/helkyle/projects/learning-module/b.js':?
???????Module?{
?????????id:?'.',
?????????exports:?{},
?????????parent:?null,
?????????filename:?'/Users/helkyle/projects/learning-module/b.js',
?????????loaded:?false,
?????????children:?[?[Object]?],
?????????paths:?
??????????[?'/Users/helkyle/projects/learning-module/node_modules',
????????????'/Users/helkyle/projects/node_modules',
????????????'/Users/helkyle/node_modules',
????????????'/Users/node_modules',
????????????'/node_modules'?]?},
??'/Users/helkyle/projects/learning-module/a.js':?
???????Module?{
?????????id:?'/Users/helkyle/projects/learning-module/a.js',
?????????exports:?{?foo:?1?},
?????????parent:?
??????????Module?{
????????????id:?'.',
????????????exports:?{},
????????????parent:?null,
????????????filename:?'/Users/helkyle/projects/learning-module/b.js',
????????????loaded:?false,
????????????children:?[Array],
????????????paths:?[Array]?},
?????????filename:?'/Users/helkyle/projects/learning-module/a.js',
?????????loaded:?true,
?????????children:?[],
?????????paths:?[?
????????????'/Users/helkyle/projects/learning-module/node_modules',
????????????'/Users/helkyle/projects/node_modules',
????????????'/Users/helkyle/node_modules',
????????????'/Users/node_modules',
????????????'/node_modules'?
????????]
???}
}
應用——實現(xiàn) Jest 的 mock module 效果
jest ?是 Facebook 開源的前端測試庫,提供了很多非常強大又實用的功能。mock module 是其中非常搶眼的特性。使用方式是在需要被 mock 的文件模塊同級目錄下的 __mock__ 文件夾添加同名文件,執(zhí)行測試代碼時運行 jest.mock(modulePath),jest 會自動加載 mock 版本的 module。舉個例子,項目中有個 apis 文件,提供對接后端 api。
//?/projects/foo/apis.js
module.export?=?{
????getUsers:?()?=>?fetch('api/users')
};
在跑測試過程中,不希望它真的連接后端請求。這時候根據(jù) jest 文檔,在 apis 文件同級目錄創(chuàng)建 mock file
//?/projects/foo/__mock__/apis.js
module.exports?=?{
????getUsers:?()?=>?[
????????{
????????????id:?"1",
????????????name:?"Helkyle"
????????},
????????{
????????????id:?"2",
????????????name:?"Chinuketsu"
????????}
????]
}
測試文件中,主動調用 jest.mock('./apis.js') 即可。
jest.mock('./apis.js');
const?apis?=?require('./apis.js');
apis.getUsers()
??.then((users)?=>?{
????console.log(users);
????//?[?{?id:?'1',?name:?'Helkyle'?},?{?id:?'2',?name:?'Chinuketsu'?}?]
??})
了解 require 的基礎原理之后,我們也來實現(xiàn)類似的功能,將加載 api.js 的語句改寫成加載 mock/api.js。
使用 require.cache
由于緩存機制的存在,提前寫入目標緩存,再次 require 將得到我們期望的結果。
//?提前 require mock apis 文件,產生緩存。
require('./__mock__/apis.js');
//?給即將?require?的文件路徑寫入緩存
const?originalPath?=?require.resolve('./apis.js');
require.cache[originalPath]?=?require.cache[require.resolve('./__mock__/apis.js')];
//?得到的將是緩存版本
const?apis?=?require('./apis.js');
apis.getUsers()
??.then((users)?=>?{
????console.log(users);
????//?[?{?id:?'1',?name:?'Helkyle'?},?{?id:?'2',?name:?'Chinuketsu'?}?]
??})
魔改 module._load
基于 require.cache 的方式,需要提前 require mock module。?提到了,由于最終都是通過 Module._load來加載模塊,在這個位置進行攔截即可完成按需 mock。
const?Module?=?require('module');
const?originalLoad?=?Module._load;
Module._load?=?function?(path,?...rest)?{
??if?(path?===?'./apis.js')?{
????path?=?'./__mock__/apis.js';
??}
??return?originalLoad.apply(Module,?[path,?...rest]);
}
const?apis?=?require('./apis.js');
apis.getUsers()
??.then((users)?=>?{
????console.log(users);
??})
注意:以上內容僅供參考。從實際運行結果上看,Jest 有自己實現(xiàn)的模塊加載機制,跟 commonjs 有出入。比如在 jest 中 require module 并不會寫入 require.cache。
程序啟動時的 require
查閱 Node 文檔發(fā)現(xiàn),在 Command Line 章節(jié)也有一個 --require ,使用這個參數(shù)可以在執(zhí)行業(yè)務代碼之前預先加載特定模塊。舉個例子,編寫 setup 文件,往 global 對象上掛載 it, assert 等方法。
//?setup.js
global.it?=?async?function?test(title,?callback)?{
??try?{
????await?callback();
????console.log(`??${title}`);
??}?catch?(error)?{
????console.error(`??${title}`);
????console.error(error);
??}
}
global.assert?=?require('assert');
給啟動代碼添加 --require 參數(shù)。引入 global.assert, global.it,就可以在代碼中直接使用 assert, it 不用在測試文件中引入。
node?--require?'./setup.js'?foo.test.js
//?foo.test.js
//?不需要?require('assert');
function?sum?(a,?b)?{
????return?a?+?b;
}
//?沒有?--require?會報?it?is?not?defined
it('add?two?numbers',?()?=>?{
????assert(sum(2,?3)?===?5);
})
??愛心三連擊
1.看到這里了就點個在看支持下吧,你的「在看」是我創(chuàng)作的動力。
2.關注公眾號
程序員成長指北,回復「1」加入Node進階交流群!「在這里有好多 Node 開發(fā)者,會討論 Node 知識,互相學習」!3.也可添加微信【ikoala520】,一起成長。
“在看轉發(fā)”是最大的支持
