1.什么是前端模块化
模块化开发,一个模块就是一个实现特定功能的文件,有了模块我们就可以更方便地使用别人的代码,要用什么功能就加载什么模块。
2.模块化开发的好处
1)避免变量污染,命名冲突
2)提高代码利用率
3)提高维护性
4)依赖关系的管理
3.浏览器加载
<script src="path/to/myModule.js"></script>
默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到<script>标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。
如果脚本体积很大,下载和执行的时间就会很长,因此造成浏览器堵塞,用户会感觉到浏览器“卡死”了,没有任何响应。这显然是很不好的体验,所以浏览器允许脚本异步加载,下面就是两种异步加载的语法。
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
<script>标签打开defer或async属性,脚本就会异步加载。渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。
defer与async的区别是:defer要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer是“渲染完再执行”,async是“下载完就执行”。另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。
浏览器加载ES6模块,也使用<script>标签,但是要加入type="module"属性。
由于type属性设为module,所以浏览器知道这是一个 ES6 模块。浏览器对于带有type="module"的<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。
如果网页有多个<script type="module">,它们会按照在页面出现的顺序依次执行。
<script>标签的async属性也可以打开,这时只要加载完成,渲染引擎就会中断渲染立即执行。执行完成后,再恢复渲染。
<script type="module" src="./foo.js" async></script>
一旦使用了async属性,<script type="module">就不会按照在页面出现的顺序执行,而是只要该模块加载完成,就执行该模块。
ES6模块也允许内嵌在网页中,语法行为与加载外部脚本完全一致。
对于外部的模块脚本(上例是foo.js),有几点需要注意:
①代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
②模块脚本自动采用严格模式,不管有没有声明use strict。
③模块之中,可以使用import命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 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.js v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。
Node.js 要求 ES6 模块采用.mjs后缀文件名。也就是说,只要脚本文件里面使用import或者export命令,那么就必须采用.mjs后缀名。Node.js 遇到.mjs文件,就认为它是 ES6 模块,默认启用严格模式,不必在每个模块文件顶部指定"use strict"。
如果不希望将后缀名改成.mjs,可以在项目的package.json文件中,指定type字段为module。
一旦设置了以后,该目录里面的 JS 脚本,就被解释用 ES6 模块。
如果这时还要使用 CommonJS 模块,那么需要将 CommonJS 脚本的后缀名都改成.cjs。如果没有type字段,或者type字段为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字段,指定模块加载的入口文件。如果没有type字段,index.js就会被解释为 CommonJS 模块。然后,import命令就可以加载这个模块。
7.package.json的exports字段
exports字段的优先级高于main字段。它有多种用法:
①子目录别名:package.json文件的exports字段可以指定脚本或子目录的别名
②main 的别名: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,是一个对象,无法被静态分析,所以只能整体加载。
加载单一的输出项,可以写成下面这样:
import packageMain from 'commonjs-package';
const { method } = packageMain;
还有一种变通的加载方法,就是使用 Node.js 内置的module.createRequire()方法。
10.同时支持两种格式的模块
一个模块同时要支持 CommonJS 和 ES6 两种格式,也很容易。
如果原始模块是 ES6 格式,那么需要给出一个整体输出接口,比如export default obj,使得 CommonJS 可以用import()进行加载。
如果原始模块是 CommonJS 格式,那么可以加一个包装层。
Node.js 的内置模块
Node.js 的内置模块可以整体加载,也可以加载指定的输出项。
11.加载路径
ES6 模块的加载路径必须给出脚本的完整路径,不能省略脚本的后缀名。import命令和package.json文件的main字段如果省略脚本的后缀名,会报错。
为了与浏览器的import加载规则相同,Node.js 的.mjs文件支持 URL 路径。
目前,Node.js 的import命令只支持加载本地模块(file:协议)和data:协议,不支持加载远程模块。另外,脚本路径只支持相对路径,不支持绝对路径(即以/或//开头的路径)。
12.内部变量
ES6 模块应该是通用的,同一个模块不用修改,就可以用在浏览器环境和服务器环境。为了达到这个目标,Node.js 规定 ES6 模块之中不能使用 CommonJS 模块的特有的一些内部变量。
首先,就是this关键字。ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块,这是两者的一个重大差异。
其次,以下这些顶层变量在 ES6 模块之中都是不存在的。
arguments
require
module
exports
__filename
__dirname
13.循环加载
“循环加载”(circular dependency)指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。
通常,“循环加载”表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现。
但是实际上,这是很难避免的,尤其是依赖关系复杂的大项目,很容易出现a依赖b,b依赖c,c又依赖a这样的情况。这意味着,模块加载机制必须考虑“循环加载”的情况。
对于JS语言来说,目前最常见的两种模块格式 CommonJS 和 ES6,处理“循环加载”的方法是不一样的,返回的结果也不一样。
①CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
CommonJS 模块的重要特性是加载时执行,即脚本代码在require的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
②ES6 处理“循环加载”与 CommonJS 有本质的不同。ES6 模块是动态引用,如果使用import从一个模块加载变量(即import foo from 'foo'),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。
14.手写 node.js 的 require 函数
- 加载时 先看一下模块是否被缓存过 第一次没有缓存过
- Module._resolveFilename 解析出当前引用文件的绝对路径
- 是否是内置模块,不是就创建一个模块 模块有两个属性 一个叫 id = 文件名, exports = {}
- 将模块放到缓存中
- 加载这个文件 Module.load
- 拿到文件的扩展名 findLongestRegisteredExtension() 根据扩展名来调用对应的方法
- 会读取文件 差一个加一个自执行函数,将代码放入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | // a.js 文件 module.exports = 'hello' ; console.log( '加载了一次' ); // require.js 文件 let fs = require( 'fs' ); let path = require( 'path' ); let vm = require( 'vm' ); function Module(id) { this .id = id; // 文件名 this .exports = {}; // exports 导出对象 } Module._resolveFilename = function(filename) { // 应该去依次查找 Object.keys(Module._extensions) // 默认先获取文件的名字 filename = path.resolve(filename); // 获取文件的扩展名 并判断是否有,若没有就是.js,若有,就采用原来的名字 let flag = path.extname(filename); let extname = flag ? flag : '.js' ; return flag ? filename : (filename + extname); } Module._extensions = Object.create( null ); Module.wrapper = [ '(function(module,exports,require,__dirname,__filename){' , '})' ] Module._extensions[ '.js' ] = function(module) { // id exports // module.exports = 'hello' let content = fs.readFileSync(module.id, 'utf8' ) let strTemplate = Module.wrapper[0] + content + Module.wrapper[1]; // console.log('111', strTemplate); // 希望让这个函数执行,并且,我希望吧exports 传入进去 let fn = vm.runInThisContext(strTemplate); // 模块中的 this 就是 module.exports的对象 fn.call(module.exports, module, module.exports, requireMe); } // json 就是直接将结果放到 module.exports 上 Module._extensions[ '.json' ] = function(module) { let content = fs.readFileSync(module.id, 'utf8' ); module.exports = JSON.parse(content); } Module.prototype.load = function() { // 获取文件的扩展名 let extname = path.extname( this .id); Module._extensions[extname]( this ); } Module._cache = {}; // 缓存对象 function requireMe(filename) { let absPath = Module._resolveFilename(filename); // console.log(absPath); if (Module._cache[absPath]) { // 如果缓存过了,直接将exports 对象返回 return Module._cache[absPath].exports; } let module = new Module(absPath); // 增加缓存模块 Module._cache[absPath] = module; // 加载 module.load(); return module.exports; // 用户将结果赋予给 exports 对象上 默认 require 方法会返回 module.exports 对象 } let str = requireMe( './a' ); str = requireMe( './a' ); console.log( '===' , str); |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗
2020-03-12 在Windows中玩转Docker Toolbox(镜像加速)
2020-03-12 Docker 清理命令 删除所有的镜像和容器
2020-03-12 修改docker安装的machine位置
2020-03-12 Sass @mixin 与 @include
2020-03-12 webpack之SourceMap
2020-03-12 element el-upload自定义上传显示进度条,多文件上传进度
2019-03-12 mysql不会使用索引,导致全表扫描情况