【翻译】深刻理解ES6的模块

回想2007年,那时候我刚加入Mozilla's JavaScript团队,那时候的一个典型的JavaScript程序只须要一行代码,听起来像个笑话。javascript

两年后,Google Maps发布。在这以前,JavaScript主要用来作表单的验证,你用来处理<input onchange=>这个程序固然只须要一行。html

时过境迁,JavaScript项目已经发展到让人叹为观止,社区涌现了许多帮助开发的工具。可是最迫切须要的是一个模块系统,它能将你的工做分散到不一样的文件与目录中,在须要的时候他们能彼此之间相互访问,而且能够有效的加载全部代码。因此JavaScript有模块系统这很正常,并且还有多个模块系统(CommonJS、AMD、CMD、UMD)。不只如此,它还有几个包管理器(npm、bower),用来安装软件还能拷贝一些深度依赖。你可能认为ES6和它的新模块系统出现得有点晚。java

那咱们来看看ES6为现存的模块系统添加了什么,以及将来的标准和工具可否创建在这个系统上。首先,让咱们看看ES6的模块是什么样子的。webpack

模块的基础知识

ES6模块是一个包含了JS代码的文件。没有所谓的module关键词,一个模块看起来和一个脚本文件没什么不同,除了一下两个区别:git

  • ES6的模块自动开启严格模式,即便你没有写"use strict";
  • 在模块中,你可使用importexprot

先来谈谈export。在默认状况下,模块中全部的声明都是私有的,若是你但愿模块中的某些声明是公开的,并在其余模块中使用它们,你就必须导出它们。这里有一些实现方法,最简单的是添加export关键字es6

// kittydar.js - Find the locations of all the cats in an image.
// (Heather Arthur wrote this library for real)
// (but she didn't use modules, because it was 2013)

export function detectCats(canvas, options) {
  var kittydar = new Kittydar(options);
  return kittydar.detectCats(canvas);
}

export class Kittydar {
  ... several methods doing image processing ...
}

// This helper function isn't exported.
function resizeCanvas() {
  ...
}
...复制代码

你能够export任何的顶级变量:functionclassvarletconstgithub

你若是要写一个模块知道这么多就够了!你没必要再把全部的东西放到一个当即执行函数或者回调函数里面,只须要在你须要的地方进行声明。因为这个代码是一个模块,而不是一个脚本,全部的声明的做用域都只属于这个模块,而不是全部脚本和模块都能全局访问的。你只要把模块中的声明导出成一组公共模块的API就足够了。web

除了导出,模块里的代码和其余普通代码没有什么区别。它能够访问全局变量,像ObjectArray。若是你的模块在浏览器运行,还可以使用documentXMLHttpRequestnpm

在另外一个文件中,咱们能够导入并使用detectCats()函数:编程

// demo.js - Kittydar demo program

import {detectCats} from "kittydar.js";

function go() {
    var canvas = document.getElementById("catpix");
    var cats = detectCats(canvas);
    drawRectangles(canvas, cats);
}复制代码

要从一个模块导入多个变量,你能够这样写:

import {detectCats, Kittydar} from "kittydar.js";复制代码

当你运行一个包含import声明的模块,会先导入要导入的模块并加载,而后根据深度优先的原则遍历依赖图谱来执行对应模块,并跳过已经执行的模块,来避免循环。

这就是模块基础知识,这真的很简单。;-)

导出列表

你能够把你要导出的功能名写在一个列表里,而后用大括号括起来,这样就不用在每一个要导出的功能前面加上export标记。

export {detectCats, Kittydar};

// no `export` keyword required here
function detectCats(canvas, options) { ... }
class Kittydar { ... }复制代码

导出列表并不须要写在文件的第一行,它能够出如今模块文件的顶级做用域的任何位置。你能够有多个导出列表,或者将导出列表与导出声明混合使用,只要不重复导出同一个变量名就行。

重命名导出和导入

有时,导入的变量名碰巧与你须要使用的一些变量名冲突了,ES6容许你重命名导入的变量。

// suburbia.js

// Both these modules export something named `flip`.
// To import them both, we must rename at least one.
import {flip as flipOmelet} from "eggs.js";
import {flip as flipHouse} from "real-estate.js";
...复制代码

