今天就来讲一讲,ES6 的模块化规范 ES Module。
什么是模块化?
百度百科解释道:模块化是指解决一个复杂问题时自顶向下逐层把系统划分成若干模块的过程,有多种属性,分别反映其内部特性。
我的理解:将代码按照 功能,作用,类别等,划分成一个个独立的文件,每个文件可以看做一个模块。
ES6 提供的模块化方案叫做 ES Module,简称 esm
;
早期的 Javascript 是没有模块化的概念,如果想利用 Javascript 构建一个大型项目,就会有很多问题。例如 1.命名冲突;2.变量私有;2.依赖关系的管理等问题。
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
在讲 ES Module 具体的知识点之前,必须要先讲,如何执行我们的 ES Module 的代码,方便后续我们调试。
运行代码主要两个环境:浏览器和 NodeJs,我在这里分别做一下说明。
正常的情况,我们可以在 html 页面中,直接使用 script 标签引入我们的 js 文件,如下述案例所示。
<script src="./main.js"></script>
<!-- <script type="application/javascript" src="./main.js"></script> -->
script 标签有一个 type 属性,默认情况为:“application/javascript”。所以大多数情况都简写了。但是在 ES Module 中,为了告诉浏览器我们是用的 ES Module,需要修改 type属性为 “module”。
如下所示:
<script src="./main.js" type="module">script>
演示具体代码:
错误的情况:
// main.js
/* 使用了 ES6 的命令 export */
export var a = 1
<body>
<script src="./main.js">script>
/* 不写type属性的情况,会直接报错 */
// Uncaught SyntaxError: Unexpected token 'export' (at main.js:1:1)
body>
正确的情况:
<body>
<script src="./main.js" type="module">script>
body>
或
<body>
<script type="module">
import { a } from './main.js'
console.log(a) // 1
script>
body>
本地直接打开 带有
的 html 页面,会有跨域问题。
解决方式:可以使用 (VSCode的插件:liveServer) 创建本地服务,打开此 html 页面即可。
跨域的原因:我猜测可能是因为 type=“module” 是异步加载所导致的。
NodeJS 本身的模块化标准是 CommonJs。如何让 NodeJS 支持运行 ES Module?
NodeJs的8.9之后的版本,就开始支持 ES6了 ,但是在 13.2 版本之后才开启 默认支持运行 ES Module。所以使用版本在 8.9~13.2 之间的 NodeJs ,执行 ES Module 的 js文件。需要添加配置项 --experimental-modules
,开启 默认关闭的 ES Module 支持。
.mjs
修改 js 文件后缀 由 .js
到 .mjs
。
演示:
// main.mjs
/* 后缀名设置为 mjs */
export var a = 1
// CMD 中的输入命令
node ./main.mjs
// 8.9~13.2 之间版本的 NodeJs
node --experimental-modules ./main.mjs
"type": "module"
配置项目的 package.json 中的 type 属性为 module。
演示:
// package.json
/* type 设置为 module */
"type": "module"
// CMD 中的输入命令
node ./main.js
// 8.9~13.2 之间版本的 NodeJs
node --experimental-modules ./main.js
对标 ES Module,有时候遇到
.cjs
结尾的文件,其实也是js文件,不过它是 CommonJS 规范。package.json 中的 type 设置为 commonjs,可以表示使用 CommonJS 规范。
Es Module 第一个特点:默认开启了严格模式。即便你没有在文件的开头添加 “use strict”;
既然开启了严格模式,严格模式限制有哪些?
我第一反应:this 指向不再是指向 window ,而是 undefined
主要有以下限制:
模块中的变量是私有的,外部是无法直接访问的,我们可以通过 export 主动向外输出变量,供外部模块使用。
export 英文释义:导出,输出;(我习惯理解为输出的意思)
!!!
export
这个单词不要混淆,需要和CommonJS 的exports
和module.exports
作区分说个我自己用来记忆理解:CommonJS 的输出,默认都是放在一个对象中输出, 对象存储的信息比较多,所以需要加
s
后缀。
1. 普通输出
export var tomato = 'sweet'
export var say = function () {
console.log('说话')
}
2. 批量输出
var a = 1
var b = 2
export { a, b }
// 这种方式更好,可以很容易知道输出了哪些变量。
3.输出别名
var n = 1
export { n as m }
// 实际上输出的名称为 m。但是取值是取 n 的值。
1.错误的导出
// 错误的输出
export 1
// 错误的输出
var num = 1
export num
上面的示例会报错, why ?
正确的导出,实质上是,在接口名与模块内部变量之间,建立了一一对应的关系。
我自己的理解:使用 export 命令输出,必须建立 输出的变量名 和 内部的变量之间的关系,才可以输出
2. export import不允许在块级作用域中使用
if (true) {
// 报错
export var a = 1
}
ES6 的 export import 不允许在块级作用域中使用。
说完export,说说 import;既然模块的变量可以导出,那么我们如何引入这些变量?这就需要命令: import
import 英文释义:进口,进口商品;输入,引进 (我习惯理解为输入的意思)
为了方便演示 import ,加上我本地使用 node 调试代码比较方便。我先定义一个导出的文件 a.mjs
后续没有明确说明的。默认都是导入下方的 a.mjs
,并且采取 node ./main.mjs
的方式运行示例代码。
export var tomato = 'sweet'
export var say = function () {
console.log('说话')
}
var a = 1
var b = 2
export { a, b }
1.直接引入 a.mjs 中的变量a
import { a } from './a.mjs'
console.log(a) // 1
2.引入 a.mjs 中的变量 b 但是取别名为 newB
import { b as newB } from './a.mjs'
console.log(newB) // 2
// console.log(b) // 直接报错: b is not defined
3.引入 a.mjs 所有的输出到 obj 中
import * as obj from './a.mjs'
obj.say() // 说话
console.log(a,b) // 1 2
4.执行块a.mjs 但不引入变量
import './a.mjs'
1. import 的变量是只读的
import { a } from './a.mjs'
console.log(a) // 1
a = 2 // TypeError Assignment to constant variable.
2. import 的变量实际上是对原本模块中变量的引用
// a.mjs
export var num = 1
export function add() {
num++
}
// main.mjs
import * as obj from './a.mjs'
console.log(obj.num)
// 1
obj.add()
console.log(obj.num)
// 2
/* 这里可以发现,import引入的变量其实是对原本模块变量的一个链接引用,当原模块变量值改变的时候,我们引入的变量的值也会跟着改变 */
3. 和 export 同理, import 语句不允许放在块级作用域中使用,会直接报错;
4. 由于ES Module是静态编译,所以 import会被提升到最顶部执行 ;
// a.mjs
console.log('a.mjs开始执行啦')
export var num = 1
// main.mjs
console.log('main.js开始执行了')
import * as obj from './a.mjs'
console.log(obj.num)
console.log('main.js结束执行了')
// a.mjs开始执行啦
// main.js开始执行了
// 1
// main.js结束执行了
/* 可以看到,优先执行了import * as obj from './a.mjs', 随即执行了 a.mjs*/
5. import 的执行逻辑 优先深度遍历,先子后父;
// a.mjs
console.log('a开始执行啦')
import { say } from './b.mjs'
import { edit } from './c.mjs'
export var a = 1
console.log('a结束了')
// b.mjs
console.log('b开始执行啦')
export function say() {
console.log('开始说话')
}
import { a } from './a.mjs'
console.log(a)
console.log('b结束了')
// c.mjs
console.log('c开始执行啦')
export function edit() {
console.log('开始编辑')
}
console.log('c结束了')
// b开始执行啦
// undefined
// b结束了
// c开始执行啦
// c结束了
// a开始执行啦
// a结束了
6. 多次重复执行同一句 import 语句,只会执行一次;
// a.mjs
console.log('执行文件a')
export var tomato = 'sweet'
// main.mjs
import './a.mjs'
import './a.mjs'
// 执行文件a
/* 不管我引入了多少次 a.mjs ,只会打印一次 执行文件a */
上面的实例,通过 import 和 export 就可以顺利的 输出和输入变量。
但是还有一个问题,对于使用者来说,拿到一个模块,有些时候我们并不知道,这个模块输出了什么。此时 ES6 提供了默认的导出和默认的引入来解决这种困扰。
// a.mjs
export default 1
// main.mjs
import a from './a.mjs'
console.log(a)
// 1
1. export default 和 export的区别
/* 默认导出和默认输出 */
// a.mjs
export default 1
// main.mjs
import a from './a.mjs'
console.log(a)
// 1
/* 普通的写法 */
// a.mjs
export var a = 1
// main.mjs
import { a } from './a.mjs'
console.log(a)
// 1
为什么
export default
不使用 var 声明一个变量导出呢?原因是
export default
可以看做就是输出一个叫做 default 的变量或方法,然后系统允许你为它取任意名字。下面看示例:
2. export default 的本质
2. export default 的本质
/* export default */
// a.mjs
export default 1
// main.mjs
import a from './a.mjs'
console.log(a)
// 1
/* 模拟 default*/
// a.mjs
var a = 1
export {a as default}
import a from './a.mjs'
console.log(a)
// 1
可以看到上述代码 ,使用
export {a as default}
,效果是和export default
是相同的。
3. 又想引入部分变量,又想引入默认的导出?
// a.mjs
export default function () {
console.log('你好呀')
}
export var a = 1
export var b = 2
// main.mjs
import xx, { a, b } from './a.mjs'
xx() // 你好呀
console.log(a, b) // 1 2
/* 需要注意:这里的xx可以任意替换 */
由于 ES Module 是静态编译,提前异步加载,所以不能在块级作用域中使用 import export;
但是有这么几个问题:
一方面 CommonJS 的 require 是运行时加载(ES Module 若要支持 CommonJS,这个运行时就需要支持)。
一方面,有时候确实希望根据代码逻辑,去引入文件。
ES2022 引入了 import()
,可以叫做 动态 import;
// a.mjs
export default function () {
console.log('你好呀')
}
export var a = 1
// main.js
console.log('main.js开始执行了')
setTimeout(() => {
import('./a.mjs')
.then((res) => {
console.dir(res)
})
.catch((err) => {
console.log(err)
})
}, 200)
// main.js开始执行了
// [Module: null prototype] { a: 1, default: [Function: default] }
1. import() 函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。
2. import() 函数返回值是一个 Promise 对象;
3. import() 适用场景
条件加载
if (xxx) { import('./xxx') }
- 1
- 2
- 3
路由懒加载
const UserDetails = () => import('./views/UserDetails.vue')
按需加载
xxx.onClick = function () { import('./xxx') }
- 1
- 2
- 3
加载文件的顺序前面有说到 标签,这个章节说一下
加载文件的顺序 。
1. 默认情况
默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。
2. 异步加载
<script src="xxx.js" defer>script>
<script src="xxx.js" async>script>
添加属性,defer
或者 async
,都会开启异步加载
defer
与async
的区别是:
defer
要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async
一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer
是“渲染完再执行”,async
是“下载完就执行”。
另外,如果有多个defer
脚本,会按照它们在页面出现的顺序加载,而多个async
脚本是不能保证加载顺序的。
3. 加载文件的逻辑
有上述的解释可以得到如下结论:
<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>
所以
可以理解为,异步加载
./foo.js
文件,等页面渲染完毕之后再开始执行我们的脚本。
当然也可以给标签加上
async
标志,使它一旦下载完,就中断渲染,执行这个脚本。
<script type="module" src="./foo.js" async>script>
总结一下学到的知识。
ES Module 是 ES6提供的模块化标准;
ES Module 基础知识主要包含这几点: