# 工程化 ES6 Module

ES6 最大的一个改进就是引入了模块规范。这个规范全方位简化了之前出现的模块加载器,原生浏览器支持意味着其他预处理器都不在必要。从很多方面来看,ES6 模块系统是集 AMD 和 CommonJS 之大成者。

# 模块标签

ES6 模块是作为一整块 JavaScript 代码而存在的。 带有 type="module" 属性的 <script> 标签会告诉浏览器应该将相关代码作为模块执行,而不是作为传统的脚本执行。

<script type="module"></script>
<script type="module" src="path/to/myModule.js"></script>

# 模块加载

ESMAScript 6 模块的独特之处在于,既可以通过浏览器原生加载,也可以与第三方加载器和构建工具一起加载。

ESM 的加载过程和 AMD 风格的模块加载非常相似。模块文件按需加载,且后续模块的请求会因为每个依赖模块的网络延迟而同步延迟。

# 模块行为

ES6 模块借用了 CommonJS 和 AMD 的很多优秀特性。下面列举一些。

  • 模块代码只在加载后执行
  • 模块代码只能加载一次
  • 模块是单例
  • 模块可以定义公共接口
  • 模块可以请求加载其他模块
  • 支持循环依赖

ES6 模块也增加了一些新行为。

  • ES6 模块默认在严格模式下执行
  • ES6 模块不共享命名空间
  • ES6 模块顶级 this 的值是 undefined 而常规脚本中是 window
  • ES6 模块中的 var 声明不会添加到 window 对象中
  • ES6 模块是异步加载和执行的

# 模块导出

ES 模块的公共导出系统和 CommonJS 很相似,控制模块那些部分对外可见使用的是export关键字。

export关键字用于声明一个命名导出。导出语句必需在模块的顶级(作用域的顶级)

// YES
export const a = "a";
// NOT
if (condition) {
  export const b = "b";
}

导出值对模块内部 JavaScript 的执行没有直接影响,因此 export 语句与导出值的相对位置或者 export 关键字在模块中出现的顺序没有限制。

// YES
const foo = "foo";
export { foo };
// YES
export const foo = "foo";
// NOT GOOD
export { foo };
const foo = "foo";

导出时也可以提供别名,别名必须在 export 子句的大括号语法中指定。

// moduleFoo
const foo = "foo";
export { foo as myFoo };
import { myFoo } from "./moduleFoo";

默认导出( default export)就好像模块与被导出的值是一回事。默认导出使用 default 关键字将一个值声明为默认导出,每个模块只能有一个默认导出。

// moduleFoo
const foo = "foo";
export default foo;

外部模块可以导入这个模块,而这个模块本身就是 foo 的值

import foo from "./moduleFoo";

另外, ES6 模块系统会识别作为别名提供的 default 关键字

const foo = "foo";
// 等同于 export default foo;
export { foo as default };

WARNING

一般来说,声明、赋值和导出标识符最好分开。这样就不容易搞错了,同时也可以让 export 语句集中在一块。

# 模块导入

模块可以通过使用 import 关键字使用其他模块导出的值。与 export 类似, import 必须出现在模块的顶级。

// YES
import path from "path";
// NOT Support
if (condition) {
  import
}

import 语句会被提升到模块顶部(代码的顶部)。因此,与 export 关键字类似, import 语句与使用导入值的语句 的相对位置并不重要。不过,还是推荐把导入语句放在模块顶部。

// GOOD
import path from "path";
const resolve = (dir) => path.resolve(__dirname, dir);
// BAD
console.log(path);
import path from "path";

# 动态导入

前面章节中介绍的导出和导入语句称为“静态”导入。语法非常简单且严格。

首先,我们不能动态生成 import 的任何参数。

模块路径必须是原始类型字符串,不能是函数调用,下面这样的 import 行不通:

import ... from getModuleName(); // Error, only from "string" is allowed

import(module) 表达式加载模块并返回一个 promise,该 promise resolve 为一个包含其所有导出的模块对象。我们可以在代码中的任意位置调用这个表达式。

我们可以在代码中的任意位置动态地使用它。例如:

// say.js
export function hi() {
  alert(`Hello`);
}

export function bye() {
  alert(`Bye`);
}
export default function () {
  alert("Module loaded (export default)!");
}
let { hi, bye } = await import("./say.js");

hi();
bye();

WARNING

尽管 import() 看起来像一个函数调用,但它只是一种特殊语法,只是恰好使用了括号(类似于 super())。

因此,我们不能将 import 复制到一个变量中,或者对其使用 call/apply。因为它不是一个函数。

# 模块转移导出

模块导入的值可以直接通过管道转移到导出。此时,也可以将默认导出转换为命名导出,或者相反。

e.g. 如果想把一个模块的所有命名导出集中在一块,可以像下面这样在 bar.js 中使用*导出:

// 一次性导入foo.js中的命名导出 如果 foo.js 有默认导出,则该语法会忽略它。
export * from "./foo.js";
// 将foo.js中的命名导出作为Foo的属性
export * as Foo from "./foo.js";

# 工作者模块

ECMAScript 6 模块与 Worker 实例完全兼容。在实例化时,可以给工作者传入一个指向模块文件的路径,与传入常规脚本文件一样。 Worker 构造函数接收第二个参数,用于说明传入的是模块文件。

// 第二个参数默认为{ type: 'classic' }
const scriptWorker = new Worker("scriptWorker.js");
const moduleWorker = new Worker("moduleWorker.js", { type: "module" });

又一个语法糖

在基于模块的工作者内部, self.importScripts()方法通常用于在基于脚本的工作者中加载外部脚本,调用它会抛出错误。这是因为模块的 import 行为包含了 importScripts()。

# 小结

模块模式是管理复杂性的永恒工具。开发者可以通过它创建逻辑彼此独立的代码段,在这些代码段之间声明依赖,并将它们连接在一起。此外,这种模式也是经证明能够优雅扩展到任意复杂度且跨平台的方案。