macOS和node.js之间的小误会

谨以此文献给个人挚友乔治G同窗!!!愿他打败一切恶魔和狗腿

事情的原由是这样的,乔治G在他的电脑上作了一个小测试,但结果和预期的大不相同。javascript

那么咱们先来看看这个小测试都写了什么:html

一共三个文件,代码总计不超过15行java

parent.jsnode

class Parent {}

module.exports = Parent

son.jswebpack

//加载时把模块文件名首字母大写了(不正确的)
const Parent = require('./Parent')

class Son extends Parent {}

module.exports = Son

test.jsgit

//加载时把模块名首字母大写(不正确的)
const ParentIncorrect = require('./Parent')
//经过正确的模块文件名加载(正确)
const Parent = require('./parent')

const Son = require('./son')

const ss = new Son()

//测试结果
console.log(ss instanceof Parent) // false
console.log(ss instanceof ParentIncorrect) // true

乔治G同窗有如下疑问:github

  1. son.jstest.js里都有错误的文件名(大小写问题)引用,为何不报错?
  2. 测试结果,为何ss instanceof ParentIncorrect === true?不报错我忍了,为何还认贼做父,说本身是那个经过不正确名字加载出来的模块的instance?

若是同窗你对上述问题已经了然于胸,恭喜你,文能提笔安天,武能上马定乾坤;上炕认识娘们,下炕认识鞋!web

但若是你也不是很清楚为何?那么好了,我有的说,你有的看。bootstrap

其实断症(装逼范儿的debug)之法和中医看病也有类似指出,望、闻、问、切四招能够按需选取一二来寻求答案。ubuntu

代码很少,看了一会,即使没有个人注释,相信仔细的同窗也都发现真正的文件名和代码中引入时有出入的,那么这里确定是有问题的,问题记住,咱们继续

这个就算了,代码我也闻不出个什么鬼来

来吧,软件工程里很重要的一环,就是沟通,不见得是和遇到bug的同事,多是本身,多是QA,固然也多是PM或者你的老板。你没问出本身想知道的问题;他没说清楚本身要回答的;都完蛋。。。。

那么我想知道什么呢?下面两件事做为debug的入口比较合理:

  1. 操做系统
  2. 运行环境 + 版本
  3. 你怎么测试的,命令行仍是其余什么手段

答曰:macOS; node.js > 8.0;命令行node test.js

激动人心的深入到来了,我要动手了。(为了完整的描述debug过程,我会伪装这下面的全部事情我事先都是不知道的)

准备电脑,完毕

准备运行环境node.js > 9.3.0, 完毕

复刻代码,完毕

运行,日了狗,果真没报错,并且运行结果就是乔治G说的那样。

为了证实我没瞎,我又尝试在test.jsrequire了一个压根不存在的文件require('./nidayede'),运行代码。

还好此次报错了Error: Cannot find module './nidayede',因此我没疯。这点真使人高兴。

因而有了第一个问题

为何狗日的模块名大小写都错了,还能加载?

会不会和操做系统有关系?来咱们再找台ubuntu试试,果真,到了ubuntu上,大小写问题就是个问题了,Error: Cannot find module './Parent'。(经朋友提醒,windows也是默认大小写不敏感的,因此以前举例说windows会报错,应该也是我本身早前修改过注册表缘故)。

那么macOS到底在干什么?连个大小写都分不出来么?因而赶忙google(别问我为何不baidu)

图片描述

原来人家牛逼的 OS X默认用了 case-insensitive的文件系统( 详细文档)。

but why?这么反人类的设计究竟是为了什么?

图片描述

更多解释,来,走你

因此,这就是你不报错的理由?(对node.js指责道),但这就是所有真相了。

但事情没完

那认贼做父又是个什么鬼?

依稀有听过node.js里有什么缓存,是那个东西引发的么?因而抱着试试看的心情,我把const ParentIncorrect = require('./Parent')const Parent = require('./parent')换了下位置,心想,这样最早按照正确的名字加载,会不会就对了呢?

果真,仍是不对。靠猜和装逼是不可以真正解决问题的

那比比ParentIncorrectParent呢?因而我写了console.log(ParentIncorrect === Parent),结果为false。因此他俩还真的不是同一个东西,那么说明问题可能在引入的部分喽?

