ES6中的模块化
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
<!-- 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
进行加载
// load module.js as a module
let worker = new Worker("module.js", { type: "module" });
通过打包工具进行打包
使用webpack
或者browserify
等打包工具进行打包后使用。
声明式语法
声明式语法分为两种类型:具名导出(每个模块有多个)和默认导出(每个模块一个)。
具名导出(named exports)
通过关键字export
前缀可以导出多个内容,并有名称来进行区分。
//------ 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)
一个模块只有一个默认导出,并且默认导出尤其容易进行导入。
//------ myFunc.js ------
export default function () { ... };
//------ main1.js ------
import myFunc from 'myFunc';
myFunc();
对于class的使用
//------ MyClass.js ------
export default class { ... };
//------ main2.js ------
import MyClass from 'MyClass';
let inst = new MyClass();
默认导出其实可以理解为特殊的具名导出
导入:
import { default as foo } from 'lib';
import foo from 'lib';
导出:
//------ 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。
使用编程式语法,可以做到:
- 对模块和脚本进行编程操作
- 配置模块的加载
<!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>
已有的实现和它之间的对比
有关模块化的进程,前端模块化详解一句讲解得比较详细了,可以参考。