书格前端

ES6模块-团队分享


ES6模块-团队分享

大纲

前端模块化的历史

前端模块化的历史

ES6模块的使用

什么是模块?

模块是通过不同于脚本的形式进行加载的JavaScript文件。区别在于:

  1. 模块代码自动运行在严格模式,并无法退出
  2. 模块中定义的变量只在模块的作用域中存在,不会自动添加到全局的作用域
  3. 模块中的thisundefined
  4. 模块中不支持HTML风格的注释<!-- comments -->
  5. 模块必须要导出任何可以在模块外可用的一切
  6. 模块可以从其他模块导入内容

基本导出

使用export关键字可以从模块中导出变量、函数和类:

// export data
export var color = "red";
export let name = "Nicholas";
export const magicNumber = 7;

// export function
export function sum(num1, num2) {
    return num1 + num1;
}

// export class
export class Rectangle {
    constructor(length, width) {
        this.length = length;
        this.width = width;
    }
}

// this function is private to the module
function subtract(num1, num2) {
    return num1 - num2;
}

// define a function...
function multiply(num1, num2) {
    return num1 * num2;
}

// ...and then export it later
export { multiply };

导出说明:

  1. 每个导出的函数和类都有一个名字,除了使用default关键字外无法使用上面的语法导出匿名函数和类
  2. 查看multiply()函数,你可以不导出声明,支持导出引用
  3. 查看substract()函数,外部不可访问,没有显式导出的变量、函数和类对于模块是私有的

基本导入

基本格式:

import { identifier1, identifier2 } from "./example.js";

说明: 从一个模块中导入一个绑定,就类似定义一个const的内容,也就是无法定义重名变量、导入前使用和修改值。

导入一个单一绑定

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

console.log(sum(1, 2));     // 3

sum = 1;        // error

尽管example.js中定义了多个导出,你可以只导入其中一个;为了在浏览器和Node.js中保持兼容性,尽量在文件名前使用相对路径。

导入多个绑定

// import multiple
import { sum, multiply, magicNumber } from "./example.js";
console.log(sum(1, magicNumber));   // 8
console.log(multiply(1, 2));        // 2

全部导入

特殊的应用场景支持导入整个模块作为一个对象,类似导入一个库文件,所有的模块导出均作为对象属性。

// import everything
import * as example from "./example.js";
console.log(example.sum(1,
        example.magicNumber));          // 8
console.log(example.multiply(1, 2));    // 2

不管多少次使用import对一个模块进行导入操作,模块只会执行一次;在导入模块的代码执行后,实例化的模块在保存在内存中并在其他import语句使用时复用。

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

代码中example.js只会执行一次.

模块语法不支持在语句或者函数中使用,也就是说不支持动态的导入和导出。ECMAScript dynamic-import Stage3

导入绑定的修改

对导入的内容是无法修改的,但是在模块内部可以。

模块导出:

export var name = "Nicholas";
export function setName(newName) {
    name = newName;
}

模块导入:

import { name, setName } from "./example.js";

console.log(name);       // "Nicholas"
setName("Greg");
console.log(name);       // "Greg"

name = "Nicholas";       // error

setName函数回到模块内执行并修改变量name的内容,随后这个修改会反映到导入的name这个绑定。

重命名导出和导入

有时候,你可能不想使用变量、函数、类在模块中的原始名字,你可以在导入和导出的时候使用as关键字来指定新的名字

导出时重命名

function sum(num1, num2) {
    return num1 + num2;
}

export { sum as add };

导入

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

导入时重命名

import { add as sum } from "./example.js";
console.log(typeof add);            // "undefined"
console.log(sum(1, 2));             // 3

模块中的默认值

模块中的默认值是指使用default指定的一个单独变量、函数或者类,每个模块中只能设定一个默认导出,指定多个默认导出会报语法错误。

导出默认值

直接导出

export default function(num1, num2) {
    return num1 + num2;
}

函数可以不写名字,因为模块本身代表这个函数。

导出引用

function sum(num1, num2) {
    return num1 + num2;
}

export default sum;

重命名导出

function sum(num1, num2) {
    return num1 + num2;
}

export { sum as default };

导入默认值

javascript // import the default import sum from “./example.js”;

console.log(sum(1, 2)); // 3


1. 默认导入不需要大括号
2. 名称可以代表任意模块中的默认函数
3. 默认导入的语法是最精简的,ES6模块规范制定者希望默认值作为主流的使用场景

**具名和默认混合**
```javascript
export let color = "red";

export default function(num1, num2) {
    return num1 + num2;
}
import sum, { color } from "./example.js";

console.log(sum(1, 2));     // 3
console.log(color);         // "red"

默认值要在非默认值前面导入。

重命名默认值

// equivalent to previous example
import { default as sum, color } from "example";

console.log(sum(1, 2));     // 3
console.log(color);         // "red"

重新导出一个绑定

几种重新导出的方式

import { sum } from "./example.js";
export { sum }
export { sum } from "./example.js";
export { sum as add } from "./example.js";

全部导出

export * from "./example.js";

这里只能导出所有的具名导出,不包括默认导出。如果要处理默认导出,你需要显式的导入后再导出。

没有绑定的导入

有些模块并没有导出任何内容,仅仅是在全局作用域中做了些修改;虽然模块无法直接修改全局作用域的变量、函数和类,但是对于內建对象的修改可以反映到其他模块中。

// module code without exports or imports
Array.prototype.pushAll = function(items) {

    // items must be an array
    if (!Array.isArray(items)) {
        throw new TypeError("Argument must be an array.");
    }

    // use built-in push() and spread operator
    return this.push(...items);
};

这个模块对Array原型增加一个pushAll方法.

import "./example.js";

let colors = ["red", "green", "blue"];
let items = [];

items.pushAll(colors);

这段代码导入和执行模块代码,添加pushAll方法到array原型中。

没有绑定的导入通常用于创建polyfill或者shim。

加载ES6 模块

默认使用defer属性,按照代码出现的顺序进行加载,执行需要在文档解析完成后,执行顺序也是按照代码出现的顺序。

<!-- this will execute first -->
<script type="module" src="module1.js"></script>

<!-- this will execute second -->
<script type="module">
import { sum } from "./example.js";

let result = sum(1, 2);
</script>

<!-- this will execute third -->
<script type="module" src="module2.js"></script>

如果模块代码中又引入其他的模块,执行的顺序会变得复杂一些:

加载顺序:

  1. Download and parse module1.js.
  2. Recursively download and parse import resources in module1.js.
  3. Parse the inline module.
  4. Recursively download and parse import resources in the inline module.
  5. Download and parse module2.js.
  6. Recursively download and parse import resources in module2.js

执行顺序:

  1. Recursively execute import resources for module1.js.
  2. Execute module1.js.
  3. Recursively execute import resources for the inline module.
  4. Execute the inline module.
  5. Recursively execute import resources for module2.js.
  6. Execute module2.js.
// load module.js as a module
let worker = new Worker("module.js", { type: "module" });

ES6模块背后的思考

对比CommonJS和AMD

CommonJS

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

Asynchronous Module Definition(AMD)

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

ES6模块

目标:

优势:

八卦: ES6 Module的主要设计者Dave Herman和Sam Tobin Hochstadt。

组件和模块

组件化参考张云龙-前端工程

参考文章