Javascript Module
回顾
随着我们的应用越来越大,我们想要将其拆分成多个文件,即所谓的“模块(module)”。一个模块可以包含用于特定目的的类或函数库。
很长一段时间,JavaScript 都没有语言级(language-level)的模块语法。这不是一个问题,因为最初的脚本又小又简单,所以没必要将其模块化。
但是最终脚本变得越来越复杂,因此社区发明了许多种方法来将代码组织到模块中,使用特殊的库按需加载模块。
- AMD: 最古老的模块系统之一,最初由require.js库实现。
- CommonJS: 为
Node.js
服务器创建的模块系统。 - UMD: 另外一个模块系统,建议作为通用的模块系统,它与
AMD
和CommonJS
都兼容。
现在,它们都在慢慢成为历史的一部分,但我们仍然可以在旧脚本中找到它们。语言级的模块系统在2015年的时候出现在了ES6标准中,此后逐渐发展,现在已经得到了所有主流浏览器和Node.js
的支持。
什么是模块?
一个模块(module)就是一个文件。
每个模块都可以使用指令export
和import
导入导出一些变量和函数,这样两个模块之间就可以互相调用:
export
关键字标记了可以从当前模块外部访问的变量、函数和类定义。import
关键字允许从其他模块导入变量、函数和类定义。
假设模块helloworld.js
导出如下函数:
export function helloWorld() {
alert(`Hello World!`);
}
这样在其它模块中可以如下所示将其导入:
import { helloWorld } from './helloworld.js';
helloWorld();
在浏览器中由于模块支持特殊的关键字和功能,因此我们必须通过使用<script type="module">
来声明此脚本应该被当作模块使用。
<script type="module">
import { helloWorld } from './helloworld.js';
helloWorld();
</script>
模块间的导出与导入
这是import
的几种常见用法:
import { export1 } from "module-name";
import defaultExport from "module-name";
import { export1 , export2 as alias2 , [...] } from "module-name";
import defaultExport, { export1 [ , [...] ] } from "module-name";
import
后面的from
指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js
后缀可以省略。
再看看export
的几种常见用法:
export { name1, name2, ...};
export { variable1 as name1, variable2 as name2, ...};
export function FunctionName() {...}
export class ClassName {...}
export default expression;
export default function(){}
export default function name(){}
export { name as default};
导入导出的内容可以是变量、函数,还可以是类定义。同时,导入的内容需要使用{}
包裹起来,当然export default
除外。这时import
命令后面不需要使用大括号,而且在导入时可以为该缺省导入指定任意名字。
另外,export
后面的名字和import
后面的名字严格一致。同时我们可以在导入或导出时使用的as
关键字,相当于起了个别名。
export default
可以为模块指定默认导出,这样在import
的时候就可以为这个缺省的导出名称另起别名。但是,一个模块只能有一个默认导出,也就是说一个模块只能用一次export default
。
模块级作用域
每个模块都有自己的作用域。换句话说,一个模块中的变量和函数在其他脚本中是不可见的。
模块应该export
它们想要被外部访问的内容,并import
它们所需要的内容。在设计时,对于模块我们应该尽量使用导入导出方式,而不应依赖全局变量。
如下示例展示了在浏览器中,每个<script type="module">
都存在着自己的作用域,它们之间的变量是不可互通的:
<script type="module">
// 变量仅在当前模块中可见
let name = "张三";
</script>
<script type="module">
// 抛出错误:变量未定义
alert(name);
</script>
模块代码仅在第一次导入时被解析
如果同一个模块被其它多个模块导入,那么它的代码只会执行一次,即在第一次被导入时。例如:
// mod1.js
alert("改代码仅会被执行一次!");
// mod2.js
import './mod1.js';
// mod3.js
import './mod1.js';
顶层模块代码可以用于初始化,初始化模块或者初始化整个系统,设置系统参数等等。如果我们需要多次调用某些内容,我们应该将其以函数的形式封装并导出。
模块脚本是延迟的
模块脚本会被延迟加载,也就是说,模块脚本会等到HTML
文档完全准备就绪(即使它们很小并且比HTML
加载速度更快),然后才会运行。当然,下载外部模块脚本<script type="module" src="...">
不会阻塞HTML
的处理,它们会与其他资源并行加载。这就导致一个结论,模块脚本总是会“看到”已完全加载的HTML
页面。
当然,也会有特殊情况出现,那就是在内联脚本中。下面给出两种类型脚本的示例:
<!-- 内联脚本 -->
<script type="module">
import {counter} from './helloworld.js';
helloworld();
</script>
<!-- 外部脚本 -->
<script type="module" src="./helloworld.js"></script>
对于内联模块脚本,可以通过添加async
异步特性,可以让其不会等待任何东西。它执行导入,fetch ./helloworld.js
,并在导入完成后立即执行,即使这时候HTML
文档还未全部加载完成,或者其他脚本仍在等待处理中。
<script async type="module">
import {counter} from './helloworld.js';
helloworld();
</script>