注:1、本文涉及的nodejs源码如无特殊说明,均基于v10.14.1。对NodeJs系列感兴趣的朋友请关注微信公众号:前端神盾局。或githubNodeJs系列文章Nodejs中模块的实现本节主要以NodeJs的源码为基础,简单介绍一下其模块的实现。如有错误或遗漏,欢迎指正。当我们使用require引入一个模块时,一般会经过两个步骤:路径分析和模块加载路径分析。路径解析其实就是模块查找的过程,由_resolveFilename函数实现。让我们通过一个例子来扩展描述:consthttp=require('http');constmoduleA=requie('./parent/moduleA');在这个例子中,我们引入了两种不同类型的模块:核心模块-http和自定义模块moduleA对于核心模块,_resolveFilename会跳过搜索这一步,直接返回,交给下一步处理if(NativeModule.nonInternalExists(request)){//这里的请求是模块名'http'returnrequest;}对于自定义模块,有以下几种情况(_findPath)filemoduledirectorymoduleloadsglobaldirectoryfromthenode_modulesdirectoryloads这些已经说清楚了官方文档中有说明,这里不再赘述。如果模块存在,_resolveFilename会返回模块的绝对路径,如/Users/xxx/Desktop/practice/node/module/parent/moduleA.js。加载模块获取到模块地址后,Node开始加载模块。首先,Node会检查模块是否存在于缓存中://filename是模块的绝对路径varcachedModule=Module._cache[filename];如果(cachedModule){updateChildren(parent,cachedModule,true);returncachedModule.exports;}exists返回对应的缓存内容,如果不存在,进一步判断模块是否为核心模块:if(NativeModule.nonInternalExists(filename)){returnNativeModule.require(filename);}如果是模块既不存在于缓存中,也不是核心模块,那么Node会实例化一个新的模块对象functionModule(id,parent){//通常是模块的绝对路径this.id=id;//要导出的内容this.exports={};//父模块this.父母=父母;this.filename=null;//是否加载成功this.loaded=false;//子模块this.children=[];}varmodule=newModule(filename,parent);然后Node会按照路径尝试加载。functiontryModuleLoad(module,filename){varthrown=true;尝试{module.load(文件名);抛出=假;}finally{if(throw){deleteModule._cache[文件名];}}}对于不同的文件扩展名,加载方式也不同。.js文件(_compile)通过fs同步读取文件内容并包装在指定函数中:Module.wrapper=['(function(exports,require,module,__filename,__dirname){','\n});'];执行此函数的调用:compiledWrapper.call(this.exports,this.exports,require,this,filename,dirname);.json文件通过fs同步读取文件内容后,用JSON.parse解析并返回内容varcontent=fs.readFileSync(filename,'utf8');try{module.exports=JSON.parse(stripBOM(content));}catch(err){err.message=文件名+':'+err.message;throwerr;}.node这是一个用C/C++写的扩展文件,最终编译后的文件是通过dlopen()方法加载的。returnprocess.dlopen(module,path.toNamespacedPath(filename));.mjs这是处理ES6模块的扩展文件,是NodeJsv8.5.0之后的新特性。对于此类扩展名的文件,只能使用ES6模块语法import导入,否则会报错(启用--experimental-modules时)thrownewERR_REQUIRE_ESM(filename);如果一切顺利,它将返回附加到exports的对象以上内容返回module.exports;模块循环依赖下面我们来探讨一下模块循环依赖的问题:模块1依赖模块2,模块2依赖模块1,会发生什么?这里我们只探讨commonjs的情况。为此,我们创建了module-a.js和module-b.js两个文件,并让它们相互引用:module-a.jsconsole.log('开始加载模块A');exports.a=2;require('./module-b.js');exports.b=3;console.log('A模块加载完毕');module-b.jsconsole.log('开始加载B模块');letmoduleA=require('./module-a.js');console.log(moduleA.a,moduleA.b)console.log('B模块已加载');运行module-a.js,可以看到控制台输出:startloadingmoduleAstartloadingmoduleB2undefinedBmoduleloadingcompletemoduleAloadingcomplete此时,因为每个require都是同步执行的,所以需要先加载module-a已完全加载。/module-b。此时对于module-a,其exports对象只附加了属性a,属性b是在./module-b加载后赋值的。QA如何删除模块缓存?可以通过deleterequire.cache(moduleId)删除对应模块的缓存,其中moduleId表示模块的绝对路径。一般如果我们需要热更新一些模块,可以使用这个特性,例如://hot-reload.jsconsole.log('thisishotreloadmodule');//index.jsconstpath=require('小路');constfs=require('fs');consthotReloadId=path.join(__dirname,'./hot-reload.js');constwatcher=fs.watch(hotReloadId);watcher.on('change',(eventType,filename)=>{if(eventType==='更改'){删除require.cache[hotReloadId];require(hotReloadId);}});ES6模块可以在Node中使用吗?从8.5.0版本开始,NodeJs开始支持原生ES6模块。要启用此功能,需要满足两个条件:所有使用ES6模块的文件扩展名必须是.mjs命令行选项--experimental-modulesnode--experimental-modulesindex。mjsnode--experimental-modulesindex.mjs但从NodeJsv10.15.0开始,ES6模块支持还在实验阶段,不建议在公司项目中使用nodejs-loader.js。
