我们常说node并不是一门新的编程语言,它只是javascript的runtime,runtime可以简单理解为运行javascript的环境。在大多数情况下,我们会在浏览器中运行javascript。随着node的出现,我们可以在node中运行javascript,也就是说只要安装了node或者浏览器,我们就可以运行javascript。1.节点模块化的实现Node有自己的模块化机制,每个文件都是一个单独的模块,遵循CommonJS规范,即使用require导入模块,通过module.export模块导出。node模块的运行机制也很简单。实际上,每个模块都包裹着一层功能。通过函数包装,可以实现代码之间的作用域隔离。你可能会说我写代码的时候没有把函数包装起来。是的,它是真实的。这层功能是node自动帮我们实现的。我们可以测试一下。我们新建一个js文件,第一行打印一个不存在的变量。比如我们这里打印window,但是node里面没有window。控制台日志(窗口);复制代码,通过node执行文件,会发现报错信息如下。(请使用系统默认的cmd来执行命令)。(函数(导出,要求,模块,__filename,__dirname){console.log(窗口);ReferenceError:windowisnotdefinedatObject.(/Users/choice/Desktop/node/main.js:1:75)atModule._compile(内部/模块/cjs/loader.js:689:30)atObject.Module._extensions..js(internal/modules/cjs/loader.js:700:10)atModule.load(internal/modules/cjs/loader.js:599:32)attryModuleLoad(internal/modules/cjs/loader.js:538:12)atFunction.Module._load(internal/modules/cjs/loader.js:530:3)atFunction.Module.runMain(internal/modules/cjs/loader.js:742:12)atstartup(internal/bootstrap/node.js:279:19)atbootstrapNodeJSCore(internal/bootstrap/node.js:752:3)复制代码可以看到有一个self-执行的函数,函数中包含exports,require,module,__filename,__dirname,这些我们常用的全局变量,我在上篇文章《前端模块化发展历程》介绍过,自执行函数也是实现方案之一对于前端模块化,在没有modu的时代早期前端的lar系统,自执行函数可以很好的解决命名空间问题,模块依赖的其他模块可以通过参数传入。cmd和amd规范也依赖于自执行函数。在模块系统中,每个文件都是一个模块,每个模块都会自动在外面包裹一个函数,并定义导出方法module.exports或exports,同时定义导入方法require。letmoduleA=(function(){module.exports=Promise;returnmodule.exports;})();复制代码2.require加载模块require依赖node中的fs模块加载模块文件,fs.readFile读取一个字符串。在javascrpt中我们可以使用eval或者newFunction将一个字符串转换成js代码运行。evalconstname='yd';conststr='consta=123;console.log(name)';eval(str);//yd;copycodenewFunctionnewFunction接收一个待执行的字符串并返回一个新的函数,调用这个新的函数字符串,它就会被执行。如果这个函数需要传参,可以在newFunction的时候依次传入参数,最后传入要执行的字符串。比如这里传递了参数b,要执行的字符串str。constb=3;conststr='leta=1;returna+b';constfun=newFunction('b',str);console.log(fun(b,str));//4复制代码看eval和FunctionInstantiation可以用来执行javascript字符串,貌似都可以用来实现require模块加载。但是,它们并没有被选中来实现节点中的模块化。原因很简单,因为它们都有一个致命的问题,就是容易受到不属于自己的变量的影响。下面的str字符串没有定义a,但是上面定义的a变量确实可以用,显然是错误的。在模块化机制中,str字符串应该有自己独立的运行空间,不允许有自己不存在的变量。可以直接使用。conststr='console.log(a)';eval(str);constfunc=newFunction(str);func();copycode节点有一个vm虚拟环境的概念,用来运行额外的js文件,他可以保证javascript执行的独立性,不会受到外界的影响。vm内置模块虽然我们在外部定义了hello,但是str是一个独立的模块,不在villagehello变量中,所以会直接报错。//引入vm模块,无需安装,node自建模块constvm=require('vm');consthello='yd';conststr='console.log(hello)';wm.runInThisContext(str);//报错复制代码,让node可以使用vm实现javascript模块。可以保证模块的独立性。3.require代码实现介绍在require代码实现之前,先回顾一下这两个node模块的用法,因为后面会用到。路径模块用于处理文件路径。basename:基本路径,如果有文件路径,则不是基本路径,基本路径为1.jsextname:获取扩展名dirname:父路径join:加入路径resolve:当前文件夹的绝对路径,注意使用时不要结束添加/__dirname:当前文件所在文件夹的路径__filename:当前文件的绝对路径constpath=require('path','s');console.log(path.basename('1.js'));控制台。日志(path.extname('2.txt'));console.log(path.dirname('2.txt'));console.log(path.join('a/b/c','d/e/f'));//a/b/c/d/e/console.log(path.resolve('2.txt'));复制代码fs模块用于对文件或文件夹进行操作,如读取文件写入、添加、删除等,常用方法有readFile和readFileSync,分别为异步和同步读取文件。constfs=require('fs');constbuffer=fs.readFileSync('./name.txt','utf8');//如果没有传入编码,则输出为二进制console.log(buffer);复制代码fs.access:判断是否存在,node10提供,exists方法已经废弃,因为不符合node规范,所以我们使用access判断文件是否存在。try{fs.accessSync('./name.txt');}catch(e){//文件不存在}复制代码4.手动实现require模块加载器首先导入依赖模块路径,fs,vm,并创建一个Require函数,这个函数接收一个modulePath参数,表示要导入的文件的路径。//导入依赖constpath=require('path');//路径操作constfs=require('fs');//文件读取constvm=require('vm');//文件执行//定义导入类,参数为模块路径functionRequire(modulePath){...}复制代码获取Require中模块的绝对路径,方便使用fs加载模块。这里我们使用newModule对模块内容进行抽象,使用tryModuleLoad加载模块内容。后面我们会实现Module和tryModuleLoad,Require的返回值应该是模块的内容,也就是module.exports。//定义导入类,参数为模块路径=newModule(absPathname);//加载当前模块tryModuleLoad(module);//返回exports对象returnmodule.exports;}复制代码Module的实现很简单,只需要为模块创建一个exports对象,在tryModuleLoad时将内容添加到exports中被执行,id是模块的绝对路径。//定义模块,添加文件id标识和exports属性functionModule(id){this.id=id;//读取的文件内容会放在exports中this.exports={};}我们在复制之前说过代码节点模块运行在一个函数中。这里我们将静态属性包装器挂载到模块中,并在其中定义了这个函数的字符串。wrapper是一个数组,数组的第一个元素是函数的参数部分,包括exports,module。require、__dirname、__filename,都是我们模块中常用的全局变量。注意这里传入的Require参数是我们自己定义的Require。第二个参数是函数的结尾。这两部分都是字符串,我们在使用时只需要将它们包裹在模块的字符串之外即可。module.wrapper=["(function(exports,module,Require,__dirname,__filename){","})"]复制代码_extensions用于对不同的模块扩展使用不同的加载方式,比如JSON和javascript加载方式绝对不同。JSON使用JSON.parse来工作。JavaScript使用vm.runInThisContext来运行。可以看到fs.readFileSync传入了module.id,也就是我们在定义Module的时候,id里面存放的是模块的绝对路径,读取的内容是一个字符串。我们使用模块。用wrapper将其包裹起来,相当于在模块外包裹了另外一个函数,同样实现了privatescope。使用call执行fn函数。第一个参数改变runningthis我们传入module.exports,后一个参数是包装参数exports,module,Require,__dirname,__filenameModule._extensions={'.js'(module){constcontent=fs.readFileSync(module.id,'utf8');constfnStr=Module.wrapper[0]+content+Module.wrapper[1];constfn=vm.runInThisContext(fnStr);fn.call(module.exports,module.exports,module,Require,_filename,_dirname);},'.json'(module){constjson=fs.readFileSync(module.id,'utf8');module.exports=JSON.parse(json);//将文件的结果放在exports属性上}}复制代码tryModuleLoad函数接收模块对象,通过path.extname获取模块的后缀名,然后使用Module._extensions加载模块。//定义模块加载方法functiontryModuleLoad(module){//获取扩展constexttension=path.extname(module.id);//通过后缀加载当前模块Module._extensions[extension](module);}复制codetothisRequireload机制我们基本写完了,我们再来看一下。Require加载模块时传入模块名,在Require方法中使用path.resolve(__dirname,modulePath)获取文件的绝对路径。然后通过实例化新的Module创建一个模块对象,将模块的绝对路径存储在模块的id属性中,并在模块中创建一个exports属性作为json对象。使用tryModuleLoad方法加载模块,在tryModuleLoad中使用path.extname获取文件的扩展名,然后根据扩展名执行相应的模块加载机制。最终将加载到mountsmodule.exports中的模块。tryModuleLoad执行后,module.exports已经存在,直接return即可。//导入依赖constpath=require('path');//路径操作constfs=require('fs');//文件读取constvm=require('vm');//文件执行//定义导入类,参数为模块路径functionRequire(modulePath){//获取要加载的绝对路径letabsPathname=path.resolve(__dirname,modulePath);//创建模块并新建Module实例constmodule=newModule(absPathname);//加载当前模块tryModuleLoad(module);//返回exports对象returnmodule.exports;}//定义模块,添加文件id标识和exports属性functionModule(id){this.id=id;//读取文件内容会放在exports中this.exports={};}//定义包装模块内容的函数Module.wrapper=["(function(exports,module,Require,__dirname,__filename){","})"]//定义扩展,不同的扩展,加载方式不同,实现js和jsonModule._extensions={'.js'(module){constcontent=fs.readFileSync(module.id,'utf8');constfnStr=模块。wrapper[0]+content+Module.wrapper[1];constfn=vm.runInThisContext(fnStr);fn.call(module.exports,module.exports,module,Require,_filename,_dirname);},'.json'(module){constjson=fs.readFileSync(module.id,'utf8');module.exports=JSON.parse(json);//将文件的结果放在exports属性上}}//定义模块加载方法functiontryModuleLoad(module){//获取展开namecontexttension=path.extname(module.id);//通过后缀加载当前模块Module._extensions[extension](module);}复制代码5.给模块添加缓存也比较简单,就是当文件加载完成后,它会将文件放入缓存中,然后在加载模块时检查缓存中是否存在。如果存在,直接使用。如果不存在,再去加爱,加载完再放到缓存中//定义导入类,参数为模块路径functionRequire(modulePath){//获取要加载的绝对路径letabsPathname=path.resolve(__dirname,modulePath);//从缓存中读取,如果存在则直接返回结果if(Module._cache[absPathname]){returnModule._cache[absPathname].exports;}//尝试加载currentmoduletryModuleLoad(module);//创建模块并新建Module实例constmodule=newModule(absPathname);//添加缓存Module._cache[absPathname]=module;//加载当前模块ModuletryModuleLoad(module);//返回exports对象returnmodule.exports;}复制代码6.自动补全路径,自动给模块添加后缀,实现加载没有后缀的模块。其实如果文件没有后缀,遍历所有扩展名,看文件是否存在。//定义导入类,参数为模块路径(Module._extensions);letindex=0;//存储原始文件路径constoldPath=absPathname;functionfindExt(absPathname){if(index===extNames.length){returnthrownewError('文件不存在');}try{fs.accessSync(absPathname);returnabsPathname;}catch(e){constext=extNames[index++];findExt(oldPath+ext);}}//递归追加后缀判断文件是否存在absPathname=findExt(absPathname);//从缓存中读取,如果存在则直接返回结果if(Module._cache[absPathname]){returnModule._cache[absPathname].exports;}//尝试加载当前模块//创建一个模块并新建一个Module实例constmodule=newModule(absPathname);//添加缓存Module._cache[absPathname]=module;//加载当前模块tryModuleLoad(module);//返回exports对象returnmodule.exports;}复制代码7.分析及实现步骤导入相关模块,创建Require方法。提取用于通过Module._load方法加载模块。Module.resolveFilename根据相对路径转换为绝对路径。缓存模块Module._cache,不要重复加载同一个模块,提高性能。createmoduleid:保存的内容是exports={}相当于这个。使用tryModuleLoad(module,filename)尝试加载模块。Module._extensions使用读取的文件。module.wrap:将读取的js包装成一个函数。使用runInThisContext运行获取的字符串。让字符串执行并使其适应导出。