一样,你在导出变量的时候也能够重命名它们。这在你想使用不一样名字导出相同功能的时候十分方便。

// unlicensed_nuclear_accelerator.js - media streaming without drm
// (not a real library, but maybe it should be)

function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};复制代码

默认的导出

新的标准在设计上是兼容已经存在的CommonJS和AMD模块的。若是你有一个Node项目,而且你已经执行了npm install lodash。你使用ES6代码可以单独引入Lodash中的函数:

import {each, map} from "lodash";

each([3, 2, 1], x => console.log(x));复制代码

若是你已经习惯使用_.each而不是each,你依然想像之前同样使用它。或者, 你想把_当成一个函数来使用,由于这才是Lodash。

这种状况下,你只要稍微改变下你的写法:不使用花括号来导入模块。

import _ from "lodash";复制代码

这个写法等同于import {default as _} from "lodash";。全部的CommonJS 和AMD模块在ES6中都能被看成default导出,这个导出和你在CommonJS中使用require()导出获得东西同样,即exports对象。

ES6模块在设计上可让你导出更多的东西,但对于如今的CommonJS模块,导出的default模块就是能导出的所有东西了。例如,在写这篇文章时,据我所知,著名的colors模块没有特地去支持ES6语法,这是一个CommonJS模块组成的包,就像npm上的那些包同样,可是你能够直接引入到你的ES6代码中。

// ES6 equivalent of `var colors = require("colors/safe");`
import colors from "colors/safe";复制代码

若是你但愿本身ES6模块也具备默认导出,这很简单。默认的导出方式并无什么魔力;他就像其余导出同样,除了它的导出名为default。你可使用咱们以前提到的重命名语法:

let myObject = {
  field1: value1,
  field2: value2
};
export {myObject as default};复制代码

或者使用简写:

export default {
  field1: value1,
  field2: value2
};复制代码

export default关键词后面能够跟任何值:一个函数、一个类、一个对象,全部能被命名的变量。

模块对象

很差意思,这篇文章有点长。JavaScript并不孤独:由于一些缘由,全部的语言中都有模块系统,而且倾向于设计大量杂乱而又无聊的小特性。幸运的是咱们只剩下一个话题,噢,不对,是两个。

import * as cows from "cows";复制代码

当你使用import *的时候,被引入的是一个模块命名空间对象(module namespace object),它的属性是模块的输出。若是“cows”模块导出一个名为moo()的函数,那么在导入“cows”以后,你可使用cows.moo()来进行调用。

聚合模块

有时候一个包的主模块只不过是导入包其余全部的模块,并用统一的方式导出。为了简化这种代码,有一种将导入导出所有合一的简写:

// world-foods.js - good stuff from all over

// import "sri-lanka" and re-export some of its exports
export {Tea, Cinnamon} from "sri-lanka";

// import "equatorial-guinea" and re-export some of its exports
export {Coffee, Cocoa} from "equatorial-guinea";

// import "singapore" and export ALL of its exports
export * from "singapore";复制代码

这种export-from表达式相似于import-from后面跟了一个export。这和真正的导入有一些区别,它不会在当前做用域中绑定将要导出的变量。若是你打算在world-foods.js中使用Tea来编写一些代码,请不要使用这种简写,你会发现Tea为定义。

若是“singapore”导出的命名与其余导出发生了冲突,那就会出现错误,因此请谨慎使用。

呼,咱们已经把语法介绍完了!下面来谈谈一些有趣的事情。

import到底作了什么?

无论你信不信,它什么都没作。

噢,你看起来没那么好骗。那么你会相信标准几乎没有说import到底该怎么作吗?这是件好事吗?(做者貌似很爱开玩笑。)

ES6将模块的加载细节彻底交给了实现,其他的模块执行部分却规定得很是详细

简单来讲,当你告诉JS引擎运行一个模块的时候,它的行为能够概括为如下四部:

  1. 解析:读取模块的源代码,并检查语法错误。
  2. 加载:加载全部的导入模块(递归进行),这是还未标准化的部分。
  3. 连接:对于每一个新加载的模块,在实现上都会建立一个做用域,并把模块中声明的全部变量都绑定在这个做用域上,包括从其余模块导入的变量。
    若是你想试试import {cake} from "paleo",可是“paleo”模块没真正导出名为cake的变量,你会获得一个错误。这很糟糕,由于你离运行js并品尝蛋糕只有一步之遥。
  4. 运行时间:最后,开始执行加载进来的新的模块中的代码。这时,整个import过程已经完成了,因此前面说代码执行到import这一行声明时,什么都没有发生。

