模块的概念
一个模块就是一个文件,一个脚本就是一个模块
模块可以相互加载,使用特殊的指令 export
和 import
就可以实现交换功能,从另一个模块调用一个模块的函数:
-
export
关键字标记了可以从当前模块外部访问的变量和函数。 -
import
关键字允许从其他模块导入功能。
例如有一个exprot.js
文件导出一个函数
export function sayHi(user) {
alert(`Hello, ${user}!`);
}
然后另一个文件导入这个函式
import {sayHi} from './export.js';
alert(sayHi); // function...
sayHi('John'); // Hello, John!
import
指令通过相对于当前文件的路径 ./export.js
加载模块,并将导入的函数 sayHi
分配给相应的变量。
在使用模块时,必须通过使用 <script type="module">
告诉浏览器此脚本要当作模块来使用
<script type="module">
import {sayHi} from './export.js';
document.body.innerHTML = sayHi('John');
</script>
// 页面输出 Hello, John!
模块核心功能
始终使用"use strict"
模块始终默认使用 use strict
,如果对一个未声明的变量赋值将报错
<script type="module">
a = 5; // error
</script>
模块级作用域
每个模块都有自己的作用域,一个模块中的顶级作用域变量和函数在其他脚本中不可使用
user.js
let user = "John";
hello.js
alert(user)
index.html
<script type="module" src="user.js"></script>
<script type="module" src="hello.js"></script>
上述三个代码,hello.js
尝试使用在 user.js
中声明的变量 user
,报错
如果想要访问到user.js
中的内容,需要export
想要被外部访问的内容,并 import
它们所需要的内容
user.js
export let user = "John";
hello.js
import {user} from './user.js';
document.body.innerHTML = user; // John
在浏览器中,每个模块也有自己的独立作用域
<script type="module">
// 变量仅在这个 module script 内可见
let user = "John";
</script>
<script type="module">
alert(user); // Error
</script>
模块代码仅在第一次被导入时解析
如果同一个模块被导入到多个其他位置,那么它的代码仅会在第一次导入时执行,讲导出的内容提供给所有的导入
// 📁 alert.js
alert("Hello World!");
// 在不同的文件中导入相同的模块
// 📁 1.js
import `./alert.js`; // Hello World!
// 📁 2.js
import `./alert.js`; // (什么都不显示)
如果一个模块导出一个对象
// 📁 admin.js
export let admin = {
name: "John"
};
这个模块被导入多个文件中,模块仅在第一次被导入时解析,并创建admin
对象,然后传入到所有的导入中
// 📁 1.js
import {admin} from './admin.js';
admin.name = "Pete";
// 📁 2.js
import {admin} from './admin.js';
alert(admin.name); // Pete
1.js
和 2.js
导入的是同一个对象,在 1.js
中对对象做的更改,在 2.js
中也是可见的
我们就可以在首次导入模块时对其进行设置,我们只需设置其属性,然后在进一步导入中可以直接使用
export let admin = { };
export function sayHi() {
alert(`Ready to serve, ${admin.name}!`);
}
在1.js
中设置了 admin.name
,现在每个位置都能看到它,包括在 admin.js
内部的调用
// 📁 1.js
import {admin} from './admin.js';
admin.name = "Pete";
2.js
中也可以看到 admin.name
:
// 📁 2.js
import {admin, sayHi} from './admin.js';
alert(admin.name); // Pete
sayHi(); // Ready to serve, Pete!
import.meta
import.meta
对象包含关于当前模块的信息。
<script type="module">
alert(import.meta.url); // 脚本的 URL,如果是内嵌脚本,则是当前HTML页面的URL
</script>
在模块中,"this"是 undefined
<script type="module">
alert(this); // undefined
</script>
浏览器特定功能
拥有 type="module"
标识的脚本有一些特定于浏览器的差异
脚本模块是延时的
模块脚本 总是 被延迟执行,与defer
特性的影响相同
- 下载外部模块脚本
<script type="module" src="...">
不会阻塞 HTML 的处理,它们会与其他资源并行加载。 - 模块脚本会等到 HTML 文档完全准备就绪,然后才会运行。
- 会保持脚本的相对顺序:在文档中排在前面的脚本先执行。
一个副作用是,模块脚本总是会看到已经加载完的HTML页面,包括在它下面的HTML元素
<script type="module">
alert(typeof button); // object:脚本可以发现下面的 button
// 因为模块是被延迟的 所以模块脚本会在整个页面加载完成后才运行
</script>
// 常规脚本
<script>
alert(typeof button); // Error: button is undefined,脚本看不到下面的元素
// 常规脚本会立即运行,常规脚本的运行是在在处理页面的其余部分之前进行的
</script>
<button id="button">Button</button>
不允许裸模块
在浏览器中,import
必须给出相对或绝对的 URL 路径。没有任何路径的模块被称为“裸(bare)”模块。在 import
中不允许这种模块。
import {sayHi} from 'sayHi'; // Error,裸模块
在Node.js或者打包工具中允许没有任何路径的裸模块,但是浏览器不支持
兼容性 nomodule
旧版本的浏览器不支持type="module"
,可以使用 nomodule
特性来提供一个后备:
<script nomodule>
alert(" is nomodule ");
</script>
总结
- 一个模块就是一个文件。浏览器需要使用
<script type="module">
使import/export
可以工作 - 模块和常规的脚本差别
- 默认是延迟解析的
- 重复的外部脚本会被忽略
- 模块具有自己的本地顶级作用域,并可以通过
import/export
交换功能。 - 模块始终使用
use strict
- 模块代码只执行一次。导出仅创建一次,然后会在导入之间共享