Node 中如何引入一個模塊及其細節(jié)
在 node 環(huán)境中,有兩個內(nèi)置的全局變量無需引入即可直接使用,并且無處不見,它們構(gòu)成了 nodejs 的模塊體系: module 與 require。以下是一個簡單的示例
const?fs?=?require('fs')
const?add?=?(x,?y)?=>?x?+?y
module.exports?=?add
雖然它們在平常使用中僅僅是引入與導(dǎo)出模塊,但稍稍深入,便可見乾坤之大。在業(yè)界可用它們做一些比較 trick 的事情,雖然我不大建議使用這些黑科技,但稍微了解還是很有必要。
- 如何在不重啟應(yīng)用時熱加載模塊?如
require一個 json 文件時會產(chǎn)生緩存,但是重寫文件時如何watch - 如何通過不侵入代碼進行打印日志
- 循環(huán)引用會產(chǎn)生什么問題?
module wrapper
當(dāng)我們使用 node 中寫一個模塊時,實際上該模塊被一個函數(shù)包裹,如下所示:
(function(exports,?require,?module,?__filename,?__dirname)?{
??//?所有的模塊代碼都被包裹在這個函數(shù)中
??const?fs?=?require('fs')
??const?add?=?(x,?y)?=>?x?+?y
??module.exports?=?add
});
因此在一個模塊中自動會注入以下變量:
exportsrequiremodule__filename__dirname
module
調(diào)試最好的辦法就是打印,我們想知道 module 是何方神圣,那就把它打印出來!
const?fs?=?require('fs')
const?add?=?(x,?y)?=>?x?+?y
module.exports?=?add
console.log(module)

