当前位置: 首页 > Web前端 > HTML

个人记录——前端Module加载实现机制

时间:2023-03-27 23:23:45 HTML

1.什么是前端模块化模块化开发?模块是实现特定功能的文件。有了模块,我们就可以更方便的使用别人的代码。Whattouse为函数加载了什么模块。2.模块化开发的好处1)避免变量污染和命名冲突2)提高代码利用率3)提高可维护性4)依赖管理3.浏览器加载默认情况下,浏览器会同步加载JavaScript脚本,即如果渲染引擎遇到带有体积大,下载和执行的时间会很长,从而导致浏览器被阻塞,用户会觉得浏览器“卡死”了,没有任何反应。这显然是一种非常糟糕的体验,所以浏览器允许异步加载脚本。以下是异步加载的两种语法。https://zhuanlan.zhihu.com/p/...https://zhuanlan.zhihu.com/p/...https://zhuanlan.zhihu.com/p/...https://zhuanlan.zhihu.com/p/...https://zhuanlan.zhihu.com/p/...https://zhuanlan.zhihu.com/p/...https://zhuanlan.zhihu.com/p/...https://zhuanlan.zhihu.com/p/...https://zhuanlan.zhihu.com/p/...https://zhuanlan.zhihu.com/p/...一旦使用async属性,也允许ES6模块嵌入网页,语法行为与加载外部脚本完全相同。对于外部模块脚本(上例中的foo.js),有几点需要注意:①代码运行在模块作用域,而不是全局作用域。模块内部的顶级变量,外部不可见。②模块脚本自动采用严格模式,无论是否声明usestrict。③模块之间,可以使用import命令加载其他模块(.js后缀不能省略,需要提供绝对或相对URL),也可以使用export命令导出对外接口。④在模块中,顶层this关键字返回undefined,而不是指向window。也就是说,在模块的顶层使用this关键字是没有意义的。⑤如果多次加载同一个模块,则只会执行一次。4.ES6模块和CommonJS模块的区别①CommonJS模块输出一个值的副本,而ES6模块输出一个值的引用。②CommonJS模块是运行时加载的,ES6模块是编译时的输出接口。③CommonJS模块的require()是同步加载模块,ES6模块的import命令是异步加载,有独立的模块依赖解析阶段。5、Node.js的模块加载方式JS目前有两个模块:一个是ES6模块,简称ESM;另一个是CommonJS模块,简称CJS。CommonJS模块是Node.js特定的,与ES6模块不兼容。在语法上,两者最明显的区别是CommonJS模块使用require()和module.exports,而ES6模块使用import和export。他们采用不同的加载方案。从Node.jsv13.2开始,Node.js默认开启了ES6模块支持。Node.js要求ES6模块使用.mjs后缀的文件名。也就是说,只要在脚本文件中使用import或export命令,就必须使用.mjs后缀。当Node.js遇到.mjs文件时,它会将其视为ES6模块。默认情况下启用严格模式,因此您不必在每个模块文件的顶部指定“usestrict”。如果不想将后缀名改为.mjs,可以在项目的package.json文件中指定type字段为module。设置后,此目录中的JS脚本将被解释为ES6模块。如果此时要使用CommonJS模块,需要将CommonJS脚本的后缀改为.cjs。如果没有类型字段,或者类型字段是commonjs,.js脚本将被解释为CommonJS模块。一句话总结:.mjs文件总是作为ES6模块加载,.cjs文件总是作为CommonJS模块加载,.js文件的加载取决于package.json中type字段的设置。注意,尽量不要混用ES6模块和CommonJS模块。require命令无法加载.mjs文件,会报错。只有import命令可以加载.mjs文件。反之,.mjs文件中不能使用require命令,必须使用import。6、package.json的main字段package.json文件有两个字段来指定模块的入口文件:main和exports。对于比较简单的模块,可以只用main字段来指定模块加载的入口文件。如果没有类型字段,index.js将被解释为CommonJS模块。然后,导入命令可以加载这个模块。7、package.json的exports字段优先级高于main字段。它有多种用法:①子目录别名:package.json文件的exports字段可以指定脚本或子目录的别名②主别名:如果exports字段的别名是.,代表模块的主入口,带一个优先级高于main字段,可以直接简写为exports字段的值。③条件加载:使用别名。为ES6模块和CommonJS指定不同的条目。目前,此功能需要在Node.js运行时打开--experimental-conditional-exports标志。8、CommonJS模块加载ES6模块CommonJS的require()命令无法加载ES6模块,会报错,只能使用import()方法加载。require()不支持ES6模块的原因之一是它是同步加载的,而ES6模块内部可以使用顶层的await命令,这样可以防止它们被同步加载。9、ES6模块加载CommonJS模块ES6模块的import命令可以加载CommonJS模块,但只能整体加载,不能只加载单个输出项。这是因为ES6模块需要支持静态代码解析,而CommonJS模块的输出接口是module.exports,是一个不能静态解析的对象,所以只能整体加载。加载单个输出项可以这样写:importpackageMainfrom'commonjs-package';const{方法}=packageMain;还有一种替代加载方式,就是使用Node.js内置的module.createRequire()方法。10.同时支持两种格式的模块一个模块同时支持CommonJS和ES6格式也是很容易的。如果原来的模块是ES6格式的,那么需要给出一个整体的输出接口,比如exportdefaultobj,这样CommonJS就可以用import()来加载了。如果原始模块是CommonJS格式,那么可以添加一个包装层。Node.js的内置模块Node.js的内置模块可以整体加载,也可以加载特定的输出项。11.加载路径ES6模块的加载路径必须给出脚本的完整路径,不能省略脚本的后缀。如果import命令和package.json文件的main字段省略了脚本的后缀名,会报错。为了和浏览器的导入加载规则一致,Node.js的.mjs文件支持URL路径。目前Node.js的import命令只支持加载本地模块(file:protocol)和data:protocol,不支持加载远程模块。另外,脚本路径只支持相对路径,不支持绝对路径(即以/或//开头的路径)。12.内部变量ES6模块应该通用。同一模块无需修改即可用于浏览器环境和服务器环境。为了达到这个目的,Node.js规定一些CommonJS模块特有的内部变量不能在ES6模块中使用。首先,this关键字。ES6模块中,最顶层的this指向undefined;CommonJS模块的顶层this指向当前模块,这是两者的主要区别。其次,ES6模块中不存在以下顶级变量。argumentsrequiremoduleexports__filename__dirname13。循环加载“循环依赖”是指脚本a的执行依赖于脚本b,脚本b的执行依赖于脚本a。通常,“循环加载”意味着存在强耦合。如果处理不当,还可能导致递归加载,使程序无法执行,所以应该避免。但实际上,这是很难避免的,尤其是对于依赖关系复杂的大型项目。很容易让a依赖b,b依赖c,c依赖a。这意味着模块加载机制必须考虑到“循环加载”的情况。对于JS语言,CommonJS和ES6这两种目前最常见的模块格式,对“循环加载”的处理方式不同,返回的结果也不同。①无论CommonJS模块被加载多少次,第一次加载时只会运行一次,以后加载会返回第一次运行的结果,除非手动清除系统缓存.CommonJS模块的一个重要特性是加载时执行,即当需要脚本代码时,会完整执行。一个模块一旦被“循环加载”,只有执行过的部分才会输出,没有执行过的部分不会输出。②ES6对“循环加载”的处理与CommonJS有着根本的不同。ES6模块是动态引用。如果使用import从模块加载变量(即importfoofrom'foo'),这些变量不会被缓存,而是成为对加载模块的引用,这需要开发人员保证。当值可用时,可以获取值。\14。加载手写node.js的require函数时,首先检查模块是否已经缓存。module._resolveFilename解析当前引用文件的绝对路径是否为内置模块。如果没有,创建一个模块。有两个模块一个属性调用id=文件名,exports={}将模块放入缓存并加载文件module.load获取文件的扩展名findLongestRegisteredExtension()根据扩展名调用相应的方法读取文件差异一加一自执行函数,将代码放入//a.js文件module.exports='hello';console.log('loadedonce');//require.js文件letfs=require('fs');letpath=require('path');letvm=require('vm');functionModule(id){this.id=id;//文件名this.exports={};//导出导出对象}模块。_resolveFilename=function(filename){//依次搜索Object.keys(Module._extensions)//默认先获取文件名filename=path.resolve(filename);//获取文件的扩展名,判断是否存在,不存在则为.js,存在则使用原名letflag=path.extname(filename);让extname=标志?标志:'.js';返回标志?文件名:(文件名+扩展名);}模块。_extensions=Object.create(null);Module.wrapper=['(function(module,exports,require,__dirname,__filename){','})']Module._extensions['.js']=function(module){//id导出//module.exports='hello'letcontent=fs.readFileSync(module.id,'utf8')letstrTemplate=Module.wrapper[0]+content+Module.wrapper[1];//console.log('111',strTemplate);//我想让这个函数执行,希望传入exportsletfn=vm.runInThisContext(strTemplate);//模块中的this是module.exports的对象fn.call(module.exports,module,module.exports,requireMe);}//json是直接把结果放在module.exports上Module._extensions['.json']=function(module){letcontent=fs.readFileSync(module.id,'utf8');module.exports=JSON.parse(content);}Module.prototype.load=function(){//获取文件扩展名letextname=path.extname(this.id);模块。_extensionsextname;}Module._cache={};//缓存对象functionrequireMe(filename){letabsPath=Module._resolveFilename(filename);//console.log(absPath);if(Module._cache[absPath]){//如果已经缓存了,直接返回exports对象returnModule._cache[absPath].exports;}letmodule=newModule(absPath);//添加缓存模块Module._cache[absPath]=module;//加载module.load();返回module.exports;//exports对象上的默认require方法将在用户分配结果时返回module.exports对象}letstr=requireMe('./a');str=requireMe('./a');安慰。日志('===',海峡);