看到没?我说了什么都不会发生,在编程语言这件事上,我历来都不说慌。

如今咱们能够开始介绍这个系统中有趣的部分了。这有一个很是炫酷的技巧。因为系统没有指定如何加载的这方面的细节,而且你能够经过查看源代码中导入的声明,提早计算出全部的依赖项,因此ES6的实现能够经过预处理器完成全部的工做,而后把全部的模块打包到一个文件中,最后经过网络进行请求一次便可。像webpack这样的工具就是这么作的。

这是一个优雅的解决方案,由于经过网络加载全部的脚本文件很耗时,假如你请求一个资源后,发现里面有import声明,而后你又得请求更多资源。一个加载器须要很是多的网络请求来回传输。经过webpack,你不只能在今天就使用ES6的模块话,你还能得到不少好处,而且不须要担忧会形成运行时的性能降低。

本来是有计划制定一个ES6中模块加载的详细规范的,而且已经初步成型。它没有成为标准的缘由之一是不知道如何与打包这一特性进行整合。我但愿模块化的加载会更加标准化,也但愿打包工具会愈来愈好。

静态 VS 动态,或者说:规则如何打破规则

做为一个动态编译语言,使人惊奇的是JavaScript拥有一个静态的模块系统。

  • 全部的importexport只能写在顶级做用域中。你不能在条件判断语句和函数做用域内使用import
  • 全部导出的变量名必须是显式的,你不能经过遍历一个数组,动态生成一组导出名进行导出。
  • 模块对象都是被冻结的,不能经过polyfill为它添加新的特性。
  • 在全部模块运行以前, 其依赖的模块都必须通过加载、解析、连接的过程,目前没有import懒加载相关的语法。(如今import()方法已经在提案中了)
  • 对于import的错误,没法进行recovery。一个应用可能依赖许多的模块,一旦有一个模块加载失败,这个应用都不会运行。你不能在try/catch中使用import。正是由于es6的模块表现得如此静态,webpack才能在编译的时候检测出代码中的错误。
  • 你无法为一个模块在加载全部依赖项以前添加钩子,这意味着一个模块没有办法控制其依赖项的加载方式。

若是你的需求是静态的,ES6的模块系统仍是至关不错的。可是你有时候你仍是向进行一些hack,对吧?

这就是为何你使用的模块加载系统会提供一些系统层次的API来配合ES6的静态的import/export语法。例如,webpack有一个API能进行代码的分割,按照你的需求对一些模块进行懒加载。这个API可以打破以前列出的规矩。

ES6的模块语法很是静态,这很好-在使用一些编译工具时咱们都能尝到一些甜头。
静态语法的设计可让它与动态加载器丰富的API进行工做。

我何时才能使用ES6模块?

若是你今天就想使用,你须要一个预编译器,如 TraceurBabel 。这个系列以前也有相关文章,Gastón I. Silva:如何使用 Babel 和 Broccoli 编译 ES6 代码为 web 可用。Gastón也将案例放在了 GitHub 上。另外这篇文章也介绍了如何使用 Babel 和 webpack。

ES6 模块系统由 Dave Herman 和 Sam Tobin-Hochstadt进行设计,他们不顾多人(包括我)的反对,多年来始终坚持模块系统是静态的。JonCoppeard正在Firefox上实现ES6的模块化功能。JavaScript Loader的相关标准的工做也正在进行中,预计在HTML中将会被添加相似<script type=module> 这样的东西。

这即是 ES6 了。

这太有趣了,我不但愿如今就结束。也许咱们还能再说一会。咱们还可以讨论一些关于ES6规范中零零碎碎的东西,但这些又不足够写成文章。也行会有一些关于ES6将来特性的一些东西,尽请期待下周的ES6 In Depth


原文连接:ES6 In Depth: Modules

相关文章
相关标签/搜索