写在前面
这是ES6笔记的最后一篇内容,也是唯一一个将来才能使用的特性
将来是什么时候?
或许是HTTP2普及的时候。但更大的可能是将来也“不能用”(还是只能在构建工具中用,仅存在于“编译期”)
一.AMD,CMD与CommonJS
AMD/CMD,一点扩展知识如下:
CommonJS是一套理论规范(比如js的理论规范是ES),而SeaJS, RequireJS都是对CommonJS的Modules部分的具体实现
CommonJS是面向浏览器外(server端)的js制定的,所以是同步模块加载,SeaJS是CommonJS的一个实现,而RequireJS虽然也是对CommonJS的一个实现,但它是异步模块加载,算是更贴近浏览器的单线程环境
总结:CommonJS的Modules部分提出了模块化代码管理的理论,为了让js可以模块化加载,而RequireJS, SeaJS等各种实现可以称为模块化脚本加载器
CMD:Common模块定义,例如SeaJS
AMD:异步模块定义,例如RequireJS
都是用来定义代码模块的一套规范,便于模块化加载脚本,提高响应速度
CMD与AMD的区别:
CMD依赖就近。便于使用,在模块内部可以随用随取,不需要提前声明依赖项,所以性能方面存在些许降低(需要遍历整个模块寻找依赖项目)
AMD依赖前置。必须严格声明依赖项,对于逻辑内部的依赖项(软依赖),以异步加载,回调处理的方式解决
(引自JS编程常识)
如果关注过JS模块化,应该清楚这三者混乱的关系,ES6模块希望通过标准来结束这种混乱
二.ES6模块语法
1.模块作用域
module引入了模块作用域,特点如下:
目前(
2016/1/312016/10/29)没有浏览器支持ES6模块(可能这样的模块加载机制不适合浏览器环境),利用webpack等工具可以把import的所有内容整合到一个文件中ES6模块默认严格模式,无论加不加
'use strict';
支持引入/导出时重命名,
import/export {api as newApi}
,引入时重命名主要解决命名冲突,导出时重命名可以实现别名($
和jQuery
)支持默认引入/导出,能够引入CommonJS和AMD模块
只可以在模块的最外层作用域使用
import/export
,且不能再条件语句中使用
总结:推进严格模式;兼容CommonJS和AMD;只是单纯的静态模块机制,没有解决按需加载之类的问题
引入/导出时重命名,示例如下:
// 引入时重命名,解决命名冲突
import {flip as flipOmelet} from "eggs.js";
import {flip as flipHouse} from "real-estate.js";
// 导出时重命名,实现别名
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
2.import
import {api1, api2...} from 'xxx.js'
语法,特点如下:
支持api部分引入(不引入不需要的功能接口,当然,
xxx.js
是完整加载的,部分引入只是作用域控制)如果
xxx.js
还有import
语句,会深度优先加载执行已执行模块会被忽略,避免形成循环引用
支持默认引入,用来支持引入CommonJS和AMD包(
default
就是export
对象),import api from 'xxx.js'
等价于import {default as api} from 'xxx.js'
支持引入模块对象,
import * as apis from 'xxx.js'
,*
表示xxx.js
中export
的所有东西,把xxx.js
中导出的所有东西整合到apis
对象中,通过apis.xx
访问
总结:加载机制类似于CSS的@import
,处理循环依赖的方式也类似;同样兼容CommonJS和AMD
作用域层面支持部分引入,有用,但意义不大,配合构建工具编译时“剪枝”(tree shaking)更好一些
3.export
export {api1, api2...}
语法,特点如下:
不需要在首行声明,可以在模块内外层作用域任何位置
export
可以声明多个
export
,但要保证api名称无重复,名称重复可能会出错支持默认导出,
export default api
或者export {api as default}
支持聚合导出,
export {api1, api2...} from 'xxx.js'
等价于import + export
,但export from
不会在当前模块作用域引入各个api变量(引入后直接导出,无法引用)export
导出的api列表必须是字面量形式,不能遍历数组导出数组元素
总结:加载时整理export
列表,所以可以在外层任何位置export
;支持聚合,从各个第三方模块抽出一部分整合起来;静态限制,不允许动态导出
示例如下:
// 默认导出
let myObject = {
field1: value1,
field2: value2
};
export {myObject as default};
// 等价于
export default {
field1: value1,
field2: value2
};
// 聚合导出
// 导入"sri-lanka"并将它导出的内容的一部分重新导出
export {Tea, Cinnamon} from "sri-lanka";
// 导入"equatorial-guinea"并将它导出的内容的一部分重新导出
export {Coffee, Cocoa} from "equatorial-guinea";
// 导入"singapore"并将它导出的内容全部导出
export * from "singapore";
三.模块执行机制
ES6标准没有写明具体模块加载机制,交由最终实现来定,但明确规定了模块执行机制,分为4个步骤
语法解析
检查语法错误
加载
递归加载所有被import
的东西,具体怎么加载,没有写明,完全交由最终实现来定
- 连接
创建模块作用域,并把所有被import
进来的东西塞进作用域
如果import
出错,就会触发错误,具体行为未知(因为还没有浏览器已经走过了第2步)
- 运行时
执行每一个模块的所有语句,此时遇到import/export
就忽略掉,因为模块相关的处理已经结束了
静态限制
只能在模块最外层作用域使用
import/export
,不能在条件语句中使用,也不能在函数作用域用export
的标识符必须是字面量形式(要在源码中有对应的声明),不能遍历数组再导出一堆东西模块对象被冻结了,不能通过
hack
模块对象来添加polyfill
风格的新特性模块的所有依赖必须在模块代码执行前加载、解析并连接完毕,不存在一种通过
import
来按需懒加载的语法import
模块产生的错误没有对应的恢复机制。如果有一个模块无法加载或连接,所有的模块都不会执行,而且无法捕获import
错误无法在模块加载依赖前执行其它代码,这意味着无法控制模块的依赖加载过程
因为存在这些限制,所以可能在HTTP2普及后,ES6模块机制还是不能在浏览器兴起,像CSS的@import
一样,能用,但都不愿意用
四.HTTP2与模块化
在HTTP1.1的环境下,为了减少HTTP请求数量,所有模块化方案最终都依赖构建工具整合出单一文件
但HTTP2带来了一些变革,也许能够改变这种工程化流程,比如多路复用流(Multiplexed stream)和服务器端推送:
Http2连接可以承载数十或数百个流的复用,多路复用意味著来自很多流的数据包能够混合在一起通过同样连接传输,两列不同火车被混合在一起传输,当到达终点时,它们又被拆开组成两列不同的火车。
客户端请求一个资源X,服务器端判断也许客户端还需要资源Z,在无需事先询问客户端情况下将资源Z推送到客户端,客户端接受到后,可以缓存起来以备后用。
(引自Http 2.0协议简介)
多路复用流抹平了文件合并的优势,服务端推送有助于解决深度import
问题,所以ES6模块可能会在浏览器环境兴起
HTTP2对于模块化进程有重要意义,为在生产环境保持模块化提供了机会,JS、CSS甚至其它资源都可能迎来真正的模块化
P.S.关于HTTP2的更多细节,请查看https://github.com/bagder/http2-explained
五.ES6模块现状
As the various milestones of the roadmap are completed, browsers will be able to implement them. See the following trackers for the current status of the main browsers:
IE/Edge: Under Consideration
Firefox: In progress
Chrome: In progress
Webkit: Meta Bug
(引自https://github.com/whatwg/loader)
关于ES6模块加载器的更多信息请关注这个repo,ES6规范没有说明加载的具体实现,所以浏览器都卡在了加载器的实现上
参考资料
- 《ES6 in Depth》:InfoQ中文站提供的免费电子书