ES6 in Depth: Modules
当我在2007年加入到Mozilla的JS团队时,有一笑话是经典的JS程序只有一行代码。
那时已经是Google Map开始的两年之后了。在此之前,最为卓越的JS代码就是表单的验证,那么你在<input onchange=>
上的平均处理代码也就是一行,对的,已经足够了。
事情已经发生了变化。JS项目已经成长为令人惊奇的量级,同时社区中也开发了一些工具来适应规模的变化。其中一个最为基本的事情是你需要模块系统,它可以将你的工作分离到多个文件和目录中,但是它依然保证在必要时能访问其他部分,同时使得加载所有代码更为有效。所以实际上,JS已经有了模块系统,并且有几种。同时,还有些包管理工具来安装软件和复制其高层的依赖。你可能会想到ES6,它为JS带来了新的模块系统,确实有点晚了些。
好的,今天我们就开始来看ES6为这已经存在的系统增加了些什么功能,同时看一下可以使用它来做些什么标准和工具吧。但是首先,让我们开始查看ES6模块长什么样子吧。
模块基础
ES6的模块是一个包含有JS代码的模块。它没有module
的关键字,一个模块几乎可以看成为一个脚本。这里有两个区别:
- ES6模块会自动启用 严格模式,即使你没有在代码中使用”use strict”
- 你可以在模块中使用
import
和export
让我们先来说一下export
。在模块中的任何声明,默认都只会作用于此模块。如果你想在模块中的声明公开出去,也就是其它模块能使用它,你必须暴露这个特性(变量、函数等)。有些方式可以做到这点,最为简单的方式就是添加一个export
的关键字。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
你可以export
任何顶级的function
、class
、var
、let
、const
。
这也就是你需要知道有关模块的所有信息。你并不需要使用IIFE或者一个回调函数。你只要正常地声明你所需要的任何事情。因此,这代码就是模块,不是脚本,所有的声明的作用域都会在此模块中,并不会在全局所有的脚本和模块是可见的。将这些声明暴露出去,会使这成为此模块对外的公开API,这就是你想要的。
与暴露代码不同,在模块中的代码仅仅只是普通的代码。你可以使用全局字面量,如Object
和Array
。如果你模块运行于浏览器中,你可以使用document
和XMLHttpRequest
。
在一个分离的文件中,我们可能导入并使用detectCats()
函数:
1 2 3 4 5 6 7 8 9 |
|
为了从一个模块中导入多个名称,你可以编写:
1
|
|
当你运行一个含有import
声明的模块时,需要导入的模块会先被加载,每个模块的代码体会根据整个依赖图进行深度遍历执行,并跳过已经执行的模块来避免循环加载。
这就是模块的基础内容了,这确实是十分简单的,;-)
导出列表
相比于在每个特性中进行导出的标记,你可以编写一个简单的列表来指出你想导出的所有名称,并放到大括号中:
1 2 3 4 5 |
|
export
的列表并不一定需要在文件的第一行,它可以出现在模块的顶级的任意地方。你可以拥有多个export
列表,或者与其它的export
混合起来用,只要保证导出的名称不超过一次就好。
对导入导出重命名
有时,一个导入的名称可能会与其它你同样需要使用的名称是一样的,所以ES6允许你对导入的名称进行重命名:
1 2 3 4 5 6 |
|
类似的,你可以对导出的名称进行重命名。有时可能会出现,你希望以不同的名称导出相同的值,如下:
1 2 3 4 5 6 7 8 9 10 11 |
|
默认导出
设计的新标准是能与已经存在的CommonJS和AMD模块能正常溶合,所以当你一个Node项目,并且完成命令npm install lodash
。你的ES6代码可以从lodash
导入单独的函数:
1 2 3 |
|
但是,你可能已经习惯看到_.each
而不是each
,并且你希望编写代码依然保持这方式。或者自从有 这个,你还想使用_
作为一个函数。
为了做到这点,你可以稍微地调整为不同的语法:导入模块时不使用大括号:
1
|
|
这其实就是等价于import {default as _} from "lodash"
。所有的CommonJS和AMD模块都会为ES6提供一个default
的导出名称,这其实就类似于你require()
这模块,也就是exports
对象。
ES6模块被设计为可以让你导出多个事物,但在已经存在的CommonJS模块中,默认的导出是包含所有事物。例如,有名的colors包在我说之前并没有任何对ES6的支持。它就是一个CommonJS的集合,就像很多在npm的包一样。但是,你可以明确地告诉ES6代码需要导入的内容。
1 2 |
|
如果你希望你的ES6模块有个默认的导出,那很容易。这并没有什么魔法性的操作,它就像其它的导出操作,只是将其命名为"default"
。你可以使用我们之前提到的重命名的语法:
1 2 3 4 5 |
|
或者使用缩写:
1 2 3 4 |
|
关键字export default
后面可以接任意的值:一个函数,一个类,一个你命名的对象字面量。
模块对象
十分抱歉,这部分内容有些长。但是,这并不仅仅是JS特有的。因为某些原因,所有语言中的模块系统都试图做到足够的小、方便。幸运,还剩一个特性。好吧,是两个。
1
|
|
当你使用import *
时,它导入的是一个模块命名对象。它的属性是这一模块的所有导出。所以,如果”cow”模块导出有一个名为moo()
的函数,那么在此行代码导入”cows”之后,你可以使用cows.moo()
。
聚合模块
有时,主模块可能会将其它模块的包导入进来,同时会将它们又以非统一的方式导出。为了简化这一类的代码,下面有个聚合的从导入到导出的缩写:
1 2 3 4 5 6 7 8 9 10 |
|
每个这些export-from
的表达式都类似于import-from
,只是调整为export
。并不像导入一样,这并不会重新添加导出的绑定到主模块的作用域中。所以,如果你想在world-foods.js
中编写代码使用Tea
,就不要使用这一缩写。你会发现找不到这字面量。
如果在导出”singapore”时会与其它的导出有冲突,会产生一个错误,所以请小心使用export *
。
好了,我们介绍完语法了。让我们开始些有趣的事情。
import
实际上做了什么
你会相信 它没什么吗?
好吧,你不是那么好欺骗的。好的,你会相信标准委员会几乎没有对import
进行说明?这是好事吗?
ES6的模块加载的整个详情可以移步到 其实现,它的实现详情可移步于 详细说明。
粗略来讲,当你告诉JS引擎在运行一个模块时,它不得不进行如下四个步骤:
- 1、解析:读取模块资源的实现代码并检查语法错误
- 2、加载:递归地加载所有需要导入的模块,这一部分还没有标准化。
- 3、链接:对于每个加载完成的模块,会为其创建一个作用域,并将此模块声明绑定到此作用域中,包括从其它模块导入过来的事物。
这就是import {cake} from "paleo"
的部分。但是,如果”paleo”模块并没有导出任何名为”cake”的字面量,你将会得到一个错误。这是很坏的体验,因为你已经实际上运行了一些JS代码了。
- 4、运行:最后,开始运行每个新加载的模块体。这时候,
import
的处理进程已经完成了,所以当代码执行到含有import
声明的代码行时,没有什么会发生。
看?我之前就告诉你答案是“没什么”。我对编程语言并不会撒谎。
但是,现在我们开始接触这模块系统的有趣部分了,这是个感觉好玩的点。因为模块系统并没有指定怎么加载模块,你可以在开始时候,找出在资源代码中所有的import
声明。ES6的其中一个实现方式,是将所有的工作都放到编译阶段,并将所有的模块捆绑放入一个文件中,才发送给网络上。webpack这工具实际上就是这么做的。
这是个大的话题,因为加载脚本会花费网络的时间。当你每次获取时,你会查找其import
的声明,时间就会成倍增长了。比较天真的加载方式是会发送多个网络请求,但通过webpack,这不仅仅是今天使用ES6模块,你会自动得到所有软件工程所要到达的运行时优点。
ES6模块加载的详情还是与原始计划的一样,并构建起来的。其中一个原因就是因为没有统一怎么实现此特性,所以它并不是最后的标准。我希望某人能够指出来,因为就如我们看到的,模块的加载确实需要标准化,而且打包非常有用,是不能放弃的。
静态 vs 动态,或者:规则或打破规则
作为动态语言,JS已经拥有其令人惊奇的静态模块系统。
所有的
import
和export
都只允许在模块的顶级声明,导入导出没有额外的限制条件,但你不能在函数作用域中使用。所有的导出定义必须是在资源代码中存在有明确的名称。你不能通过编程来循环一个数组并导出一堆的名称。
模块对象是冻结的。不能通过hack的方式来操作模块对象,polyfill方式的也不行。
在任一模块代码运行之前,所有模块依赖必须加载完成、解析和关联上。按照要求,不允许
import
导入模块懒加载的。对于
import
的错误没有任何的恢复机制。一个App可能会有上百的模块,如果有任何的加载或者关联失败,所有代码将不会运行。你不能在try/catch
中使用import
。(这里有个优点,因为这模块系统是静态的,所以webpack可以在编译阶段检查出可能存在的错误。)在没有加载完依赖之前,不允许运行模块中的任何代码。这意味着如果依赖没有加载完成时,模块本身不知道怎么控制代码的运行。
如果你需求是静态的话,这模块系统是十分好的。但是,有时你可能需要些hack,是不是?
这就是为什么你需要编程API来处理与ES6的import/export
语法相违背的模块系统加载机制。例如,webpack includes an API 你可以使用“code splitting”,按需求对某些模块进行懒加载。这个API可以让你打破上面所提到的多数规则。
ES6的模块语法是十分静态的(晕,什么叫十分静态),同时它也是好的,因为这样可以缩短其编译工具的时间。但是,通过编程后的加载API,这静态的语法已经可以动态操作了。
什么时候可以使用这ES6的模块?
现在为了使用模块,你需要一个编译器,如 Traceur 或者 Babel。早些时候,Gaston I. Silva 有一文章 来说明怎么为Web编译ES6代码。在这一文章中,Gaston 已经有一个例子是有关ES6模块的。这有个Axel Rauschmayer 编写的例子,它使用Bable和webpack。
模块系统主要由 Dave Herman 和 Sam Tobin-Hochstadt 设计,他们为此模块系统的静态化辩护,同时为此长年与包括我在内的很多人抗争着。Jon Coppeard 实现了 Firefox 中的模块。另外的JS 加载器标准化也正在进行中,人们所希望的在HTML中添加<script type=module>
的特性也会随之而来。
这就是ES6。
这些实在是太有意思了,以致我并不想结束。也许,我们只是完成了部分的故事情节。我们可以讨论些ES6说明中零碎的特性,但它们又不能足够单独写成文章。也许,将来会对这些进行讨论。请在下周加入我们,一起对深入ES6进行个完美的总结吧。