module.id: 如果是.代表是入口模塊,否則是模塊所在的文件名,可見如下的koamodule.exports: 模塊的導(dǎo)出
koa modulemodule.exports 與 exports
?`module.exports` 與 `exports` 有什么關(guān)系?[1]
?
從以下源碼中可以看到 module wrapper 的調(diào)用方 module._compile 是如何注入內(nèi)置變量的,因此根據(jù)源碼很容易理解一個模塊中的變量:
exports: 實際上是module.exports的引用require: 大多情況下是Module.prototype.requiremodule__filename__dirname:path.dirname(__filename)
//?/internal/modules/cjs/loader.js:1138
Module.prototype._compile?=?function(content,?filename)?{
??//?...
??const?dirname?=?path.dirname(filename);
??const?require?=?makeRequireFunction(this,?redirects);
??let?result;
??//?從中可以看出:exports = module.exports
??const?exports?=?this.exports;
??const?thisValue?=?exports;
??const?module?=?this;
??if?(requireDepth?===?0)?statCache?=?new?Map();
??if?(inspectorWrapper)?{
????result?=?inspectorWrapper(compiledWrapper,?thisValue,?exports,
??????????????????????????????require,?module,?filename,?dirname);
??}?else?{
????result?=?compiledWrapper.call(thisValue,?exports,?require,?module,
??????????????????????????????????filename,?dirname);
??}
??//?...
}
require
通過 node 的 REPL 控制臺,或者在 VSCode 中輸出 require 進行調(diào)試,可以發(fā)現(xiàn) require 是一個極其復(fù)雜的對象
require從以上 module wrapper 的源碼中也可以看出 require 由 makeRequireFunction 函數(shù)生成,如下
//?/internal/modules/cjs/helpers.js:33
function?makeRequireFunction(mod,?redirects)?{
??const?Module?=?mod.constructor;
??let?require;
??if?(redirects)?{
????//?...
??}?else?{
????//?require?實際上是?Module.prototype.require
????require?=?function?require(path)?{
??????return?mod.require(path);
????};
??}
??function?resolve(request,?options)?{?//?...?}
??require.resolve?=?resolve;
??function?paths(request)?{
????validateString(request,?'request');
????return?Module._resolveLookupPaths(request,?mod);
??}
??resolve.paths?=?paths;
??require.main?=?process.mainModule;
??//?Enable?support?to?add?extra?extension?types.
??require.extensions?=?Module._extensions;
??require.cache?=?Module._cache;
??return?require;
}
?關(guān)于
?require更詳細的信息可以去參考官方文檔: Node API: require[2]
require(id)
require 函數(shù)被用作引入一個模塊,也是平常最常見最常用到的函數(shù)
//?/internal/modules/cjs/loader.js:1019
Module.prototype.require?=?function(id)?{
??validateString(id,?'id');
??if?(id?===?'')?{
????throw?new?ERR_INVALID_ARG_VALUE('id',?id,
????????????????????????????????????'must?be?a?non-empty?string');
??}
??requireDepth++;
??try?{
????return?Module._load(id,?this,?/*?isMain?*/?false);
??}?finally?{
????requireDepth--;
??}
}
而 require 引入一個模塊時,實際上通過 Module._load 載入,大致的總結(jié)如下:
- 如果
Module._cache命中模塊緩存,則直接取出module.exports,加載結(jié)束 - 如果是
NativeModule,則loadNativeModule加載模塊,如fs、http、path等模塊,加載結(jié)束 - 否則,使用
Module.load加載模塊,當(dāng)然這個步驟也很長,下一章節(jié)再細講
//?/internal/modules/cjs/loader.js:879
Module._load?=?function(request,?parent,?isMain)?{
??let?relResolveCacheIdentifier;
??if?(parent)?{
????//?...
??}
??const?filename?=?Module._resolveFilename(request,?parent,?isMain);
??const?cachedModule?=?Module._cache[filename];
??//?如果命中緩存,直接取緩存
??if?(cachedModule?!==?undefined)?{
????updateChildren(parent,?cachedModule,?true);
????return?cachedModule.exports;
??}
??//?如果是?NativeModule,加載它
??const?mod?=?loadNativeModule(filename,?request);
??if?(mod?&&?mod.canBeRequiredByUsers)?return?mod.exports;
??//?Don't?call?updateChildren(),?Module?constructor?already?does.
??const?module?=?new?Module(filename,?parent);
??if?(isMain)?{
????process.mainModule?=?module;
????module.id?=?'.';
??}
??Module._cache[filename]?=?module;
??if?(parent?!==?undefined)?{?//?...?}
??let?threw?=?true;
??try?{
????if?(enableSourceMaps)?{
??????try?{
????????//?如果不是?NativeModule,加載它
????????module.load(filename);
??????}?catch?(err)?{
????????rekeySourceMap(Module._cache[filename],?err);
????????throw?err;?/*?node-do-not-add-exception-line?*/
??????}
????}?else?{
??????module.load(filename);
????}
????threw?=?false;
??}?finally?{
????//?...
??}
??return?module.exports;
};
require.cache
「當(dāng)代碼執(zhí)行 require(lib) 時,會執(zhí)行 lib 模塊中的內(nèi)容,并作為一份緩存,下次引用時不再執(zhí)行模塊中內(nèi)容」。
這里的緩存指的就是 require.cache,也就是上一段指的 Module._cache
//?/internal/modules/cjs/loader.js:899
require.cache?=?Module._cache;
這里有個小測試:
?有兩個文件:
?index.js與utils.js。utils.js中有一個打印操作,當(dāng)index.js引用utils.js多次時,utils.js中的打印操作會執(zhí)行幾次。代碼示例如下
「index.js」
//?index.js
//?此處引用兩次
require('./utils')
require('./utils')
「utils.js」
//?utils.js
console.log('被執(zhí)行了一次')
「答案是只執(zhí)行了一次」,因此 require.cache,在 index.js 末尾打印 require,此時會發(fā)現(xiàn)一個模塊緩存
//?index.js
require('./utils')
require('./utils')
console.log(require)

那回到本章剛開始的問題:
?如何不重啟應(yīng)用熱加載模塊呢?
?
答:「刪掉 Module._cache」,但同時會引發(fā)問題,如這種 一行 delete require.cache 引發(fā)的內(nèi)存泄漏血案[3]
所以說嘛,這種黑魔法大幅修改核心代碼的東西開發(fā)環(huán)境玩一玩就可以了,千萬不要跑到生產(chǎn)環(huán)境中去,畢竟黑魔法是不可控的。
總結(jié)
- 模塊中執(zhí)行時會被
module wrapper包裹,并注入全局變量require及module等 module.exports與exports的關(guān)系實際上是exports = module.exportsrequire實際上是module.requirerequire.cache會保證模塊不會被執(zhí)行多次- 不要使用
delete require.cache這種黑魔法?
??看完三件事
如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:
- 點贊,讓更多的人也能看到介紹內(nèi)容(收藏不點贊,都是耍流氓-_-)
- 關(guān)注公眾號“前端勸退師”,不定期分享原創(chuàng)知識。
- 也看看其他文章
勸退師個人微信:huab119
也可以來我的GitHub博客里拿所有文章的源文件:
前端勸退指南:https://github.com/roger-hiro/BlogFN一起玩耍呀。
Reference
[1]module.exports 與 exports 有什么關(guān)系?: https://github.com/shfshanyue/Daily-Question/issues/351
Node API: require: https://nodejs.org/api/modules.html#modules_require_id
[3]一行 delete require.cache 引發(fā)的內(nèi)存泄漏血案: https://zhuanlan.zhihu.com/p/34702356
[4]shfshanyue/blog: https://github.com/shfshanyue/blog
[5]前端工程化系列: https://github.com/shfshanyue/blog#%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96
[6]Node進階系列: https://github.com/shfshanyue/blog#node-%E5%AE%9E%E8%B7%B5
