模块化是一种将JavaScript程序拆分为单独模块的机制,可以按需导入。在JavaScript脚本变得越来越大、越来越复杂的今天,JavaScript的模块化机制也变得越来越重要。现在,几乎所有最新的浏览器都支持js原生模块化机制。模块化的意义何在?js模块化机制将js代码拆分成不同的小文件,有以下好处:每个文件都有私有命名空间,避免了全局污染和变量冲突。逻辑分离,不同的逻辑代码可以放在不同的js文件中。提高代码的可重用性、可维护性和可读性Object-basedandclosure-basedmodularizationObject-basedmodularization在CommonJs和ES6Module出现之前,为了避免全局变量污染,一种常见的方法是将一个类分类,将变量放在一个对象中,这样每个对象中的属性(变量)都是对象私有的,避免了变量冲突的问题。leta={sayHello:'hello1'}letb={sayHello:'hello2'}这样即使出现相同的变量名也不会造成冲突。将每个逻辑点相关的变量放到一个对象中,尽量减少变量污染的情况。js内置对象Math也是采用这种思路实现的。Closure-basedmodularIIFE(immediatelyinvokedfunctionexpression)IIFE是一个定义时会被调用的函数。定义IIFE非常简单。你只需要写两个括号。第一个括号声明匿名函数,第二个括号声明匿名函数。实际参数在两个括号中传递。//IIFE有两种写法,都可以正常使用(function(arg){console.log(arg)})(1);//必须在IIFE之后添加一个分号来表示结束//1(function(arg){console.log(arg)}(2));//IIFE后必须加分号表示结束//2立即调用函数表达式(IIFE)有以下优点:函数中的变量不会造成全局污染。函数执行完后会立即销毁,不会造成资源浪费。想象一个解析代码文件的工具,将每个文件的内容包装到一个立即函数表达式中,并跟踪每个函数的返回值,将所有内容组装到一个大文件中。一些代码打包工具就是基于这种思想实现的。两种方式的缺点虽然两种方式都可以实现私有命名空间,避免变量污染的问题,但是仍然存在一些明显的缺陷。对于基于对象的模块化:声明一个变量成为声明对象的一个??属性,没有办法使用一些有用的机制来声明变量,这可能会导致属性重复命名导致的属性覆盖问题。对象之间可能存在覆盖代码还在一个文件中,会导致文件中的代码越来越多//不小心覆盖了属性bleto={b:1}o.a=1;o.b=2;//覆盖属性b,js中不会有提示leta=1;让b=2;让b=3;//js会报错,提示不能重复声明对于基于闭包的模块化:IIFE中的变量和函数不可重用,使用不方便,测试困难,维护困难。另外,这两种方法都不是真正理想的模块化。两者都不能将代码拆分到不同的文件中,难以维护。私有命名空间的实现存在缺陷。等问题。Node.js模块化(CommonJs)理想的模块化应该是不同的代码可以拆分成不同的文件,有利于可维护性和可读性,不同的代码文件可以相互导入。有利于复用,每个代码文件都有一个私有的命名空间,可以避免变量冲突和污染。CommonJs的模块机制实现了上述需求。CommonJs是Nodejs内置的模块化机制。它可以将一个复杂的程序拆分成任意数量的代码文件,每个文件都是一个独立的模块,有私有的命名空间,你可以选择导出一个或全部的变量和函数,另一个代码文件可以导入到你自己的文件中,实现了变量和函数的复用。节点导出节点导出有两种方式,一种是module.exports,一种是exports。这两个对象是全局内置的,可以直接使用。它们的用法如下//你可以一个一个地导出变量exports.a="a";exports.b=123;exports.fn=function(){console.log('我是一个函数,它还是anonymous')}//记住,这样写是不行的,具体原因后面会解释exports={a,b}//也可以一起导出letc=true;letfn2=function(){console.log('我是函数,还是匿名的')}module.exports={c,fn2}你可能觉得module.exports和exports很像,其实它们是有关系的,module.exports而exports指的是同一个对象,也就是说exports.a相当于module.exports.a。详细来说,module.exports是一个对象,node会暴露它的属性和方法供其他模块导入,而exports是一个指向module.exports的变量,exports.xx其实就是module.exports.xx。同时这也解释了为什么不能直接让exports=xx,因为这样会改变exports原来的方向。node的导入既然有导出,自然就有导入。nodejs模块通过调用require()实现其他模块数据的导入。你可能见过以下两种导入方法constfs=require('fs');constuser=require('./user.js')第一种是引入nodejs内置的fs模块,不写路径,第二种是引入用户写的模块,所以路径一定要写(路径可以是相对路径或绝对路径)。模块的分类其实前面我们提到的node内置模块也称为核心模块,在编译nodejs源码的时候会编译成二进制文件。当nodejs启动时,这些核心模块会直接加载到内存中。因此,在加载核心模块时,相对于文件模块,核心模块在引用时不需要进行文件定位和动态编译,在速度上具有优势。导入时直接写模块名不填路径,比如http、fs、path等常用模块。属于核心模块。用户编写的代码模块称为文件模块,文件模块根据导入方式也分为路径导入模块和自定义模块。constexpress=require('express');//自定义模块constusersRouter=require('./routes/users');//以路径形式引入的模块对于以路径形式引入的模块,自精确路径提供了,在导入的时候,require方法会将指定的路径转换为硬盘上的真实路径,并以此路径为索引来缓存编译后的结果。因为指定了路径,这种形式的文件模块可以节省大量的路径分析时间,比自定义模块快,但比核心模块慢。核心模块文件模块导入带有路径的模块。自定义模块路径分析对于自定义模块,自定义模块遵循以下策略进行路径分析,这会消耗大量时间。找到当前目录下的node_modules目录,看是否匹配找到父目录下的node_modules目录,看是否匹配按照这个规则查找父目录,直到找到根目录下的node_modules文件。路径解析完成后,导入的路径没有文件扩展名,node会解析文件扩展名,会按照.js、.node、.json的顺序一一尝试。如果路径不是指向文件而是指向目录,那么:首先会在hit目录下查找package.json文件并用JSON.parse解析,取出json中main属性的值文件作为命中文件。如果没有找到package.json或者对应的main属性,那么会以该目录下的index文件作为命中文件,仍然会按照.js、.node、.json的顺序一一尝试。如果仍然没有找到该索引,那么该文件定位失败,就会按照上面说的路径遍历规则,继续向上一层查找。由于是逐层搜索,自定义模块的路径解析需要消耗大量的事件,会导致搜索效率低下。因此,自定义模块加载性能比基于路径的加载慢。缓存此外,node会缓存导入的模块。下次引用时,会先检查缓存中是否有对应的文件,优先从缓存中加载,减少不必要的消耗。总结因此,node可以使用module.exports和exports进行导出操作,其中module.exports指向要导出的对象,exports指向module.exports;node通过require()进行导入操作,导入方式可以是有路径的,也可以是无路径的(核心模块、自定义模块)。node的模块分为以下几类:核心模块文件模块路径导入的模块自定义模块加载速度:缓存>核心模块>路径导入的模块>自定义模块ES6模块化ES6为JavaScript添加导入导出键支持模块化为核心语言功能。从概念上讲,ES6Module与CommonJs基本相同,都是将代码拆分成不同的代码文件。每个代码文件都是一个模块,模块之间可以相互导入导出。ES6Module基本使用//a.jsexporta='a';exportfn1(){console.log("helloES6")}//和CommonJs一样,es6Module也可以一起导出letb=1;letfn2=function(){console.log("helloES6module")}export{b,fn2}//b.jsimportafrom"./a.js"console.log(a.a);a.fn1()//helloES6Note:export{b,fn2}看似声明了一个对象字面量,但实际上这里的花括号并没有定义对象字面量。这种导出语法只需要一对花括号给出一个逗号分隔的列表。另外,ES6模块会自动采用严格模式,不管你有没有加“usestrict”;是否到模块头。扩展除了上面的基本用法外,import和export还有一些扩展的使用方法。使用解构import{a}from"./a.js"console.log(a)//等于importafrom"./a.js"console.log(a.a)重命名import{aasbbyaskeyword}from"./a.js"console.log(b)//等于import{a}from"./a.js"console.log(a)//也可以在导出的时候设置别名export{aasb}执行模块,但不导入任何值。你可以这样写import"./a.js"。如果多次执行同一个import语句,只会执行一次,不会执行多次。整体加载,即使用星号(*)指定一个对象,所有输出值都加载到这个对象上。import*asmoduleAfrom'./a.js';console.log(moduleA.a)//'a'从上面的例子中,导入需要知道导出的函数/变量名才能使用,也就是不方便的话,我们可以设置一个默认值。//moduleA.jsexportdefaultfunction(){console.log('foo');}这样导入的时候不需要知道moduleA导出的函数或者变量的名称。//moduleB.js//可以取任意名importfnfrom"./moduleA"fn()//上面代码foo的import命令可以使用任意名指向moduleA.js的输出方法,所以此时你不需要知道原模块导出的函数名。需要注意的是,此时导入命令后不使用花括号。注意:一个模块中只能有一个导出默认值。exportdefault本质上就是导出一个名为default的变量或方法,然后系统允许你给它取任何名字。因此,下面的写法也是成立的。//modules.jsfunctionadd(x,y){returnx*y;}export{addasdefault};如果export和import复合写在一个模块中,同一个模块先输入再输出,import语句可以和export语句写在一起。export{foo,bar}from'my_module';//可以简单理解为import{foo,bar}from'my_module';export{foo,bar};更多复合写法见Webway-ES6静态导入和动态导入使用的import导入有以下特点:有提升行为,会提升到整个模块的头部。首先,导入的变量和函数是只读的,因为它的本质是导入导出命令只能在输入接口编译的时候导入。在模块的顶层,不能在代码块中(比如在if代码块中,或者在函数中),因为引擎在编译阶段处理import,模块内容在import之前代码被执行。此时if语句不会被解析或执行。//Errorif(true){importmoduleAfrom"./a.js"}正是因为import是静态导入,所以要导入的模块在写代码的时候就确定了,而不能在代码写的时候确定被执行。//报错constpath='./'+fileName;importafrompath;为了解决这个问题,ES2020提案引入了import()函数来支持动态加载模块。//上面的例子可以这样写constpath='./'+fileName;import(path).then(module=>{}).catch(err=>{});import和import()最大的区别是前者是静态导入,后者是动态导入,后者返回一个Promise。import()函数可以在任何地方使用,不仅可以在模块中使用,也可以在非模块脚本中使用。它是在运行时执行的,也就是说,当这句话运行时,就会加载指定的模块。另外,import()函数与加载的模块没有静态连接关系,这一点也不同于import语句。import()类似于Node的require方法,主要区别在于前者是异步加载,后者是同步加载。import()适用于:按需加载条件加载动态路径加载import()也可以配合解构和asyn/await使用。import('./myModule.js').then(({export1,export2})=>{//...});asyncfunctionmain(){constmyModule=awaitimport('./myModule.js');const{export1,export2}=awaitimport('./myModule.js');//同时加载多个模块const[module1,module2,module3]=awaitPromise.all([import('./module1.js'),import('./module2.js'),import('./module3.js'),]);}main();CommonJsvsEs6ModuleCommonJsEs6Module支持node程序和web,未来会支持nodeCommonJs可以动态加载语句,代码发生在运行时。EsModule可以动态导入,也可以静态导入。CommonJs的导出值是一个副本,导出值是可以修改的。ES6Module的输出是对值的引用,它是只读的。CommonJs会缓存导入的模块,不会缓存值,写入到最后。随着官方对模块化作为核心语言特性的支持,未来的模块化方案很可能会统一使用ES6Module(Node13开始支持ES6module),但由于绝大多数node程序都使用CommonJs,而这两个module解决方案在未来很长一段时间内仍可能并行使用。最后,码字不易。如果本文对您有帮助,请点个赞,谢谢。参考部分参考自:Netway-ES6Module《JavaScript权威指南第七版》
