ESModule
基本概念
基本使用
一个 js 文件就可以理解为一个模块,模块之间可以相互加载,使用如下两个关键字:
export
用于从当前模块中导出变量或函数import
用于在当前模块导入其他模块的导出的内容
export function sum(a, b) {
return a + b;
}
import { sum } from "./tools.js";
const res = sum(10, 20); // 30
如果在 html 文件中,引入的脚本想要被当作模块,则需要加上 type='module'
。
<!DOCTYPE html>
<script src="./main.js" type="module"></script>
<script type="module">
import { sayHi } from "./say.js";
document.body.innerHTML = sayHi("John");
</script>
模块脚本的特点
- 与普通脚本相比,模块内部自动启动了 严格模式
- 每个模块都具有自己 独立的作用域,模块内部的变量对于其他模块来说是不可见的
- 每个模块只会在它第一次导入时执行内部所有代码,后续无论被导入多少次都不会再被执行
- 模块脚本默认使用
defer script
,即一定会等到 html 解析完毕后再执行,如果想让模块脚本在 html 解析过程中就执行,那么可以使用async script
- 对于模块脚本而言,当
src
指向相同的资源时,只会执行一次模块
important
当我们通过 script 引入一个本地的 js 文件,然后直接用浏览器运行该 html,可能会出现 CROS 错误,无法正常加载该 js 文件,原因是此时请求 js 文件的协议是 file://,因为安全原因浏览器不支持这种方式,所以建议启动一个服务(live server)来运行 html。
模块的导出
普通导出
在变量、函数、类这些数据的声明的前面加上 export
表示对其导出。
// 导出数组
export let arr = [1, 2, 3];
// 导出函数
export function foo() {}
// 导出类
export class Home {}
也可以将数据的声明和导出分离开,单独进行导出。
function sayHi(user) {
alert(`Hello, ${user}!`);
}
function sayBye(user) {
alert(`Bye, ${user}!`);
}
export { sayHi, sayBye };
导出过程中也可以通过 as
关键字修改对外暴露的变量名。
// ...省略
export { sayHi as hi, sayBye as bye };
默认导出
另外还有一种默认导出的方式,有如下两种语法:
export default xxx
export { xxx as default }
同一个模块中只可能存在一个默认导出的数据。
export let arr = [1, 2, 3];
export function foo() {}
export class Home {}
export default class {}
需要注意的是,通过 export default
形式默认导出的数据中,对于 函数、类 的标识符可以加上也可以缺省,但是对于其他数据则不需要加上声明部分,在 export default
后面直接加上数据值就可以了。
// good
export default class Home {}
// good
export default function () {}
// good
export default { name: "kll", age: 18 };
// good
export default 123;
// good
const a = 123;
export default a;
// bad
export default const a = 123;
另外,如果你想一次性批量导出模块中的内容,顺带也进行默认导出,则只使用 export { xxx as default }
。
let arr = [1, 2, 3];
function foo() {}
class Home {}
export { arr, foo, Home as default };
重新导出
这个需求在编写一个 package 时很常见,因为在我们开发的包中大量的模块内容需要导出,并且我们希望最终要暴露出来的接口全都从 index.js
中导出,这时就需要使用重新导出 export {} from
。
export { Button, ButtonGroup } from './button.jsx';
export { Form, Input, InputGroup } from './form.jsx';
export { default as User } from './user.jsx';
需要注意的是,如果重新导出的数据是一个默认导出的数据,那么前面需要加上 default as
。
模块的导入
普通导入
import { arr, foo, Home } from "./data.js";
你也可以给导入的内容通过 as
关键字进行重新命名。
import { arr as array, foo as fn, Home as home } from "./data.js";
如果嫌导入的内容过多,可以直接使用 import * as
将目标模块中通过 export
导出的所有内容存放到一个对象中。
import * as data from "./data.js";
data.arr;
data.foo;
data.Home;
默认数据导入
默认数据导入是针对默认数据导出而言的,即 export default xxx
或 export { xxx as default }
形式导出的数据。当我们导入这种数据时,不需要加上花括号。
let arr = [1, 2, 3];
function foo() {}
class Home {}
export { arr, foo, Home as default };
import Home, { arr, foo } from "./data.js";
当然如果你嫌导入内容过多,也可以使用 import * as
形式将所有内容导入到一个对象中,此时默认导入的内容在这个对象的 default
属性上。
import * as data from "./data.js";
data.arr;
data.foo;
data.default;
动态导入
前面所有的导入语法都称为 "静态" 导入,语法非常严格。
首先我们不能动态生成 import
的任何参数。
模块路径必须是原始类型的字符串,不能是函数调用等表达式,下面这种就行不通:
import ... from getModuleName(); // Error, only from "string" is allowed
其次,我们无法根据条件或者在运行时导入:
if(...) {
import ...; // Error, not allowed!
}
{
import ...; // Error, we can't put import in any block
}
原因是 JS 引擎在解析代码之前就需要清楚模块之间的依赖关系,这种带条件的模块依赖关系会导致 JS 引擎无法确定模块之间的依赖关系。
important
CJS 中的 require
可以直接进行动态引入模块,因为它的本质是一个函数,它是在 JS 运行阶段才确定模块依赖关系。
但是,我们如何才能动态地按需导入模块呢?答案是使用 import()
表达式。
使用 import()
表达式的返回值是一个 Promise 对象。
const sayHi = () => {
console.log("hi");
}
const arr = [1, 2, 3];
export {
sayHi,
arr as default
};
let flag = true;
if (flag) {
import("./modules/foo.js").then(
(res) => {
// 这里的 res 就是目标模块中所有导出的内容
const sayHi = res.sayHi;
const arr = res.default;
}
);
}
important
- 动态导入在常规脚本中工作时,它们不需要
script type="module"
- 尽管
import()
看起来像一个函数调用,但它只是一种特殊语法,只是恰好使用了括号(类似于super()
),因此,我们不能将import
复制到一个变量中,或者对其使用call/apply
,因为它不是一个函数