ES6中的模块化

这里主要是针对ES6 Module技术出现的讨论,对比历史的一些模块化解决方案。

背景、初衷、目标

背景

简单的讲,随着前端应用的日益庞大和复杂,对于代码的拆分复用要求更高,也就出现了对代码模块化的需求。

在ES6 Module出现前,社区中出现的两个方案CommonJS和AMD:

CommonJS

在Node.js中的主要实现方式,特性如下:

  • 紧凑的语法
  • 同步加载
  • 主要使用: 服务端

Asynchronous Module Definition(AMD)

主要的实现是RequireJS,特性如下:

  • 稍微复杂的语法
  • 异步加载
  • 主要使用: 浏览器

这里提一句,ES6 Module的主要设计者Dave Herman 和 Sam Tobin Hochstadt。

初衷

这里讲的ES6 Module的设计初衷,在ES6 Module出现前社区已经在模块化上有一些令人印象深刻的变通方案,以CommonJS和AMD为代表,这两者各有优缺点并且不兼容,因此ES6 Module的设计初衷是为了吸取这两者的优点实现ECMAScript的标准。

目标

ES6 Module的目标是出一个让CommonJS和AMD社区都能接受的方案:

  • 偏爱默认导出
  • 静态模块结构
  • 支持同步和异步加载
  • 支持模块间的循环依赖

优势和劣势(trade-off)

ES6 Module实际达成的方案特性如下:

  • 比CommonJS更紧凑的语法
  • 结构可以被静态分析(静态检测、优化等)
  • 对循环引用比CommonJS更好
  • 支持异步加载

适用的场景(业务场景、技术场景)

适用于浏览器端和服务端(Nodejs),目前(2019年)主流的浏览器大部分都不支持ES6 Module的特性,因此使用的话还得使用Babel来转译。

组成部分和关键点

ES6 Module分两部分:

  • 声明式语法(import和export)
  • 编程式加载API: 支持配置如何加载模块以及条件加载

使用场景

在浏览器侧的使用

使用<script>进行加载

通过type属性值为module来标识模块进行加载,支持外部文件和内联的方式,默认使用defer的行为,也可以指定async

1
2
3
4
5
6
7
8
9
10
11
<!-- load a module JavaScript file -->
<script type="module" src="module.js"></script>

<!-- include a module inline -->
<script type="module">

import { sum } from "./example.js";

let result = sum(1, 2);

</script>

通过Worker进行加载

1
2
// load module.js as a module
let worker = new Worker("module.js", { type: "module" });

通过打包工具进行打包

使用webpack或者browserify等打包工具进行打包后使用。

声明式语法

声明式语法分为两种类型:具名导出(每个模块有多个)和默认导出(每个模块一个)。

具名导出(named exports)

通过关键字export前缀可以导出多个内容,并有名称来进行区分。

1
2
3
4
5
6
7
8
9
10
11
12
13
//------ lib.js ------
export const sqrt = Math.sqrt;
export function square(x) {
return x * x;
}
export function diag(x, y) {
return sqrt(square(x) + square(y));
}

//------ main.js ------
import { square, diag } from 'lib';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5

默认导出(default exports)

一个模块只有一个默认导出,并且默认导出尤其容易进行导入。

1
2
3
4
5
6
//------ myFunc.js ------
export default function () { ... };

//------ main1.js ------
import myFunc from 'myFunc';
myFunc();

对于class的使用

1
2
3
4
5
6
//------ MyClass.js ------
export default class { ... };

//------ main2.js ------
import MyClass from 'MyClass';
let inst = new MyClass();

默认导出其实可以理解为特殊的具名导出

导入:

1
2
import { default as foo } from 'lib';
import foo from 'lib';

导出:

1
2
3
4
5
6
//------ module1.js ------
export default 123;

//------ module2.js ------
const D = 123;
export { D as default };

答疑

  • 为什么我们需要具名导出(named exports)?

The answer is that you can’t enforce a static structure via objects and lose all of the associated advantages (described in the next section).

如果通过对象进行导出会丢失静态结构,从而失去原先的优势。

  • ES6 Module输出的是值的引用,并且是在编译时输出接口。

编程式语法

针对ES6 Module的编程式实现,目前规范处于stage 3。

使用编程式语法,可以做到:

  • 对模块和脚本进行编程操作
  • 配置模块的加载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html>
<nav>
<a href="books.html" data-entry-module="books">Books</a>
<a href="movies.html" data-entry-module="movies">Movies</a>
<a href="video-games.html" data-entry-module="video-games">Video Games</a>
</nav>

<main>Content will load here!</main>

<script>
const main = document.querySelector("main");
for (const link of document.querySelectorAll("nav > a")) {
link.addEventListener("click", e => {
e.preventDefault();

import(`./section-modules/${link.dataset.entryModule}.js`)
.then(module => {
module.loadPageInto(main);
})
.catch(err => {
main.textContent = err.message;
});
});
}
</script>

已有的实现和它之间的对比

有关模块化的进程,前端模块化详解一句讲解得比较详细了,可以参考。

参考文档

您的支持将鼓励我继续创作!
0%