Skip to main content

ESModule

基本概念

基本使用

一个 js 文件就可以理解为一个模块,模块之间可以相互加载,使用如下两个关键字:

  • export 用于从当前模块中导出变量或函数
  • import 用于在当前模块导入其他模块的导出的内容
tools.js
export function sum(a, b) {
return a + b;
}
main.js
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 表示对其导出。

data.js
// 导出数组
export let arr = [1, 2, 3];

// 导出函数
export function foo() {}

// 导出类
export class Home {}

也可以将数据的声明和导出分离开,单独进行导出。

say.js
function sayHi(user) {
alert(`Hello, ${user}!`);
}

function sayBye(user) {
alert(`Bye, ${user}!`);
}

export { sayHi, sayBye };

导出过程中也可以通过 as 关键字修改对外暴露的变量名。

say.js
// ...省略

export { sayHi as hi, sayBye as bye };

默认导出

另外还有一种默认导出的方式,有如下两种语法:

  • export default xxx
  • export { xxx as default }

同一个模块中只可能存在一个默认导出的数据。

data.js
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

"index.js
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 xxxexport { xxx as default } 形式导出的数据。当我们导入这种数据时,不需要加上花括号。

data.js
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 对象。

modules/foo.js
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,因为它不是一个函数