因而一个装逼看node.js源码的想法诞生了(其实不看,问题最终也能想明白)。 日了狗,怀着忐忑的心情,终于clone了一把node.js源码(花了很久,真tm慢)

来,咱们一块儿进入神秘的node.js源码世界。既然咱们的问题是有关require的,那就从她开始吧,不过找到require定义的过程须要点耐心,这里不详述,只说查找的顺序吧

src/node_main.cc => src/node.cc => lib/internal/bootstrap_node.js => lib/module.js

找到咯,就是这个lib/module.js,进入正题:

lib/module.js => require

Module.prototype.require = function(path) {
  assert(path, 'missing path');
  assert(typeof path === 'string', 'path must be a string');
  return Module._load(path, this, /* isMain */ false);
};
好像没什么卵用,对不对?她就调用了另外一个方法 _load,永不放弃,继续

lib/module.js => _load

Module._load = function(request, parent, isMain) {
  //debug代码,么卵用,跳过
  if (parent) {
    debug('Module._load REQUEST %s parent: %s', request, parent.id);
  }

  if (isMain && experimentalModules) {
    //...
    //...
    //这段是给ES module用的,不看了啊
  }

  //获取模块的完整路径
  var filename = Module._resolveFilename(request, parent, isMain);

  //缓存在这里啊?好激动有没有?!?终于见到她老人家了
  //原来这是这样的,简单的一批,毫无神秘感啊有木有
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    updateChildren(parent, cachedModule, true);
    return cachedModule.exports;
  }

  //加载native但非内部module的,不看
  if (NativeModule.nonInternalExists(filename)) {
    debug('load native module %s', request);
    return NativeModule.require(filename);
  }

  //构造全新Module实例了
  var module = new Module(filename, parent);

  if (isMain) {
    process.mainModule = module;
    module.id = '.';
  }

  //先把实例引用加缓存里
  Module._cache[filename] = module;

  //尝试加载模块了
  tryModuleLoad(module, filename);

  return module.exports;
};
彷佛到这里差很少了,不过咱们再深刻看看 tryModuleLoad

lib/module.js => tryModuleLoad

function tryModuleLoad(module, filename) {
  var threw = true;
  try {
    //加载模块
    module.load(filename);
    threw = false;
  } finally {
    //要是加载失败,从缓存里删除
    if (threw) {
      delete Module._cache[filename];
    }
  }
}
接下来就是真正的 load了,要不咱们先停一停?

好了,分析问题的关键在于不忘初心,虽然到目前为止咱们前进的比较顺利,也很爽对不对?。但咱们的此行的目的并非爽,好像是有个什么疑惑哦!因而,咱们再次梳理下问题:

  1. son.js里用首字母大写(不正确)的模块名引用了parent.js
  2. test.js里,引用了两次parent.js,一次用彻底一致的模块名;一次用首字母大写的模块名。结果发现son instanceof require('./parent') === false

既然没报错的问题前面已经解决了,那么,如今看起来就是加载模块这个部分可能出问题了,那么问题究竟是什么?咱们怎么验证呢?

这个时候我看到了这么一句话var cachedModule = Module._cache[filename];,文件名是做为缓存的key,来吧,是时候看看Module._cache里存的模块key都是什么牛鬼蛇神了。因而怎么可以查看到Module._cache就是咱们的下一个探索目标。那么咱们就得顺着刚才发现的,真正的load继续看下去了。

lib/module.js => load

Module.prototype.load = function(filename) {
  debug('load %j for module %j', filename, this.id);

  assert(!this.loaded);
  this.filename = filename;
  this.paths = Module._nodeModulePaths(path.dirname(filename));

  var extension = path.extname(filename) || '.js';
  if (!Module._extensions[extension]) extension = '.js';
  
  //这里就是关键,根据文件名,扩展名找到了该文件,加载的好戏上演了
  Module._extensions[extension](this, filename);
  this.loaded = true;

  //ES6 module相关,不看
  if (ESMLoader) {
    ...
    ...
    ...
  }
};
顺着这条路,咱们如今应该去找那个 Module._extensions['.js']的实现了

lib/module.js => Module._extensions

