我们常说node并不是一门新的编程语言,它只是javascript的runtime,runtime可以简单理解为运行javascript的环境。在大多数情况下,我们会在浏览器中运行javascript。随着node的出现,我们可以在node中运行javascript,也就是说只要安装了node或者浏览器,我们就可以运行javascript。前端实训1.node模块化的实现Node有自己的模块化机制,每个文件都是一个独立的模块,遵循CommonJS规范,即使用require导入模块,通过module.export方式导出模块。node模块的运行机制也很简单。实际上,每个模块都包裹着一层功能。通过函数包装,可以实现代码之间的作用域隔离。你可能会说我写代码的时候没有把函数包装起来。是的,它是真实的。这层功能是node自动帮我们实现的。我们可以测试一下。我们新建一个js文件,第一行打印一个不存在的变量。比如我们这里打印window,但是node里面没有window。控制台日志(窗口);复制代码,通过node执行文件,会发现报错信息如下。(请使用系统默认的cmd来执行命令)。(function(exports,require,module,__filename,__dirname){console.log(window);ReferenceError:windowisnotdefinedatObject.(/Users/choice/Desktop/node/main.js:1:75)在Module._compile(internal/modules/cjs/loader.js:689:30)在Object.Module._extensions..js(internal/modules/cjs/loader.js:700:10)在Module.load(internal/modules/cjs/loader.js:599:32)在tryModuleLoad(internal/modules/cjs/loader.js:538:12)在Function.Module._load(internal/modules/cjs/loader.js:530:3)在Function.Module.runMain(internal/modules/cjs/loader.js:742:12)在启动时(internal/bootstrap/node.js:279:19)在bootstrapNodeJSCore(internal/bootstrap/node.js:752:3)复制代码可以看到在报错的最顶层有一个自执行函数,该函数包含exports、require、module、__filename、__dirname这些常用的全局变量,我在前一篇文章《前端模块化发展历程》.自执行函数也是前端模块化的实现方案之一。在早期前端没有模块化体系的时代,自执行函数可以很好的解决命名空间问题,模块依赖的其他模块可以通过参数传入。cmd和amd规范也是依靠自执行函数来实现的。在模块系统中,每个文件都是一个模块,每个模块外自动放置一个函数,定义导出方法module.exports或exports,导入方法也定义require。让moduleA=(function(){module.exports=Promise;returnmodule.exports;})();复制代码2.require加载模块require依赖node中的fs模块加载模块文件,fs.readFile读取的是一个字符串。在javascrpt中我们可以使用eval或者newFunction将一个字符串转换成js代码运行。evalconstname='yd';conststr='consta=123;控制台日志(名称)';评估(海峡);//码;copycodenewFunctionnewFunction接收一个待执行的字符串,返回的是一个新函数,调用这个新函数字符串就会执行。如果这个函数需要传参,可以在newFunction的时候依次传入参数,最后传入要执行的字符串。比如这里传递了参数b,要执行的字符串str。常量b=3;conststr='让a=1;返回a+b';constfun=newFunction('b',str);console.log(fun(b,str));//4复制代码可以看出eval和Function实例化都可以用来执行javascript字符串,貌似都可以实现require模块加载。但是,它们并没有被选中来实现节点中的模块化。原因很简单,因为它们都有一个致命的问题,就是容易受到不属于自己的变量的影响。下面的str字符串没有定义a,但是上面定义的a变量确实可以用,显然是错误的。在模块化机制中,str字符串应该有自己独立的运行空间,不允许有自己不存在的变量。可以直接使用。常量=1;conststr='console.log(a)';评估(海峡);constfunc=newFunction(str);功能();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:当前文件夹的绝对路径,注意使用Add时不要结束/__dirname:当前文件所在文件夹的路径__filename:当前文件的绝对路径constpath=require('path','s');console.log(path.basename('1.js'));console.log(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.手动实现requiremoduleloader先import依赖的模块路径,fs,vm,创建Arequire函数,该函数接收一个modulePath参数,表示要导入的文件路径。//导入依赖constpath=require('path');//路径操作constfs=require('fs');//读取文件constvm=require('vm');//文件执行//定义导入类,参数为模块路径functionRequire(modulePath){...}复制代码获取Require中模块的绝对路径,方便使用fs加载模块。这里我们使用newModule来抽象模块的内容,使用tryModuleLoad加载模块内容,Module和tryModuleLoad我们后面会实现,Require的返回值应该是模块的内容,也就是module.exports。//定义导入类,参数为模块路径functionRequire(modulePath){//获取当前要加载的绝对路径letabsPathname=path.resolve(__dirname,modulePath);//创建一个模块,新建一个模块Moduleinstanceconstmodule=newModule(absPathname);//加载当前模块tryModuleLoad(module);//返回exports对象returnmodule.exports;}复制代码Module的实现很简单,为创建一个exports对象即可模块,并在tryModuleLoad执行时将内容添加到exports中,id为模块的绝对路径。//定义模块,添加文件id标识和exports属性functionModule(id){this.id=id;//读取的文件内容会放在exports中this.exports={};}在复制之前代码,我们说node模块运行在一个函数中。这里我们将静态属性包装器挂载到模块中,并在其中定义了这个函数的字符串。wrapper是一个数组,数组的第一个元素是函数的参数部分,包括exports。模块。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){//获取扩展constextension=path.extname(module.id);//通过后缀加载当前模块Module._extensions[extension](module);}把代码复制到这里Require加载机制我们基本写完了,我们再来看看。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);//创建模块并创建一个新的模块实例constmodule=newModule(absPathname);//加载当前模块tryModuleLoad(module);//返回导出对象returnmodule.exports;}//定义模块,添加文件id标识符和导出attributefunctionModule(id){this.id=id;//读取接收到的文件的内容将放在exportsthis.exports={};}//定义包装模块内容的函数Module.wrapper=["(function(exports,module,Require,__dirname,__filename){","})"]//定义扩展,不同的扩展,不同的加载方式,实现js和jsonModule._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属性上}}//定义模块加载方法functiontryModuleLoad(module){//获取扩展constextension=path.extname(module.id);//加载当前模块通过后缀Module._extensions[extension](module);}复制代码5.给模块添加缓存添加缓存也比较简单,就是在加载文件的时候把文件放到缓存中,然后看加载模块时在缓存中,如果存在,直接使用,如果不存在,再去加爱,加载后放入缓存中//定义导入类,参数为模块路径函数Require(modulePath){//获取当前要加载的绝对路径letabsPathname=path.resolve(__dirname,modulePath);//从缓存中读取,如果存在则直接返回结果if(Module._cache[absPathname]){returnModule._cache[absPathname].exports;}//尝试加载当前模块tryModuleLoad(module);//创建模块并创建新的模块实例constmodule=newModule(absPathname);//添加缓存模块。_cache[absPathname]=module;//加载当前模块tryModuleLoad(module);//返回exports对象returnmodule.exports;}复制代码6.自动补全路径,自动给模块添加后缀,实现加载模块没有后缀。其实如果文件没有后缀,遍历所有的后缀,看文件是否存在。//定义导入类,参数为模块路径functionRequire(modulePath){//获取要加载的绝对路径letabsPathname=path.resolve(__dirname,modulePath);//获取所有后缀名constextNames=Object.keys(Module._extensions);letindex=0;//存储原始文件路径constoldPath=absPathname;functionfindExt(absPathname){if(index===extNames.length){returnthrownewError('The文件不存在');}尝试{fs.accessSync(absPathname);返回绝对路径名;}catch(e){constext=extNames[index++];findExt(旧路径+分机);}}//递归追加后缀判断文件是否存在absPathname=findExt(absPathname);//从缓存中读取,如果存在则直接返回结果if(Module._cache[absPathname]){returnModule._cache[absPathname].exports;}//尝试加载当前模块tryModuleLoad(module);//创建一个模块,创建一个新的Module实例constmodule=newModule(absPathname);//添加缓存Module._cache[absPathname]=module;//加载当前模块tryModuleLoad(module);//返回导出对象返回module.exports;}复制代码七、分析及实现步骤1、导入相关模块,创建Require方法。2.提取用于通过Module._load方法加载模块。3.Module.resolveFilename根据相对路径转换为绝对路径。4、缓存模块Module._cache,不要重复加载同一个模块,提高性能。5.创建模块id:保存的内容是exports={}相当于这个。6.使用tryModuleLoad(module,filename)尝试加载模块。7.Module._extensions使用读取文件。8.Module.wrap:将读取的js用函数包装起来。9.使用runInThisContext运行获取的字符串。10.让字符串执行并使其适应导出。作者:殷东