阅读原著CommonJS概述CommonJS是一个模块化的标准,而NodeJS就是这个标准的实现。每个文件都是一个具有自己范围的模块。文件中定义的变量、函数和类都是私有的,对其他文件不可见。NodeJS模块化的简单实现在实现模块加载之前,我们需要明确模块加载过程:假设A文件夹下有一个.js,我们需要解析出一个绝对路径;我们写的路径可以没有后缀.js,.json;获取真正的加载路径(模块会被缓存),首先去缓存中查看文件是否存在,如果没有返回缓存,则创建模块;获取对应文件的内容,加一个闭包,把内容插进去执行即可。1.提前加载需要使用的模块。因为我们只实现了CommonJS的模块加载方法,不会实现整个Node。这里需要依赖一些Node模块,于是就“厚颜无耻”的使用了Node自带的模块。require方法加载模块。//依赖模块//操作文件的模块constfs=require("fs");//处理路径的模块constpath=require("path");//虚拟机,帮我们创建一个黑盒来执行代码防止变量污染constvm=require("vm");2.创建一个Module构造函数其实我们需要通过Module构造函数为CommonJS中引入的每一个模块创建一个实例。//创建模块构造器/**@param{String}p*/functionModule(p){this.id=p;//当前文件的表示(绝对路径)this.exports={};//每个模块都有一个exports属性,用来存放模块的内容this.loaded=false;//标记是否已经加载}3.定义静态属性,存放一些我们需要用到的值//模块静态变量//后面函数中需要用到闭包的字符串Module.wrapper=["(function(exports,require,module,__dirname,__filename){","\n})"];//根据绝对路径缓存的模块的对象Module._cacheModule={};//方法处理不同文件扩展名Module._extensions={".js":function(){},".json":function(){}};4.创建导入模块的req方法为了避免与Node内置的require方法重复,我们将模拟方法重命名为req。//导入模块方法req/**@param{String}moduleId*/functionreq(moduleId){//将req传入的参数处理成绝对路径letp=Module._resolveFileName(moduleId);//生成一个newLetmodule=newModule(p);}在上面的代码中,我们首先通过Module._resolveFileName将传入的参数处理成一个绝对路径,并创建一个模块实例将绝对路径作为参数传入。现在我们实现看看Module._resolveFileName方法。5、返回绝对文件路径Module._resolveFileName方法该方法的作用是根据是否有后缀,将req方法的参数处理成带后缀的绝对文件路径。如果req的参数没有后缀,就会去按照Module._extensions的key的后缀的顺序查找文件,直到找到后缀对应的文件的绝对路径,.js先是.json,这里我们只实现对这两种文件类型的处理。//处理绝对路径_resolveFileNamemethod/**@param{String}moduleId*/Module._resolveFileName=function(moduleId){//将参数拼接成绝对路径letp=path.resolve(moduleId);//判断是否包含后缀if(!/\.\w+$/.test(p)){//创建一个.js.json数组letarr=Object.keys(Module._extensions);//循环搜索(leti=0;i=arr.length)thrownewError("notfoundmodule");}}}else{//有后缀直接返回绝对路径returnp;}};6.加载模块的load方法//改进req方法/**@param{String}moduleId*/functionreq(moduleId){//将req传入的参数处理成绝对路径let'sgotp=Module._resolveFileName(moduleId);//生成一个新模块letmodule=newModule(p);//**********以下是新代码**********//加载模块letcontent=module.load(p);//将加载后返回的内容赋值给模块实例的exports属性module.exports=content;//最后返回模块实例的exports属性,即加载模块的内容returnmodule.exports;//**********上面是新的代码************}上面的代码实现了一个实例方法load,并且传入文件路径的绝对值,加载模块的文件内容,加载后将值存入模块实例的exports属性,最后返回,其实req函数返回的是模块加载的内容//load方法//的方法模块加载Module.prototype。load=function(filepath){//判断加载文件的扩展名letext=path.extname(filepath);//根据不同的扩展名处理文件内容,参数为当前实例letcontent=Moudule._extensions[ext](this);//返回处理后的结果返回内容;};7.实现加载.js文件和.json文件的方法前面准备的静态属性方法中记得Module._extensions是用来存放这两个的,下面我们来完善一下这两个方法。//处理后缀方法的_extensions对象Module._extensions={".js":function(module){//读取js文件并返回文件内容letscript=fs.readFileSync(module.id,"utf8");//给js文件的内容添加一个闭包环境letfn=Module.wrap(script);//创建虚拟机,执行我们创建的js函数,并将this指向模块实例的exports属性vm.runInThisContext(fn).call(module.exports,module.exports,req,module);//返回模块实例上的exports属性(即模块的内容)returnmodule.exports;},".json":function(module){//.json文件的处理比较简单,将读取的字符串转换成对象即可returnJSON.parse(fs.readFileSync(module.id,"utf8"));}};我们这里使用了Module.wrap方法,代码如下,其实是帮我们加了一个闭包环境(也就是设置一层函数,传入我们需要的参数),里面的变量都是私有的。//创建闭包包装方法Module.wrap=function(content){returnModule.wrapper[0]+content+Module.wrapper[1];};Module.wrapper的两个值其实就是我们需要在外层包裹的一个函数的前半部分和后半部分。这里要重点说一下关键点,非常重要:1.我们在虚拟机中执行构建的闭包函数时,使用执行上下文/上下文调用将this指向模块实例的exports属性,所以这就是为什么我们用Node来启动一个js文件,打印这个的时候,不是全局对象global,而是一个空对象。这个空对象就是我们的module.exports,也就是当前模块实例的exports属性。2.依旧是第一个函数执行。我们传入的第一个参数是改变this的方向,第二个参数是module.exports,所以在导出每个模块的时候,使用module.exports=xxx,其实就是直接替换模块实例的值,即就是,模块的内容直接存放在模块实例的exports属性中,req最终返回的是我们模块导出的内容。3.第三个参数之所以传给req,是因为我们可能在一个模块中还引入了其他模块,req会返回其他模块的exports在当前模块中使用,这样整个CommonJS规则就建立在这边走。8.缓存加载的模块。我们当前的程序有问题。当重复加载一个已经加载过的模块时,当执行req方法时,会发现创建了一个新的模块实例。这是错误的。有道理,下面我们来实现一下缓存机制。还记得之前的静态属性Module._cacheModule,它的值是一个空对象,我们会将所有加载的模块实例存放在这个对象上。//改进req方法(句柄缓存)/**@param{String}moduleId*/functionreq(moduleId){//将req传入的参数处理成绝对路径letp=Module._resolveFileName(moduleId);//***********下面是新增的代码************//判断是否已经加载if(Module._cacheModule[p]){//模块存在,如果存在则直接返回exports对象即可returnModule._cacheModule[p].exprots;}//**********以上是新的代码***********//生成一个新的Moduleletmodule=newModule(p);//加载模块letcontent=module.load(p);//**********以下为新代码**********//存储时,以模块的绝对路径为key,对应模块内容Module._cacheModule[p]=模块;//是否缓存意味着改为truemodule.loaded=true;//********上面添加了代码************//将加载后返回的内容赋值给模块实例的exports属性module.exports=content;//最后返回模块实例的exports属性,即加载模块内容returnmodule.exports;}9.尝试req加载模块,在同级目录下新建文件a.js,使用module.exports随便导出一些内容,在我们模块加载打印内容的底部尝试导入导入。//导出自定义模块//a.jsmodule.exports="Helloworld";//检测请求方法consta=req("./a");console.log(a);//Helloworld
CommonJS模块搜索规范其实我们只实现了CommonJS规范的一部分,即自定义模块的加载。事实上,在CommonJS规范中有很多关于模块搜索的规则。具体的,我们用下面的流程图来表达。这篇文章让我们了解什么是CommonJS。主要目的是了解Node模块化的实现思路。如果想对CommonJS的实现细节有更深入的了解,建议看一下NodeJS源码的对应部分。如果你觉得源码比较多,不要很容易找到模块化实现的代码,在VSCode中调用require方法导入模块时,断点调试,后续一步步查看节点源代码。