有几个重要的模块化规范:commonjs、ES6模块机制、AMD、CMD。由于本人在业务上接触过Vue+webpack+babel架构项目,在打包代码时更多使用ES6规范,对其他模块化规范不熟悉,所以在此记录一下自己学习到的模块化知识。CommonJS模块化的目的是创建一个安全封闭的范围,并有一个易于引用的接口。按照我的理解,可以分为模块定义和模块介绍两部分。模块中有一个模块对象,代表模块本身,可以通过在exports属性上挂载需要导出的API来定义导出的API;CommonJS规范中有一个require()方法,用于接受模块标识符。将模块导入当前上下文。1.模块定义要了解一个模块是如何定义的,首先要了解模块对象。在Node中,每个文件模块都是一个对象,即模块对象。其定义如下:functionModule(id,parent){this.id=id//模块标识符this.exports={}//模块导出的值this.parent=parent//调用该模块的模块。当parent为null时表示该模块为入口模块if(parent&&parent.children){parent.children.push(this)}this.filename=filename//文件名this.loaded=false//是否this.childrenhasbeenloaded=[]//代表这个模块调用的其他模块}定义一个模块的目的是为了定义输出值。写法很简单。例如?functionsayHello(){console.log('hello')}module.exports=sayHello//或exports.sayHello=sayHello为了方便接口的导出,Node也定义了一个exports变量,但是有a容易踩的坑是exports只是一个引用,本来就是指向module.exports的。如果你只是给exports变量赋值,exports变量将失去它对module.exports的引用。最后,必须在module.exports上定义一个接口来实际导出值。先说解决方案,常见的写法是:exports=module.exports=sayHello//或者严格只在exports变量中添加属性exports.sayHello=sayHello,然后举例说明具体场景犯了一个错误://a.jsexports。name='kent'exports.sayHi=function(){console.log('hi')}console.log(module)//{exports:{name:'kent',sayHi:function(){console.log('hi')}}}//如果exports被重新赋值=_=exports={name:'nicolas',sayBye:function(){console.log('bye')}}//模块中的exports属性为不会有变化console.log(module)//{exports:{name:'kent',sayHi:function(){console.log('hi')}}}console.log(exports)//{name:'nicolas',sayBye:function(){console.log('bye')}}//b.js//require时读取的名字还是kentvarperson=require('a.js')console.log(person.name)//kent具体原因也可以从模块机制中看出functionrequire(...){varmodule={exports:{}};((module,exports)=>{//你的模块代码在这里。在这个例子中,定义一个函数。functionsome_func(){};exports=some_func;//在这一点上,exports不再是模块的快捷方式。exports,and//这个模块仍然会导出一个空的默认对象。模块。exports=some_func;//此时,模块现在将导出some_func,而不是//默认对象。})(模块,模块.exports);returnmodule.exports;}2.Moduleimportmoduleimport的语法也很简单,上一节已经讲过了。这是另一个?//book.jsexports.name='javascript'exports.logName=function(){console.log('javascript')}//main.jsvarbook=require('./book.js')//require的参数是模块标识console.log(book.name)//'javascript'book.logName()//'javascript'下面详细说说模块引入的步骤。不过在此之前,你需要了解两个概念:核心模块和文件模块。在Node中,有一些模块是Node自己提供的,称为核心模块。Node进程启动时,核心模块直接加载到内存中。因此,核心模块的引入只需要一步路径分析,其加载速度是最快的。另一部分在运行时动态加载。通常,有带有路径标识符的用户定义模块,或自定义模块(例如第三方提供的包)。这类模块需要完成以下三个步骤:路径解析、文件定位、编译执行。①路径分析:路径分析可以理解为对模块标识符的分析。Node中的模块标识主要包括:核心模块,如:http、fs等;以“./”或“../”开头的相对路径模块,相对于当前目录位置;以“/”开头的绝对路径模块;·非路径文件模块,类似于核心模块的标识符。Node在所有级别搜索node_modules目录。·核心模块:核心模块解析完路径后直接加载。需要注意的是,自定义文件模块不能与核心模块标识相同,也不能替换不同的标识,也不能使用相对路径或绝对路径标识。·路径形式的文件模块:在解析文件模块时,require方法会将路径转换为真实的路径,并以此为索引编译模块并存入缓存(缓存加载将在下文介绍).·非路径文件模块(自定义模块):自定义模块的路径解析,在我们引用第三方库的时候经常会遇到。这种非路径文件模块在加载时,会以模块路径为线索一步步查找。比如?://在"/Users/zhazheng/Documents/www"下新建module_path.js//module_path.jsconsole.log(module.paths)//执行module_path.jsnodemodule_path//得到如下log['/Users/zhazheng/Documents/www/node_modules','/Users/zhazheng/Documents/node_modules','/Users/zhazheng/node_modules','/Users/node_modules','/node_modules']可以看到,这样的模块它会从当前文件目录一步步递归到根目录下的node_modules目录。因此,此类模块的路径分析是最耗时的。②文件定位:文件定位主要包括文件扩展名解析、目录和包处理。·文件扩展名分析:文件扩展名不包括在分析标识符的过程中是很常见的。在标识符不包含文件扩展名的情况下,Node将依次尝试以下三种扩展名:.js、.json、.node。由于尝试解析的过程是同步阻塞的,大量解析的文件扩展名会导致性能问题。在这种情况下,您可以尝试添加扩展或充分利用缓存加载。·目录解析和包处理:如果扩展名解析后没有找到对应的文件,只得到一个目录,那么Node会把这个目录当作一个包。首先会检查当前目录下是否有package.json文件,如果有则检查是否有main属性(main属性指向入口文件)。如果没有package.json文件或者package.json中没有main属性,那么Node会默认使用index作为文件名,最后重复“文件扩展名分析”的步骤。3、缓存加载实际上,Node模块无论是核心模块还是文件模块,在第一次加载后都会被缓存起来。require()方法将缓存重新加载的模块。所以如果有多次加载模块的需求,那么需要记得先从缓存中删除模块。缓存都存储在require.cache对象中。删除单个模块或者模块的所有缓存,可以这样写://删除单个模块缓存deleterequire.cache[moduleName]//删除所有模块缓存Object.getOwnPropertyNames(require.cache).forEach(key=>{deleterequire.cache[key]})当然,一般情况下,缓存可以带来性能上的优势。对于具有非常深层嵌套路径的自定义文件模块尤其如此。4.循环加载循环加载是一个无法回避的问题。在Node中,你需要了解循环加载的性能。首先要明白require是一个同步加载过程,读取接口只指向exports对象中的属性,例如?:(以下三个模块都在同一个目录下)//a.jsexports.name='a1'console.log(`a.js,${require('./b.js').name}`)exports.name='a2'//b.jsexports.name='b1'console.log(`b.js,${require('./a.js').name}`)exports.name='b2'//main.jsconsole.log(`main.js,${require('./a.js').name}`)console.log(`main.js,${require('./b.js').name}`)nvmrunnode然后.loadmain.js得到下面的结果b.js,a1a.js,b2//这两行的结果应该大致明白了两个模块的require方法发生了什么main.jsa2main.jsb2再次执行.loadmain.js会读取到缓存结果main.jsa2main.jsb2循环加载示例代码可以在我的github中查看AMD...未完待续
