关于node.js和macOS之间的故事

小云云
Lepaskan: 2017-12-18 11:22:14
asal
1311 orang telah melayarinya

本文由一个小故事来和大家分享关于node.js和macOS之间的故事,希望能帮助到大家。

乔治G在他的电脑上做了一个小测试,但结果和预期的大不相同。

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

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

<span style="font-size: 14px;">parent.js</span>

<span style="font-size: 14px;">class Parent {}<br><br>module.exports = Parent<br></span>
Salin selepas log masuk

<span style="font-size: 14px;">son.js</span>

<span style="font-size: 14px;">//加载时把模块文件名首字母大写了(不正确的)<br/>const Parent = require(&#39;./Parent&#39;)<br/><br/>class Son extends Parent {}<br/><br/>module.exports = Son<br/></span>
Salin selepas log masuk

<span style="font-size: 14px;">test.js</span>

<span style="font-size: 14px;">//加载时把模块名首字母大写(不正确的)<br/>const ParentIncorrect = require(&#39;./Parent&#39;)<br/>//通过正确的模块文件名加载(正确)<br/>const Parent = require(&#39;./parent&#39;)<br/><br/>const Son = require(&#39;./son&#39;)<br/><br/>const ss = new Son()<br/><br/>//测试结果<br/>console.log(ss instanceof Parent) // false<br/>console.log(ss instanceof ParentIncorrect) // true<br/></span>
Salin selepas log masuk

乔治G同学有以下疑问:

  1. <span style="font-size: 14px;">son.js</span><span style="font-size: 14px;">test.js</span> 里都有错误的文件名(大小写问题)引用,为什么不报错?

  2. 测试结果,为什么 <span style="font-size: 14px;">ss instanceof ParentIncorrect === true</span> ?不报错我忍了,为什么还认贼作父,说自己是那个通过不正确名字加载出来的模块的instance?

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

但如果你也不是很清楚为什么?那么好了,我有的说,你有的看。

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

代码不多,看了一会,即便没有我的注释,相信仔细的同学也都发现真正的文件名和代码中引入时有出入的,那么这里肯定是有问题的,问题记住,我们继续

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

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

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

  1. 操作系统

  2. 运行环境 + 版本

  3. 你怎么测试的,命令行还是其他什么手段

答曰:macOS; <span style="font-size: 14px;">node.js > 8.0</span> ;命令行 <span style="font-size: 14px;">node test.js</span>

激动人心的深刻到来了,我要动手了。(为了完整的描述 <span style="font-size: 14px;">debug</span> 过程,我会假装这下面的所有事情我事先都是不知道的)

准备电脑,完毕

准备运行环境 <span style="font-size: 14px;">node.js > 9.3.0</span> , 完毕

复刻代码,完毕

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

