当前位置: 首页 > 后端技术 > Node.js

前端模块化简介

时间:2023-04-03 19:17:56 Node.js

一、模块化简介1、模块化的由来——多人协作导致变量命名冲突——代码混乱不易维护基于以上问题的出现,有一个模块化解决方案。结果-可以将复杂的代码拆分成小模块,方便代码管理和维护-每个模块的直接内容相互独立,互不影响2模块化历史2.1最早的模块化方式1单例模式如果两个开发人员都有相同的变量a,可以通过以下方式区分。varname1={a:1}varname2={a:2}但是这个方法并不能彻底解决问题,毕竟name1和name2也需要取不同的名字,而且这个方法不太方便调用2自己-executingfunctionfunction(){vara=1}()function(){vara=1}()每个函数都有自己的作用域,所以上面两个函数里面的a变量不会冲突。但这种解决方案也不美观。2.2过时的模块化方法1AMD模块规范AMD——异步模块加载规范,即即使在模块加载过程中还没有获取到需要的模块,也不会影响后面代码的执行。RequireJS-AMD规范的实现。其实也可以说AMD是RequireJS推广过程中模块定义的标准化输出。示例如下://独立模块定义define({a:function(){}b:function(){}});//依赖模块定义define(['f1','f2'],function(f1,f2){a:function(){}b:function(){}});//模块引用require(['m1','m2'],function(m1,m2){m1.a();m2.b();})2CMD模块规范CMD——通用模块规范,由国内宇博提出。SeaJS——CMD的实现,其实CMD也可以说是SeaJS推广过程中模块定义的标准化输出。使用示例:define(function(require,exports,module){//依赖模块avara=require('./a');//调用模块a的方法a.method();})andAMDspecification主要区别在于定义模块和导入依赖项的部分。AMD在声明模块时需要指定所有的依赖关系,通过形参将依赖关系传递到模块内容中。CMD模块更接近于Node对CommonJS规范的定义(后面会强调)。CMD支持动态导入。require、exports和modules通过形参传递给模块。当你需要依赖模块时,你可以随时调用require()来引入它。与AMD相比,CMD提倡靠就近,AMD提倡靠前面。3UMD通用模块规范所谓的兼容模式,就是将几种通用的模块定义方式进行兼容。(function(global,factory){typeofexports==='object'&&typeofmodule!=='undefined'?module.exports=factory()//Node,CommonJS:typeofdefine==='function'&&define.amd?define(factory)//AMDCMD:(global.CodeMirror=factory());//全局挂载模块}(this,(function(){...})接下来我们介绍最主流的前端模块化方案,Node中的两个模块Node应用由模块组成,使用CommonJS模块规范,每个文件都是一个模块,有自己的作用域,一个文件中定义的变量、函数、类都是Private的,不可见2.1CommonJs简介CommonJS规范规定,在每个模块内部,module变量代表当前模块,这个变量是一个对象,它的exports属性(即module.exports)是一个外部接口,加载一个模块是实际上加载了模块的module.exports属性。//引用模块文件varx=5;varaddX=function(value){returnvalue+x;};module.exports.x=x;module.exports.addX=addX;//加载模块文件varexample=require('./example.js');console.log(example.x);//5console.log(example.addX(1));//62.2模块对象2.2.1模块实现Node提供了一个Modulebuildinside函数。所有模块都是Module的实例。在每个模块内部,都有一个表示当前模块的模块对象。functionModule(id,parent){this.id=id;this.exports={};this.parent=parent;如果(父&&parent.children){parent.children.push(这个);}this.filename=null;this.loaded=false;this.children=[];}module.id模块的标识符,通常是带有绝对路径的模块文件名。module.filename模块的文件名,带有绝对路径。module.loaded返回一个布尔值,指示模块是否已完成加载。module.parent返回一个代表调用该模块的模块的对象。module.children返回一个数组,表示该模块将使用的其他模块。module.exports表示模块导出的值。2.2.2module.exports和exportsmodule.exports属性表示当前模块的对外输出接口。当其他文件加载模块时,它实际上读取module.exports变量。为了方便起见,Node为每个模块提供了一个exports变量,指向module.exports。这相当于在每个模块的顶部都有一行这样的命令。varexports=module.exports;如果你觉得exports和module.exports的区别很难区分,一个简单的解决办法就是放弃使用exports,只使用module.exports。2.3require命令Node使用CommonJS模块规范,内置的require命令用于加载模块文件。require命令的基本功能是读取并执行一个JavaScript文件,然后返回模块的exports对象。如果没有找到指定的模块,就会报错。2.3.1node中模块的分类1.核心模块/内置模块(fshttp路径等)2.需要安装第三方模块3.自定义模块需要通过绝对或相对路径导入2.3.2Node模块中的模块分类在导入过程中,一般分为三个步骤:路径分析、文件定位、编译和执行。核心模块会省略文件定位和编译执行两步,在路径分析时会优先判断,加载速度比普通模块快。文件模块——是外部导入的模块比如在node_modules中通过npm安装的模块,或者是我们项目工程中自己写的js文件或者json文件。文件模块导入过程要经过以上三个步骤。2.3.3路径分析核心模块和文件模块都需要经过路径分析步骤。Node支持以下形式的模块标识符//coremodulerequire('http')----------------------------//文件模块//以.开头的相对路径,(可以不带扩展名)require('./a.js')//以..开头的相对路径,(可以不带扩展名)require('../b.js')//以/开头的绝对路径,(不带扩展名)require('/c.js')//外部模块名require('express')//外部模块的某个文件requires('codemirror/addon/merge/合并.js');那么对于字符串的引入,Node会先在内存中搜索匹配的核心模块,如果匹配成功则不再继续搜索(1)比如requirehttp模块,会优先匹配成功的核心模块。如果核心模块没有匹配成功,就会被归类为文件模块。(2)对于以.、..和/开头的标识符,require会把当前文件路径的相对路径或绝对路径转换成真实路径,也就是我们平时做的最常见的路径分析。(3)非路径文件模块如上面的'express'和'codemirror/addon/merge/merge.js',这种模块是一种特殊的文件模块,一般称为自定义模块。自定义模块的查找是最耗时的,因为自定义模块有一个模块路径,Node会根据这个模块路径递归查找。模块路径-Node的模块路径是一个数组,模块路径存储在module.paths属性中。模块路径的生成规则如下:当前路径文件下的node_modules目录,父目录下的node_modules目录,父目录下的node_modules目录,沿路径递归,直到根目录下的node_modules目录2.3.4文件定位扩展名分析我们在使用require的时候有时会省略扩展名,那么Node是如何定位到具体文件的呢?在这种情况下,Node会按照.js、.json、.node的顺序一一匹配。(.node是编译C++扩展文件后生成的文件。)如果扩展名匹配不上,会被当做一个包。我直接理解为npm包处理。对于包Node,它会先在当前包目录下搜索包。.json(CommonJS包规范)通过JSON.parse()解析包描述对象,根据main属性指定的入口文件名定位下一步。如果文件缺少扩展名,则根据扩展名分析规则进行定位。如果main指定的文件名错误或根本没有package.json,Node将使用包目录中的索引作为默认文件名。然后依次匹配index.js、index.json、index.node。如果以上步骤均未定位成功,则进入下一个模块路径——父目录下的node_modules目录查找,直至找到根目录下的node_modules。如果都找不到,则抛出查找失败的异常。2.3.5用模块编译.js文件——通过fs模块同步读取文件然后编译执行.node文件——C/C++写的扩展文件,通过dlopen()加载最终编译好的文件方法。.json——通过fs模块同步读取文件后,使用JSON.parse()解析并返回结果。其余的扩展文件。它们都作为.js文件加载。2.4CommonJS模块加载机制网上很多地方都说CommonJS模块的加载机制是输入是输出值的副本。这句话是错误的。以下面的代码为例://index.jsconst{ss}=require('./lib');constlib=require('./lib');console.log('ss',ss);console.log('lib',lib);setTimeout(()=>{console.log('ss',ss);console.log('lib',lib);},3000);//lib.jsmodule.exports.ss='ss1';setTimeout(()=>{module.exports.ss='ss2';console.log('module.exports',module.exports);},2000);//执行结果ssss1lib{ss:'ss1'}libmodule.exports{ss:'ss2'}ssss1lib{ss:'ss2'}从执行结果可以看出commonjs导出了对象module.exports,导出的值为添加到此对象的新属性会影响导入的值。const{ss}=require('./lib');等价于const{ss}={ss:'ss1'};解构赋值等价于constss='ss1';所以修改导出对象的ss不能使导入对象ss也变成2。3ESModuleES6在语言规范层面实现模块功能,编译时加载,可以完全替代CommonJS和AMD规范,可以成为浏览器和服务器的通用模块解决方案。3.1ES6模块使用——export//导出变量exportvarname='pengpeng';//导出函数exportfunctionfoo(x,y){}//推荐常用的导出方式//person.jsconstname='dingman';const年龄='18';constaddr='Carlsterforest';export{name,age,addr};//asusageconsts=1;export{sast,sasm,}3.2ES6模块用法——import//一般用法import{name,age}from'./person.js';//Asusageimport{nameaspersonName}from'./person.js';//整体加载import*aspersonfrom'./person.js';console.log(person.name);console.log(person.age);3.3ES6模块使用——exportdefaultexportdefault其实在项目中用的比较多。通常,我们对Vue组件或React组件使用exportdefault命令。注意使用exportdefault命令时,import不需要加{}。不使用exportdefault时,import必须加{},例子如下://person.jsexportfunctiongetName(){...}//my_moduleimport{getName}from'./person.js';----------------对比--------------------//person.jsexport默认函数getName(){...}//my_moduleimportgetNamefrom'./person.js';exportdefault实际上是导出一个名为default的变量,所以它后面不能跟变量声明语句。值得注意的是,我们可以同时使用export和exportdefault//person.jsexportname='dingman';exportdefaultfunctiongetName(){...}//my_moduleimportgetName,{name}from'./person.js';3.4ES6模块和CommonJS模块加载的区别ES6模块的设计思想是尽可能让它静态化,这样模块的依赖关系和输入输出的变量在编译的时候就可以确定。所以ES6是在编译时加载的,不同于CommonJS运行时加载(实际上是加载整个对象)。ES6模块不是对象,而是通过export命令明确指定输出代码,输入也是静态命令的形式。://ES6模块import{basename,dirname,parse}from'path';//CommonJS模块let{basename,dirname,parse}=require('path');上面的写法和CommonJS模块加载有什么区别?当需要路径模块时,CommonJS会实际运行路径模块并返回一个对象,该对象将被缓存。该对象包含路径模块的所有API。无论以后这个模块被加载多少次,都会取这个缓存的值,这是第一次运行的结果,除非手动清除。ES6只会从path模块中加载3个方法,其他的不会加载,都是编译时加载的。ES6可以在编译时完成模块加载。ES6遇到import时,不会像CommonJS那样执行模块,而是生成一个动态的只读引用,真正需要的时候再去模块中取值,所以ES6模块是动态引用,不会缓存值。四小结以上介绍了模块化的一些知识,欢迎批评指正!