在日常的前端/Node开发中,require
是最常使用的api之一,了解其背后的逻辑有助于我们日常的开发以及排坑。但在阅读源码的时候,我们往往会觉得枯燥无味,看完了,看起来好像也知道了代码是怎么跑,但很难跳出这个流程,去对它进行hack,进行补充。
本文不会从头开始讲解require
被调用后,里面发生的所有事情,如果你感兴趣,可以自己去源码(node/lib/internal/modules/cjs/loader.js
)里翻看,或者自行去网上搜索关键字去阅读别人写的源码解析。本文是希望带给读者在已经大致了解了require
背后的发生的事情后,是否可以抓住内部实现的关键点,实现一些好玩并且有意义的事情。
ps:本文不需要你提前阅读过相关的源码,但起码你曾经使用过这个require
这个api,如果知道一些内部原理更佳。
在正式开始之前,我们先通过一行简单的代码,并使用流程图记录一下这段代码在Node运行时发生甚么事:
代码如下:
const xx_mod = require("xx_mod");
在Node源码里的注释:
// Check the cache for the requested file.
// 1. If a module already exists in the cache: return its exports object.
// 2. If the module is native: call // NativeModule.prototype.compileForPublicLoader()
and return the exports.
// 3. Otherwise, create a new module for the file and save it to the cache. // Then have it load the file contents before returning its exports // object.
从流程图上看,当我们需要加载某一模块时,会先走缓存。如果缓存里没有,就用对应的加载器去加载它,并存到缓存里。本文会围绕缓存和加载器去挖掘一些有意思的内容。
话题一:代码热更新
按照我个人对
热更新
的理解(发现不对的请帮忙指正),热更新
是指在不重启
应用的前提下,实现对文件内容的更新。
举个例子,假如你写了一个模块的叫xx_mod.js,内容如下:
//xx_mode.jsconst _str = "hello world"
module.export = { sayHi: ()=>{ return _str; }}
复制代码
当你在程序中加载这个模块,并使用其导出的sayHi
时,它会返回 "hello world",但是你现在又有了新的idea,想让它返回"hello 个",通常的做法,就是修改xx_mode.js
,再重启这个应用,这时候就会返回"hello 个",但是热更新
可以做到不重启
应用,在你修改xx_mode.js
完成后,也能实现返回"hello 个"。
回到第二节,我们在提起模块被加载时的过程中有一个缓存的概念。这和实现热更新
是互相矛盾的。在不重启的情况下,之前加载xx_mod.js
不就已经被缓存住了?怎么实现更新呢?聪明的同学肯定早就想到了办法:删缓存
。
大致可以贴一下删减版的源码:
Module._load = function(request, parent, isMain) {
//...
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
if (!cachedModule.loaded) {
const parseCachedModule = cjsParseCache.get(cachedModule);
if (!parseCachedModule || parseCachedModule.loaded)
return getExportsForCircularRequire(cachedModule);
parseCachedModule.loaded = true;
} else {
return cachedModule.exports;
}
}
// mod = 加载器返回;
// Module._cache[filename] = mod;
// return module.exports = mod;
}
复制代码
从这一行代码中,我们可以想到的最简单的方案就是:
通过文件监听,监听出被修改的文件,得到filename
.
引入Module,直接delete Module._cache[filename];
再重新require(filename)
但这里面还有一些深坑,可能会造成内存泄漏,感兴趣的可以去看看这篇文章:https://zhuanlan.zhihu.com/p/34702356
话题二:hack模块加载器
这个是我在上家公司做测试相关工作时,同事问我的一个问题,大致内容如下:
fucntion _private(){
//do something
}
module.exports = function _public(){
// ....
_private();
// ....
}
复制代码
同事对外开放的是_public方法,现在要求他对他写的js模块做单元测试,并且要求coverage 100%,但是他不想把_private暴露出去。
如果图方便,直接用rewire
这个npm包即可,他可以做到这一切。感兴趣的可以仔细阅读它里面的代码,它其实就是对原生的模块加载器进行了套娃从而满足自己需求。为了方便阅读,我这里贴出可运行的精简版代码:
你也可以去这里下载代码:https://github.com/huenchao/simple_rewire
.
├── __test__.js //测试文件
├── libhack
│ └── index.js //精简版的 rewire
└── library.js //同事写的js库
复制代码
library.js :
//library.js
function _private(){
return "_private";
}
module.exports = function _public(){
//... whatever
return _private();
}
复制代码
__ test__.js :
const assert = require("assert");
const lib = require("./libhack");
//这里既想对 _public 方法做单元测试
console.log(lib("./library.js").getPrivateMember("_private")());
assert(lib("./library.js").getPrivateMember("_private")() === '_private');
复制代码
libhack/index.js:
const Module = module.constructor;
function _getPrivateMember() {
arguments.varName = arguments[0];
if (arguments.varName && typeof arguments.varName === "string") {
return eval(arguments.varName);
} else {
throw new TypeError("__get__ expects a non-empty string");
}
}
function _hackCode(){
return "\n Object.defineProperty(module.exports, \"getPrivateMember\", {enumerable: false, value: " + _getPrivateMember.toString() + ", " + "writable: true}); ";
}
function _fakeModule(parentModule, id) {
const targetPath = Module._resolveFilename(id, parentModule);
const targetModule = new Module(targetPath, parentModule);
Module.wrapper[1] = _hackCode() + Module.wrapper[1];
targetModule.load(targetModule.id);
return targetModule.exports;
}
function injectLib(fileName) {
return _fakeModule(module.parent, fileName)
}
module.exports = injectLib;
复制代码
运行的结果就是:
输出:_private 并且assert通过。
如果大家想学习前端方面的技术,我把我多年的经验分享给大家,还有一些学习资料,分享Q群:1046097531