Module._extensions['.js'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  module._compile(internalModule.stripBOM(content), filename);
};
至此,咱们尚未发现如何从开发者角度访问到 _cache的踪影,因此继续向下走

lib/module.js => _compile

Module.prototype._compile = function(content, filename) {

  content = internalModule.stripShebang(content);

  // create wrapper function
  //为了保证每一个模块独立的做用域,这个有个wrapper的过程,
  //相信了解browserify、webpack工做原理的朋友懂得
  var wrapper = Module.wrap(content);

  var compiledWrapper = vm.runInThisContext(wrapper, {
    filename: filename,
    lineOffset: 0,
    displayErrors: true
  });

  ...
  ...
  var dirname = path.dirname(filename);
  //这个步骤是关键,看到了require,请容许我草率的决定进去看看这个makeRequireFunction
  var require = internalModule.makeRequireFunction(this);
  var depth = internalModule.requireDepth;
  if (depth === 0) stat.cache = new Map();
  var result;
  if (inspectorWrapper) {
    result = inspectorWrapper(compiledWrapper, this.exports, this.exports,
                              require, this, filename, dirname);
  } else {
    result = compiledWrapper.call(this.exports, this.exports, require, this,
                                  filename, dirname);
  }
  if (depth === 0) stat.cache = null;
  return result;
};

lib/internal/module.js => makeRequireFunction

function makeRequireFunction(mod) {
  const Module = mod.constructor;

  function require(path) {
    try {
      exports.requireDepth += 1;
      return mod.require(path);
    } finally {
      exports.requireDepth -= 1;
    }
  }

  function resolve(request, options) {
    return Module._resolveFilename(request, mod, false, options);
  }

  require.resolve = resolve;

  function paths(request) {
    return Module._resolveLookupPaths(request, mod, true);
  }

  resolve.paths = paths;

  require.main = process.mainModule;

  // Enable support to add extra extension types.
  require.extensions = Module._extensions;

  //开心,我看到Module._cache被赋值到require上了
  //接下来只要知道这个require是否是咱们在使用时的那个就行了
  require.cache = Module._cache;

  return require;
}
我在这里能够明确告诉你,是的,这里的 require,就是咱们代码里用到的 require。线索就在上面那步 Module.prototype._compile里,请仔细看 var wrapper = Module.wrap(content);result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname);两行内容

注:其实若是你熟读文档, 上述寻找_cache访问方法的过程是没必要要的,但为了保持叙事完整,我仍是装了个逼,请见谅

打完收工,如今咱们已经知道如何查看_cache里的内容了,因而我在test.js里最后面加了一句console.log(Object.keys(require.cache)),咱们看看打出了什么结果

false
true
[ '/Users/admin/codes/test/index.js',
  '/Users/admin/codes/test/Parent.js',
  '/Users/admin/codes/test/parent.js',
  '/Users/admin/codes/test/son.js' ]
真相已经呼之欲出了, Module._cache里真的出现了两个 [p|P]arentmacOS默认不区分大小写,因此她找到的实际上是同一个文件;但 node.js当真了,一看文件名不同,就当成不一样模块了),因此最后问题的关键就在于 son.js里到底引用时用了哪一个名字(上面咱们用了首字母大写的 require('./Parent.js')),这才致使了 test.js认贼做父的梗。

若是咱们改改son.js,把引用换成require('./parEND.js'),再次执行下test.js看看结果如何呢?

false
false
[ '/Users/haozuo/codes/test/index.js',
  '/Users/haozuo/codes/test/Parent.js',
  '/Users/haozuo/codes/test/parent.js',
  '/Users/haozuo/codes/test/son.js',
  '/Users/haozuo/codes/test/parENT.js' ]

没有认贼做父了对不对?再看Module._cache里,原来是parENT.js也被当成一个单独的模块了。

因此,假设你的模块文件名有n个字符,理论上,在macOS大小写不敏感的文件系统里,你能让node.js将其弄出最大2n次方个缓存来

是否是很惨!?还好macOS仍是能够改为大小写敏感的,格盘重装系统;新建分区都行。

问题虽然不难,但探究问题的决心和思路仍是重要的。

最后祝愿你们前程似锦!!

相关文章
相关标签/搜索