技术分享:你真的很清楚nodejs里的require吗

在日常的前端/Node开发中,require是最常使用的api之一,了解其背后的逻辑有助于我们日常的开发以及排坑。但在阅读源码的时候,我们往往会觉得枯燥无味,看完了,看起来好像也知道了代码是怎么跑,但很难跳出这个流程,去对它进行hack,进行补充。

本文不会从头开始讲解require被调用后,里面发生的所有事情,如果你感兴趣,可以自己去源码(node/lib/internal/modules/cjs/loader.js)里翻看,或者自行去网上搜索关键字去阅读别人写的源码解析。本文是希望带给读者在已经大致了解了require背后的发生的事情后,是否可以抓住内部实现的关键点,实现一些好玩并且有意义的事情。

ps:本文不需要你提前阅读过相关的源码,但起码你曾经使用过这个require这个api,如果知道一些内部原理更佳。

2.前戏工作

在正式开始之前,我们先通过一行简单的代码,并使用流程图记录一下这段代码在Node运行时发生甚么事:

  1. 代码如下:

    const xx_mod = require("xx_mod");

  2. 流程图如下所示: 技术分享:你真的很清楚nodejs里的require吗_第1张图片

  3. 在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.

从流程图上看,当我们需要加载某一模块时,会先走缓存。如果缓存里没有,就用对应的加载器去加载它,并存到缓存里。本文会围绕缓存加载器去挖掘一些有意思的内容。

3.内容挖掘

话题一:代码热更新

按照我个人对热更新的理解(发现不对的请帮忙指正),热更新是指在不重启应用的前提下,实现对文件内容的更新。

举个例子,假如你写了一个模块的叫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;
}
复制代码

从这一行代码中,我们可以想到的最简单的方案就是:

  1. 通过文件监听,监听出被修改的文件,得到filename.

  2. 引入Module,直接delete Module._cache[filename];

  3. 再重新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
 

技术分享:你真的很清楚nodejs里的require吗_第2张图片

 

你可能感兴趣的:(前端开发,web开发)