为了证明我没瞎,我又尝试在 <span style="font-size: 14px;">test.js</span><span style="font-size: 14px;">require</span> 了一个压根不存在的文件 <span style="font-size: 14px;">require(&#39;./nidayede&#39;)</span> ,运行代码。

还好这次报错了 <span style="font-size: 14px;">Error: Cannot find module &#39;./nidayede&#39;</span> ,所以我没疯。这点真令人高兴。

于是有了第一个问题

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

会不会和操作系统有关系?来我们再找台 <span style="font-size: 14px;">windows</span> 试试,果然,到了 <span style="font-size: 14px;">windows</span> 上,大小写问题就是个问题了, <span style="font-size: 14px;">Error: Cannot find module &#39;./Parent&#39;</span>

那么 <span style="font-size: 14px;">macOS</span> 到底在干什么?连个大小写都分不出来么?于是赶紧 <span style="font-size: 14px;">google</span> (别问我为什么不baidu)

关于node.js和macOS之间的故事

原来人家牛逼的 <span style="font-size: 14px;">OS X</span> 默认用了 <span style="font-size: 14px;">case-insensitive</span> 的文件系统( 详细文档 )。

but why?这么反人类的设计到底是为了什么?

关于node.js和macOS之间的故事

更多解释, 来,走你

所以,这就是你不报错的理由?(对 <span style="font-size: 14px;">node.js</span> 指责道),但这就是全部真相了。

但事情没完

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

依稀有听过 <span style="font-size: 14px;">node.js</span> 里有什么缓存,是那个东西引起的么?于是抱着试试看的心情,我把 <span style="font-size: 14px;">const ParentIncorrect = require(&#39;./Parent&#39;)</span><span style="font-size: 14px;">const Parent = require(&#39;./parent&#39;)</span> 换了下位置,心想,这样最先按照正确的名字加载,会不会就对了呢?

果然, 还是不对 。靠猜和装逼是不能够真正解决问题的

那比比 <span style="font-size: 14px;">ParentIncorrect</span><span style="font-size: 14px;">Parent</span> 呢?于是我写了 <span style="font-size: 14px;">console.log(ParentIncorrect === Parent)</span> ,结果为 <span style="font-size: 14px;">false</span> 。所以他俩还真的不是同一个东西,那么说明问题可能在引入的部分喽?

于是一个装逼看 <span style="font-size: 14px;">node.js</span> 源码的想法诞生了(其实不看,问题最终也能想明白)。 日了狗,怀着忐忑的心情,终于 <span style="font-size: 14px;">clone</span> 了一把 <span style="font-size: 14px;">node.js</span> 源码(花了好久,真tm慢)

来,我们一起进入神秘的 <span style="font-size: 14px;">node.js</span> 源码世界。既然我们的问题是有关 <span style="font-size: 14px;">require</span> 的,那就从她开始吧,不过找到 <span style="font-size: 14px;">require</span> 定义的过程需要点耐心,这里不详述,只说查找的顺序吧

<span style="font-size: 14px;">src/node_main.cc => src/node.cc => lib/internal/bootstrap_node.js => lib/module.js</span>

找到咯,就是这个 <span style="font-size: 14px;">lib/module.js</span> ,进入正题:

lib/module.js => require

<span style="font-size: 14px;">Module.prototype.require = function(path) {<br/>  assert(path, &#39;missing path&#39;);<br/>  assert(typeof path === &#39;string&#39;, &#39;path must be a string&#39;);<br/>  return Module._load(path, this, /* isMain */ false);<br/>};<br/></span>
Salin selepas log masuk

好像没什么卵用,对不对?她就调用了另一个方法 <span style="font-size: 14px;">_load</span> ,永不放弃,继续

lib/module.js => _load

<span style="font-size: 14px;">Module._load = function(request, parent, isMain) {<br/>  //debug代码,么卵用,跳过<br/>  if (parent) {<br/>    debug(&#39;Module._load REQUEST %s parent: %s&#39;, request, parent.id);<br/>  }<br/><br/>  if (isMain && experimentalModules) {<br/>    //...<br/>    //...<br/>    //这段是给ES module用的,不看了啊<br/>  }<br/><br/>  //获取模块的完整路径<br/>  var filename = Module._resolveFilename(request, parent, isMain);<br/><br/>  //缓存在这里啊?好激动有没有?!?终于见到她老人家了<br/>  //原来这是这样的,简单的一批,毫无神秘感啊有木有<br/>  var cachedModule = Module._cache[filename];<br/>  if (cachedModule) {<br/>    updateChildren(parent, cachedModule, true);<br/>    return cachedModule.exports;<br/>  }<br/><br/>  //加载native但非内部module的,不看<br/>  if (NativeModule.nonInternalExists(filename)) {<br/>    debug(&#39;load native module %s&#39;, request);<br/>    return NativeModule.require(filename);<br/>  }<br/><br/>  //构造全新Module实例了<br/>  var module = new Module(filename, parent);<br/><br/>  if (isMain) {<br/>    process.mainModule = module;<br/>    module.id = &#39;.&#39;;<br/>  }<br/><br/>  //先把实例引用加缓存里<br/>  Module._cache[filename] = module;<br/><br/>  //尝试加载模块了<br/>  tryModuleLoad(module, filename);<br/><br/>  return module.exports;<br/>};<br/></span>
Salin selepas log masuk

似乎到这里差不多了,不过我们再深入看看 <span style="font-size: 14px;">tryModuleLoad</span>

lib/module.js => tryModuleLoad

<span style="font-size: 14px;">function tryModuleLoad(module, filename) {<br/>  var threw = true;<br/>  try {<br/>    //加载模块<br/>    module.load(filename);<br/>    threw = false;<br/>  } finally {<br/>    //要是加载失败,从缓存里删除<br/>    if (threw) {<br/>      delete Module._cache[filename];<br/>    }<br/>  }<br/>}<br/></span>
Salin selepas log masuk

接下来就是真正的 <span style="font-size: 14px;">load</span> 了,要不我们先停一停?

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

  1. <span style="font-size: 14px;">son.js</span> 里用首字母大写(不正确)的模块名引用了 <span style="font-size: 14px;">parent.js</span>

  2. <span style="font-size: 14px;">test.js</span> 里,引用了两次 <span style="font-size: 14px;">parent.js</span> ,一次用完全一致的模块名;一次用首字母大写的模块名。结果发现 <span style="font-size: 14px;">son instanceof require(&#39;./parent&#39;) === false</span>

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

这个时候我看到了这么一句话 <span style="font-size: 14px;">var cachedModule = Module._cache[filename];</span> ,文件名是作为缓存的 <span style="font-size: 14px;">key</span> ,来吧,是时候看看 <span style="font-size: 14px;">Module._cache</span> 里存的模块 <span style="font-size: 14px;">key</span> 都是什么牛鬼蛇神了,打出来看看吧,于是我在 <span style="font-size: 14px;">test.js</span> 里最后面加了一句 <span style="font-size: 14px;">console.log(Object.keys(require.cache))</span> ,我们看看打出了什么结果

<span style="font-size: 14px;">false<br/>true<br/>[ &#39;/Users/admin/codes/test/index.js&#39;,<br/>  &#39;/Users/admin/codes/test/Parent.js&#39;,<br/>  &#39;/Users/admin/codes/test/parent.js&#39;,<br/>  &#39;/Users/admin/codes/test/son.js&#39; ]<br/></span>
Salin selepas log masuk

真相已经呼之欲出了, <span style="font-size: 14px;">Module._cache</span> 里真的出现了两个 <span style="font-size: 14px;">[p|P]arent</span><span style="font-size: 14px;">macOS</span> 默认不区分大小写,所以她找到的其实是同一个文件;但 <span style="font-size: 14px;">node.js</span> 当真了,一看文件名不一样,就当成不同模块了),所以最后问题的关键就在于 <span style="font-size: 14px;">son.js</span> 里到底引用时用了哪个名字(上面我们用了首字母大写的 <span style="font-size: 14px;">require(&#39;./Parent.js&#39;)</span> ),这才导致了 <span style="font-size: 14px;">test.js</span> 认贼作父的梗。

如果我们改改 <span style="font-size: 14px;">son.js</span> ,把引用换成 <span style="font-size: 14px;">require(&#39;./parEND.js&#39;)</span> ,再次执行下 <span style="font-size: 14px;">test.js</span> 看看结果如何呢?

<span style="font-size: 14px;">false<br/>false<br/>[ &#39;/Users/haozuo/codes/test/index.js&#39;,<br/>  &#39;/Users/haozuo/codes/test/Parent.js&#39;,<br/>  &#39;/Users/haozuo/codes/test/parent.js&#39;,<br/>  &#39;/Users/haozuo/codes/test/son.js&#39;,<br/>  &#39;/Users/haozuo/codes/test/parENT.js&#39; ]<br/></span>
Salin selepas log masuk

没有认贼作父了对不对?再看 <span style="font-size: 14px;">Module._cache</span> 里,原来是 <span style="font-size: 14px;">parENT.js</span> 也被当成一个单独的模块了。

所以,假设你的模块文件名有 <span style="font-size: 14px;">n</span> 个字符,理论上,在 <span style="font-size: 14px;">macOS</span> 大小写不敏感的文件系统里,你能让 <span style="font-size: 14px;">node.js</span> 将其弄出最大 <span style="font-size: 14px;">2</span><span style="font-size: 14px;">n</span> 次方个缓存来

是不是很惨!?还好 <span style="font-size: 14px;">macOS</span> 还是可以改成大小写敏感的,格盘重装系统;新建分区都行。

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

相关推荐:

教大家如何利用node.js创建子进程

PHP与Node.js

node.js 发布订阅模式的方法

Atas ialah kandungan terperinci 关于node.js和macOS之间的故事. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Label berkaitan:
sumber:php.cn
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan
Tentang kita Penafian Sitemap
Laman web PHP Cina:Latihan PHP dalam talian kebajikan awam,Bantu pelajar PHP berkembang dengan cepat!