谨以此文档献给我的朋友GeorgeG!!!愿他战胜一切心魔和狗腿事情的起因是这样的,GeorgeG在他的电脑上做了一点测试,但是结果却和预想的大相径庭。那么我们来看看这个小测试都写了什么:一共三个文件,总共不超过15行代码parent.jsclassParent{}module.exports=Parentson.js//加载时模块文件名大写(Incorrect)constParent=require('./Parent')classSonextendsParent{}module.exports=Sontest.js//加载时模块名首字母大写(Incorrect)constParentIncorrect=require('./Parent')//加载正确的模块文件名(correct)constParent=require('./parent')constSon=require('./son')constss=newSon()//测试结果console.log(ssinstanceofParent)//falseconsole.log(ssinstanceofParentIncorrect)//trueGeorgeG有以下问题:son.js和test.js都有错误的文件名(大小写问题)引用,为什么不报错呢?测试结果,为什么ssinstanceofParentIncorrect===true?不报错我就忍了,你怎么还承认你是贼爹,说你是加载错名字的模块实例?如果你对以上问题有了清醒的认识,那么恭喜你,文能笔能护天,武能骑马定天下;上炕识女人,上炕识鞋!但如果你不知道为什么?好吧,我有话要说,你有话要读。其实看病的方法(假装是调试者)和中医看病的方法差不多。指出可根据需要选择看、听、问、切四种方法中的一种或两种来寻找答案。希望代码不要太多。看了一会,即使没有我的评论,相信细心的同学也会发现,真实的文件名和代码的介绍有出入,所以这里肯定有问题。记住问题,我们会继续听到算了,我闻不到代码,所以来问,软件工程很重要的一个环节就是沟通,不一定是和遇到bug的同事,可能是你自己,也可能是是QA,当然可以是PM或你的老板。你没有问你想知道的问题;他没有说清楚他想回答什么;一切都结束了。...那我想知道什么?下面两个作为调试入口比较合理:操作系统运行环境+版本你怎么测试,命令行还是其他方式回答:macOS;node.js>8.0;命令行nodetest.js令人兴奋和深刻开始吧,我要开始了。(为了完整描述调试过程,我会假装我事先不知道以下所有的事情)准备电脑,准备运行环境node.js>9.3.0,完成copy代码,完成运行,并且对狗进行处理,果然没有报错,运行结果也和乔治G说的一模一样。为了证明我不是瞎子,我尝试在test.js中要求一个不存在的文件require('./nidayede')并运行代码。还好这次报了Error:Cannotfindmodule'./nidayede',我才没有疯。这是非常令人愉快的。所以第一个问题出现了。为什么Gouri的模块名错了还能加载?会不会跟操作系统有关系?我们再找个ubuntu试试。果然在ubuntu上,大小写问题出问题了,Error:Cannotfindmodule'./Parent'。(有朋友提醒,windows默认也是不区分大小写的,所以比如之前windows会报错,应该是我之前修改了注册表)。那么macOS到底在做什么呢?你分不清大写字母和小写字母吗?所以赶紧google(别问我为什么不baidu)。事实证明,出色的OSX默认使用不区分大小写的文件系统(详细文档)。但为什么?如此反人类的设计到底有何用意?更多的解释,来来去去你所以,这就是你不报错的原因?(指责node.js),但这就是全部事实。可事情还没完,认贼当爹又算什么?隐约听说node.js有缓存,是不是那个东西造成的?于是抱着试一试的心情,我把constParentIncorrect=require('./Parent')和constParent=require('./parent')的位置换了个思路,让第一个加载根据正确的名字,会是正确的吗?果然还是错了。靠猜测和伪装并不能真正解决问题。BibiParentIncorrect和Parent呢?于是我写了console.log(ParentIncorrect===Parent),结果是false。所以他们真的不是一回事,所以这意味着问题可能在介绍部分?于是萌生了一个假装看node.js源码的想法(其实不看最后也能搞清楚问题所在)。和狗打交道后,怀着忐忑的心情,终于克隆了一份node.js源码(花了好长时间,真的很慢)。让我们一起进入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=>requireModule.prototype.require=function(path){assert(path,'缺少路径');assert(typeofpath==='string','pathmustbeastring');返回Module._load(path,this,/*isMain*/false);};似乎没用,对吧?她调用了另一个方法_load,永不放弃,继续lib/module.js=>_loadModule._load=function(request,parent,isMain){//调试代码,使用它,跳过if(parent){debug('Module._loadREQUEST%sparent:%s',request,parent.id);}if(isMain&&experimentalModules){//...//...//这一段是针对ES模块的,不要看}//获取模块的完整路径varfilename=Module._resolveFilename(请求,父母,isMain);//缓存在这里吗?你兴奋吗?!?终于看到她老人家了//原来是这样的,简单的批次,没有一点神秘感。varcachedModule=Module._cache[文件名];如果(cachedModule){updateChildren(parent,cachedModule,true);urncachedModule.exports;}//加载原生但非内部模块,不要看if(NativeModule.nonInternalExists(filename)){debug('loadnativemodule%s',request);返回NativeModule.require(文件名);}//构造一个新的Module实例varmodule=newModule(filename,parent);如果(isMain){process.mainModule=module;module.id='.';}//首先将实例引用添加到缓存中Module._cache[filename]=module;//尝试加载模块tryModuleLoad(module,filename);返回module.exports;};好像差不多了,但是让我们仔细看看tryModuleLoadlib/module.js=>tryModuleLoadfunctiontryModuleLoad(module,filename){varthrown=true;try{//加载模块module.load(filename);扔=假;}finally{//如果加载失败,从缓存中删除if(throw){deleteModule._cache[filename];}}}接下来是真正的负载,我们为什么不停一会儿呢?那么,分析问题的关键就是不忘初心。虽然目前我们进展的比较顺利,但是也很爽不是吗?但是我们此行的目的并不是为了玩,似乎有些疑惑!那么,我们再梳理一下问题:在son.js中,首字母大写的模块名(不正确)指的是parent.jstest.js,parent.js被引用了两次,一次是同一个模块名;oncewith第一个字母大写的模块名称。原来是soninstanceofrequire('./parent')===false既然前面已经解决了没有报错的问题,看来可能是加载模块有问题,那么到底是什么问题呢?我们如何验证它?这时候看到了这么一句varcachedModule=Module._cache[filename];,文件名作为缓存的key,来吧,是时候看看Module._cache中存放的模块key是什么了。那么如何查看Module._cache就是我们接下来的探索目标。然后我们必须按照我们刚刚发现的内容,真正的负载继续阅读。lib/module.js=>loadModule.prototype.load=function(filename){debug('load%jformodule%j',filename,this.id);断言(!this.loaded);this.filename=文件名;this.paths=Module._nodeModulePaths(path.dirname(filename));varextension=path.extname(文件名)||'.js';如果(!Module._extensions[extension])extension='.js';//这里是关键。根据文件名和扩展名,找到文件,开始加载节目。Module._extensions[extension](this,filename);this.loaded=true;//ES6模块相关,不用看if(ESMLoader){.........}};按照这条路径,我们现在应该找到Module._extensions['.js']lib/module.js=>Module._extensionsModule._extensions['.js']=function(module,filename){varcontent=fs.readFileSync(文件名,'utf8');module._compile(internalModule.stripBOM(content),文件名);};到此为止,我们还没有从开发者的角度去获取_cache的踪迹,所以继续往下走lib/module.js=>_compileModule.prototype._compile=function(content,filename){content=internalModule.stripShebang(content);//创建包装函数n//为了保证每个模块的独立作用域,有一个wrapper过程,//相信了解browserify和webpack工作原理的朋友都明白varwrapper=Module.wrap(content);varcompiledWrapper=vm.runInThisContext(wrapper,{filename:filename,lineOffset:0,displayErrors:true});......vardirname=path.dirname(filename);//这一步是关键,看到require,请允许我仓促决定进入看看这个makeRequireFunctionvarrequire=internalModule.makeRequireFunction(this);vardepth=internalModule.requireDepth;如果(深度===0)stat.cache=newMap();变量结果;如果(inspectorWrapper){结果=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;返回结果;};lib/internal/module.js=>makeRequireFunctionfunctionmakeRequireFunction(mod){constModule=mod.constr构造器;函数要求(路径){尝试{exports.requireDepth+=1;返回mod.require(path);}最后{exports.requireDepth-=1;}}functionresolve(request,options){returnModule._resolveFilename(request,mod,false,选项);}require.resolve=resolve;函数路径(请求){returnModule._resolveLookupPaths(请求,mod,true);}resolve.paths=路径;require.main=process.mainModule;//启用支持添加额外的扩展类型。require.extensions=Module._extensions;//开心,我看到Module._cache被赋值给了require//接下来,只要知道这个require是不是我们用的就好了require.cache=Module._cache;returnrequire;}这里我可以明确的告诉你,没错,这里的require就是我们代码中使用的require线索就在上面这一步Module.prototype._compile,请仔细看varwrapper=Module.wrap(content);和result=compiledWrapper.call(this.exports,this.exports,require,this,filename,dirname);两行内容说明:其实,如果你熟悉文档,上面寻找_cache访问方法的过程是没有必要的,但为了保持叙述的完整,我还是装逼。请原谅我,收工吧。现在我们知道怎么检查_cache里的内容没有了,所以我在test.js最后加了一个console.log(Object.keys(require.cache)),看看结果是什么falsetrue['/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]arent(macOS默认不区分大小写,所以她找到的其实是同一个文件;但是node.js很重视,看文件名不一样,会认为是不同的模块),所以最后一个问题的关键是在son中引用的时候用的是哪个名字,导致了test.js认贼为父的梗。如果我们改son.js,把引用换成require('./parEND.js'),再执行test.js看看结果?falsefalse['/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.!?好在macOS还是可以改成区分大小写的,可以在格盘上重装系统;新分区没问题。虽然问题不难,但是探索问题的决心和思路还是很重要的。最后祝大家前程似锦!!
