文章目录
前言
没有模块化带来的问题
早期没有模块化带来了很多的问题:比如命名冲突的问题
当然,我们有办法可以解决上面的问题:立即函数调用表达式
但是,我们其实带来了新的问题:
- 第一,我必须记得每一个模块中返回对象的命名,才能在其他模块使用过程中正确的使用;
- 第二,代码写起来混乱不堪,每个文件中的代码都需要包裹在一个匿名函数中来编写;
- 第三,在没有合适的规范情况下,每个人、每个公司都可能会任意命名、甚至出现模块名称相同的情况;
所以,我们会发现,虽然实现了模块化,但是我们的实现过于简单,并且是没有规范的。
我们需要制定一定的规范来约束每个人都按照这个规范去编写模块化的代码;
这个规范中应该包括核心功能:模块本身可以导出暴露的属性,模块又可以导入自己需要的属性;JavaScript社区为了解决上面的问题,涌现出一系列好用的规范。
CommonJS
CommonJS模块化规范通常是在node的运行环境里面,每一个独立的JS文件就是一个模块,exports和module.exports可以负责对模块中的内容进行导出;require函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容;
至于module.exports和exports的关系,大致就是这样的关系:
module.exports = {},exports = module.exports导出的是取决于module.exports而不是exports,但是一般情况下他们都是相同的,但是如果给exports单独赋值了一个对象,会导致exports = module.exports不成立,是不会达到导出的目的的。
实际上,在一个JS文件中,通过module.exports导出一个对象时,然后其他地方通过require获取这个文件导出的对象,我们可以理解为实际上,通过module.exports指向了这个对象,并且对外暴露了这个对象,使得我们可以通过require访问到,在其他地方,我们通过require访问的时候,会根据require里面传入的文件路径,然后去寻找这个文件暴露的对象,然后指向这个对象,也就是说,module.exports和require指向的都是同一个对象,那么我们也可以在require导入的文件里面去修改这个对象,当然可以是可以,我们并不支持这样做,这样会导致在当多个文件引入同一个文件时候,出bug后,使得错误的寻找变得毫无头绪。
1、require的细节
情况一:
X是一个Node核心模块,比如path、http直接返回核心模块,并且停止查找
情况二:X是以 ./ 或 …/ 或 /(根目录)开头的
- 第一步:将X当做一个文件在对应的目录下查找;
1.如果有后缀名,按照后缀名的格式查找对应的文件
2.如果没有后缀名,会按照如下顺序: 1> 直接查找文件X 2> 查找X.js文件 3> 查找X.json文件 4> 查找X.node文件- 第二步:没有找到对应的文件,将X作为一个目录 查找目录下面的index文件 1> 查找X/index.js文件 2> 查找X/index.json文件 3> 查找X/index.node文件
- 如果没有找到,那么报错:not found
情况三:直接是一个X(没有路径),并且X不是一个核心模块
这样的话,会先在当前文件夹里面寻找node_modules,如果没有找到,会在上一层文件夹里面寻找,如果寻找到了node_modules文件夹,便会去内部寻找要引入的模块。
2、模块的加载过程
模块如果在被第一次引入时,模块中的js代码会被运行一次;
模块被多次引入时,会缓存,最终只加载(运行)一次
为什么只会加载运行一次呢? 这是因为每个模块对象module都有一个属性:loaded。 为false表示还没有加载,为true表示已经加载
如果有循环引入,那么加载顺序是什么?Node采用的是深度优先算法
如下:
顺序为main -> aaa -> ccc -> ddd -> eee ->bbb
3、CommonJS规范缺点
CommonJS加载模块是同步的:
同步的意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行;
这个在服务器不会有什么问题,因为服务器加载的js文件都是本地文件,加载速度非常快;
如果将它应用于浏览器呢?
浏览器加载js文件需要先从服务器将文件下载下来,之后再加载运行;
那么采用同步的就意味着后续的js代码都无法正常运行,即使是一些简单的DOM操作;
所以在浏览器中,我们通常不使用CommonJS规范:当然在webpack中使用CommonJS是另外一回事;因为它会将我们的代码转成浏览器可以直接执行的代码;
ESModule
1、如何使用
在使用ESModule前,我们需要先设置type=“module”
<script src="./main.js" type="module"></script>
需要注意的是,在浏览器运行ESModule模块化代码的时候,必须通过开启本地服务器的方式打开,而不能通过右键open in browser 打开。如果你通过本地加载Html 文件 (比如一个 file:// 路径的文件), 你将会遇到 CORS 错误,因为Javascript 模块安全性需要。所以需要通过一个服务器来测试,我这里使用的VSCode,VSCode中有一个插件:Live Server
ES Module和CommonJS的模块化有一些不同之处:一方面它使用了import和export关键字;另一方面它采用编译期的静态分析,并且也加入了动态引用的方式;
ES Module模块采用export和import关键字来实现模块化:
export负责将模块内的内容导出;
import负责从其他模块导入内容;
ESModule通过export关键字导出内容,export 后跟上一个{},里面放入导出的内容,要注意的是,这并不是对象,这只是一种导出的写法而已,当我们需要导入的时候,使用import…from…来实现。当我们需要将在导出或者导入时候以另外一个变量名接收时,只需要在导出的变量后面加as (新变量名),*表示全部内容,还可以使用default关键字表示默认导出。例如export default foo;
index.html中:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./main.js" type="module"></script>
</body>
</html>
main.js中:
// 1、
// import { name as name1, age } from "./info.js"
// console.log(name1, age);
// 2、
// import * as foo from "./info.js"
// console.log(foo.name, foo.age);
// 3、default
import foo from "./info.js"
console.log(foo.name, foo.age);
info.js中:
const name = "cy"
const age = 18;
const height = 1.88
// export {
// name,
// age
// }
const foo = {
name,
age
}
export {
height
}
export default foo
2、import的细节
通过import加载一个模块,是不可以在其放到逻辑代码中的,比如:
为什么会出现这个情况呢?
这是因为ES Module在被JS引擎解析时,就必须知道它的依赖关系;
由于这个时候js代码没有任何的运行,所以无法在进行类似于if判断中根据代码的执行情况;
但是某些情况下,我们确确实实希望动态的来加载某一个模块:
如果根据不同的条件,动态来选择加载模块的路径; 这个时候我们需要使用 import() 函数来动态加载
import.meta是一个给JavaScript模块暴露特定上下文的元数据属性的对象。
它包含了这个模块的信息,比如说这个模块的URL; 在ES11(ES2020)中新增的特性
// import { name, age, foo } from './foo.js'
// import函数返回的结果是一个Promise
import("./foo.js").then(res => {
console.log("res:", res.name)
})
console.log("后续的代码都是不会运行的~")
// ES11新增的特性
// meta属性本身也是一个对象: { url: "当前模块所在的路径" }
console.log(import.meta)
3、ES Module的解析流程
ES Module的解析过程可以划分为三个阶段:
阶段一:构建(Construction),根据地址查找js文件,并且下载,将其解析成模块记录(Module Record);
阶段二:实例化(Instantiation),对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址。
阶段三:运行(Evaluation),运行代码,计算值,并且将值填充到内存地址中
阶段一:构建阶段
阶段二和阶段三:实例化阶段 – 求值阶段