为什么要使用模块化?当我们的项目越来越大,维护起来肯定不是那么方便,多人协作开发,肯定会遇到很多问题,比如:方法覆盖:很有可能你定义的一些函数会覆盖函数在公共类中使用相同的名称,因为您可能根本不知道公共类中有哪些函数,也不知道它们是如何命名的。这些公共组件:但是你不知道这些组件会依赖于哪些模块。同时,在维护这些公共方法的时候,会增加或者删除一些依赖,所以每一个引入这些公共方法的地方都需要相应的添加或者删除。等等,还有很多问题。我们采用模块化的方式,让每个模块相对独立。可能每个文件就是一个功能块,可以满足特定的功能,这样我们引用某个功能就会很方便。CommonJSNode应用由模块组成,采用CommonJS模块规范。每个文件都是一个模块,有自己的作用域。文件中定义的变量、函数和类都是私有的,对其他文件不可见。CommonJS规范的加载模块是同步的,即加载完成后才能进行以下操作。Node.js主要用于服务端编程,模块一般存放在本地硬盘,加载速度比较快,所以Node.js采用了CommonJS规范。CommonJS模块的输出是值的缓存,在运行时加载。CommonJS规范规定,在每个模块内部,模块变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是一个外部接口。加载模块实际上是加载模块的module.exports属性。//tools.tsconstadd=(a:number,b:number)=>{returna+b}constreduce=(a:number,b:number)=>{returna-b}constmulty=(a:number,b:number)=>{returna*b}exports.add=addexports.reduce=reduceexportdefaultmulty//相当于module.exports={add,reduce}//app.tsconsttools=require('./tools.ts')tools.add(2,3)//5tools.reduce(3,2)//1Node内部提供了一个Module构造函数。所有模块都是Module的实例。//模块构造函数Module(id,parent){this.id=id//模块的标识,通常是带绝对路径的模块文件名this.exports={}//表示模块对外输出值this.parent=parent//返回一个代表调用该模块的模块的对象...}//tools.tsconstadd=(a:number,b:number)=>{returna+b}exports.add=addconsole.log(module)//输出模块{id:'.',exports:{add:[Function:add]},parent:null,filename:'C:\\Users\\viruser.v-desktop\\Desktop\\zz\\aa.js',//模块的文件名,加载绝对路径:false,//返回一个布尔值,表示模块是否已经加载children:[],//返回一个数组,表示本模块要使用的其他模块路径:['C:\\Users\\viruser.v-desktop\\Desktop\\zz\\node_modules','C:\\Users\\viruser.v-desktop\\Desktop\\node_modules','C:\\Users\\viruser.v-desktop\\node_modules','C:\\Users\\node_modules','C:\\node_modules']}如果调用于命令行某个模块,比如nodetools.js,那么module.parent为null。如果是在脚本中调用,比如require('./tools.js'),那么module.parent就是调用它的模块。利用这一点可以判断当前模块是否为入口脚本。if(!module.parent){//使用`nodesomething.js`运行app.listen(8088,function(){console.log('applisteningonport8088')})}else{//使用`require('/.something.js')`module.exports=app}为了方便,Node为每个模块提供了一个exports变量,指向module.exports。这相当于在每个模块的顶部都有一行这样的命令。因此,在对外导出模块接口时,可以在exports对象中添加方法。注意不能直接将exports变量指向一个值,因为这相当于切断了exports和module.exports之间的联系。constexports=module.exportsAMDRequireJS同学应该都知道,RequireJS是基于AMD规范的。AMD是“AsynchronousModuleDefinition”的缩写,意思是“异步模块定义”。它异步加载模块,模块的加载不影响后面语句的运行。所有依赖该模块的语句都定义在一个回调函数中,回调函数只有在加载完成后才会运行。并且也是在运行时加载,使用require.config()指定引用路径等,使用define()定义模块,使用require()加载模块,但是不像CommonJS需要两个参数:定义模块//define([module],callback)define(['myLib'],()=>{functionfoo(){console.log('mylib')}return{foo:foo }})require.config({baseUrl:"js/lib",paths:{"jquery":"jquery.min",//实际路径是js/lib/jquery.min.js"underscore":"underscore.分钟",}});使用模块//require([module],callback)require(['myLib'],mod=>{mod.foo()})//为什么myLib使用AMD规范?因为AMD在浏览器规范中专门针对js环境设计的。它吸收了CommonJS的一些优点,但不是全部。它也非常易于使用。CMDCMD与AMD有很多相似之处,这里只说说两者的不同点。首先,CMD规范兼容CommonJS规范,比AMD简单多了。一个遵循CMD规范,可以在Node.js中运行的模块。SeaJS推荐用CMD写,所以用SeaJS写一个简单的例子://AMD写AMD的依赖需要预先写define(["a","b","c","d","e","f"],function(a,b,c,d,e,f){//等于在顶部声明和初始化所有要使用的模块a.doSomething()if(false){//即使如果某个模块b没有使用,b还是会提前执行b.doSomething()}})//CMD写入CMD依赖可以就近写入,不需要提前声明define(function(require,exports,module){vara=require('./a')//在需要时声明同步a.doSomething()if(false){varb=require('./b')b.doSomething()}require.async('a',math=>{//异步a.add(1,2);})})/**sea.js**///定义模块math.jsdefine(function(require,exports,module){var$=require('jquery.js')varadd=function(a,b){returna+b}exports.add=add})//加载模块seajs.use(['math.js'],function(math){varsum=math.add(1+2)})CMD规范我们可以发现它的API有特定的职责。比如同步加载和异步加载的API分为require和require.async,而AMD的API更加通用。UMDUMD是AMD和CommonJS的组合。AMD模块是基于浏览器优先的原则开发的,模块是异步加载的。CommonJS模块基于服务器优先原则开发,选择同步加载,其模块不需要打包(unwrappedmodules)。这就迫使人们想出另一种更通用的模型UMD(UniversalModuleDefinition)。希望有一个跨平台的解决方案。UMD首先判断支持Node.js的模块(exports)是否存在,存在则使用Node.js模块模式。在判断是否支持AMD时(define是否存在),存在则使用AMD方式加载模块。(function(window,factory){if(typeofexports==='object'){module.exports=factory()}elseif(typeofdefine==='function'&&define.amd){define(factory)}else{window.eventUtil=factory()}})(this,function(){//module...})ESModule从历史上看,JavaScript没有模块系统,不可能将一个大程序拆分成相互依赖的小文件,然后以简单的方式组装。其他语言都有这个特性,比如Ruby的require,Python的import,甚至CSS都有@import,但是JavaScript对此没有任何支持,这给开发大型复杂项目带来了巨大的障碍。ES6模块的设计思想是尽可能静态化,这样模块的依赖关系,以及输入输出变量都可以在编译时确定。CommonJS和AMD模块都只能在运行时确定这些东西。例如,CommonJS模块是对象,必须在导入时查找对象属性。模块功能主要由两个命令组成:export和import。export命令用于指定模块对外接口,import命令用于导入其他模块提供的功能//a.jsvarnum1=1varnum2=2export{num1,num2}//b.jsimport{num1,num2}from'./a.js'functionadd(num1,num2){returnnum1+num2}console.log(add(num1,num2))如果要重命名输入变量,导入命令必须使用as关键字,输入变量重命名。import{num1assnum}from'./a'导入命令有提升作用,会提升到整个模块的头部add();如果import{add}from'./tools'在一个模块中,则先输入再输出同一个模块,import语句可以和export语句一起写。export{es6asdefault}from'./a'//等同于import{es6}from'./a'exportdefaultes6也可以使用整体加载,即指定带星号(*)的对象,并且所有输出值都加载到这个对象上,但不包括defaultimport*astoolsfrom'./a'importmultyfrom'./atools.add(2,1)//3tools.reduce(2,1)//1multy(2,1)//2ES6模块加载实际的ES6模块加载机制与CommonJS模块完全不同。CommonJS模块输出值的副本,而ES6模块输出对值的引用。CommonJS模块输出一个输出值的副本,即一个值一旦输出,模??块内部的变化不会影响这个值//lib.jsvarcounter=3functionincCounter(){counter++;}module.exports={计数器:计数器,incCounter:incCounter,}//main.jsvarmod=require('./lib')console.log(mod.counter)//3mod.incCounter()console.log(mod.counter)//3lib.js模块加载后,其内部变化不会影响输出的mod.counter。这是因为mod.counter是原始值,将被缓存。除非写成函数,否则可以获得内部变化的值//lib.jsvarcounter=3functionincCounter(){counter++}module.exports={getcounter(){returncounter},incCounter:incCounter,}//main.jsvarmod=require('./lib')console.log(mod.counter)//3mod.incCounter()console.log(mod.counter)//4ES6模块的运行机制不同于CommonJS,遇到moduleloading命令导入时,模块不会被执行,只会生成一个动态的只读引用。当你真正需要使用它时,去模块中获取值。也就是说,ES6的输入有点像Unix系统的“符号链接”。如果原值发生变化,导入输入的值也会发生变化。因此,ES6模块是动态引用,不会缓存值。模块中的变量绑定到它们所在的模块。
