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