2015 为原生 新增了模块体系,自其发布以来便引起了开发者们广泛的讨论和积极的实践。经过一年多的发展,原生 模块目前处于什么状态?它的未来又将如何?本文试图围绕这两个问题,对原生 模块做一个全面的介绍。

中的模块

诞生之初的 没有内建模块化支持。当然,在那个时代,对于一个用来编写表单校验和页面上浮动公告栏的语言来说,「模块化」确实显得有些大材小用。

但是随着互联网的发展,尤其是 2006 年 ajax 技术的出现和之后 Web 2.0 的兴起,越来越多的业务逻辑向前端转移,前端开发的复杂程度和代码量逐渐提升。这时,由于缺乏模块化概念, 的一些问题便凸显出来:代码难以复用、容易出现全局变量污染和命名冲突、依赖管理难以维护。开发者们使用诸如暴露全局对象、自执行函数等方法来规避这些问题,但仍无法从根本上解决问题。

2009 年,基于将 应用于服务端的尝试, 诞生了。之后 更名为 ,并逐步发展为一个完整的模块规范。

为模块的使用定义了一套 API。比如,它定义了全局函数 ,通过传入模块标识来引入其他模块,如果被引入的模块又依赖了其他模块,那么会依次加载这些模块;通过 . 向外部暴露 API,以便其他的模块引入。

由于 加载模块是同步的,即只有加载完成才能进行接下来的操作,因此当应用于浏览器端时会受到网速的限制。

AMD

之后,在 组织的讨论中,AMD( )应运而生。和前者不同的是,它使用异步方式加载模块,因此更适合被浏览器端采用。AMD 用全局函数 来定义模块,它需要三个参数:模块名称、模块的依赖数组、所有依赖都可用之后执行的回调函数(该函数按照依赖声明的顺序,接收依赖作为参数)。

UMD

如果需要同时支持 和 AMD 两种格式,那么可以使用 UMD( )。事实上,UMD 通过一系列 if/else 判断来确定当前环境支持的模块体系,因此多数情况下 UMD 格式的模块会占用更大的体积。

ES6

无论是 ,AMD 还是 UMD,它们都不是标准的 模块解决方案。换句话说,它们都没有被写进 ECMA 的规范中。直到 2015 年 6 月,TC39 委员会终于将 写进 2015 中,标志着原生模块新时代的到来。至此, 文件有了两种形式:脚本(自 诞生起我们就在使用的)和模块(即 2015 )。下面就让我们来一起探索 2015 (以下简称 ES6 )。

ES6 现状

规范方面,在 2015 年的早些时候,ES6 的语法就已经设计完毕并且蓄势待发,但是模块在语义方面的实现,比如具体怎样加载和执行等,却仍然悬而未决,因为这牵扯到大量与现有 引擎和宿主环境(浏览器和 Node.js 等)的整合工作。随着最后期限的临近,委员会不得不进行妥协,即标准只定义 的语法,而具体的实现则交由宿主环境负责。

使用 Babel 和

由于绝大多数浏览器都不支持 ES6 ,所以目前如果想使用它的语法,需要借助 Babel 和 ,即通过 Babel 将代码编译为 ES5 的语法,然后使用 打包成目标格式。一个精简后的 配置为:

module.exports = {
  entry: './main.js',
  output: {
    filename: 'bundle.js',
    path: '/'
  },
  module: {
    rules: [{test: /.js$/, use: 'babel-loader'}]
  }
}

以上配置告诉 ,项目入口为./main.js,用babel-处理所有 js 文件,然后将结果打包至.js。

如何开启 ES6

时至今日,几大主流浏览器都在积极推进支持原生 ES6 的工作,部分浏览器的技术预览版也已经初步完成了这一使命。可以通过查看目前浏览器的支持情况。

的 版本已经实现了对 ES6 的支持。想在浏览器中体验的话,需要执行以下步骤:

首先在 网站下载新版

执行安装后,在标签页打开about:,并点击I the risk!按钮

找到dom..选项,并双击将其开启

小试 ES6

既然已经在 中开启了支持,那么下面就让我们从一个例子开始,详细介绍 ES6 的特点。

一个例子

首先,新建一个 HTML 文件 index.html:

<html lang="en"><head>
  <meta charset="UTF-8">
  <title>Title</title></head><body>
  <script type="module" src="main.js"></script></body></html>

值得注意的是,在 标签上我们增加了type=""这条属性,目的是告诉浏览器我们引入的 js 文件会包含其他的模块。为何浏览器无法自行判断一个 js 文件是一般的脚本还是 ES6 模块?我们会在下一节具体说明这个问题,现在暂时放在一边。

接下来,编写以下两个 js 文件:

// main.js
import utils from "./utils.js"
console.log(utils.add(3, 4))
// utils.js
export default {
  add(a, b) {
    return a + b
  }
}

在 中打开 index.html,就能在控制台看到utils.add(3, 4)的结果被打印出来。

发生了什么

在 utils.js 中,我们使用关键字导出了模块,它具有一个名为add的方法,返回两个参数的和;在 main.js 中当前页面的脚本发生错误,我们使用关键字导入了 utils 模块,并调用其中的add方法,将结果打印出来。

相信大家对和都不陌生,因为 、 等打包工具早已支持了这种写法。但是和打包工具的处理不同的是,原生 ES6 要求在引入时提供完整路径,包括文件的扩展名。因此,在 main.js 中,如果将第一行代码改为 utils from "./utils",那么是无法在浏览器中正常运行的。基于同样的原因,如果我们需要引入 目录下的第三方包,现有打包工具支持的 from ''也是不能被 ES6 识别的,必须要写为:

import Package from './node_modules/package/dist/lib.js'

命名空间

ES6 是如何解决命名冲突的问题的?试试把上述 main.js 的内容修改为:

var x = 1
console.log(x === window.x)
console.log(this === undefined)

如果将这段代码直接复制进浏览器的控制台并运行,那么会依次打印出true和false。但是再次打开我们的 index.html,会发现控制台依次打印出了false和true,和前者完全相反。

这是因为 ES6 执行在一个独立于全局的、只属于自己的作用域中(-local scope)。由于这种机制,模块之间的命名冲突不复存在,并且同时也避免了变量污染全局作用域的问题。

严格模式强制开启

在 ES6 中,严格模式是默认开启并且无法关闭的。现在将 main.js 的内容修改为:

var x = 1
delete x

再次运行时,浏览器就会抛出一个 错误,这正是严格模式下试图删除一个变量时的浏览器行为。

异步加载

ES6 默认是异步加载的,并且在页面渲染完毕后才会执行,这等同于打开了 标签的 defer 属性。为了验证这一点,可以将 index.html 改写为:




  
  Title


  
  

然后新建两个 js 文件:

// script1.js
console.log(1)
// script2.js
console.log(2)

在浏览器中打开 index.html,能够看到控制台分别打印出了 2 和 1。

对照 HTML 规范里的这幅图我们可以看出,对于 defer 的 标签,它的加载与后续文档元素的加载会并行执行,并且它的执行要等到所有元素解析完成之后。因此在上面的例子中,.js 会在 .js 之前执行。

总结

综上所述,ES6 具有以下特点:

ES6 的解析问题

上一节中我们提到,浏览器中运行的 ES6 对应的 标签上需要增加type=""属性。为何浏览器不自行判断某个 是脚本还是模块?第一眼看上去,似乎只要在代码中找到或就能够说明是 ES6 了。但是事情并没有这么简单。

挑战

我们首先假设浏览器能够在解析时通过检测代码中是否包含或来判断 js 文件是脚本还是模块。对于一个模块来说,一种可能的情况是,整个文件只有最后一行出现了一个。由于这种情况的存在,浏览器必须解析整个文件才有可能得出最终的结论。

但是从上一节我们了解到,模块是强制运行在严格模式下的。如果浏览器在解析到最后一行时才发现或,那么就需要以严格模式将整个文件重新进行解析。这样一来,第一次非严格模式下的解析就浪费了。

除此之外,真正的问题是,一个不含有和的 js 文件也有可能是一个模块。比如下面的两段代码:

// main.js
import './onload.js'
console.log('onload.js is loaded')
// onload.js
window.addEventListener('load', _ => {
  console.log('Window is loaded')
})

虽然 .js 中没有出现和,但是 main.js 以关键字引入了它,所以它也是一个模块。只解析它本身是没有办法知道这一点的。

总而言之,虽然文件中包含和预示了该文件是模块,但是不包含这两个关键字却不能说明它不是模块。

浏览器端的解决方案

浏览器端的解决方案很简单,就是在 标签上显式地注明一个文件是模块:type=""。这样浏览器就会以模块的方式解析这个文件,并且加载它所依赖的模块。

围绕 Node.js 的讨论

对于 Node.js 而言,浏览器的解决方法显然是行不通的。Node.js 社区对此进行了激烈的讨论,目前主要有四个方案:

讨论还没有最终的结论,目前看来后两者更有希望。

最新进展与展望

ES6 进入标准以来,开发者们对它进行了充分的研究和积极的探索,以下就是两个例子。

动态加载方案()

目前 ES6 采用的是静态声明和静态解析,即在编译时就能确定模块的依赖关系,完成模块的加载。这不仅提高了加载效率,也使得 tree 成为可能( 和 2 都基于此实现了 tree )。

但是另一方面,某些时候仍然有动态加载的需求。举例来说,在一些场景下,直到运行时才能确定是否需要引入一个模块(比如根据用户的语言引入不同的模块)。为应对动态加载的需求,TC39 整理出了一套所谓「类函数」的模块加载语法提案:(),目前已经处于规范发布流程的 stage 3 阶段。一个典型的用例如下:

const main = document.querySelector('main')
main.addEventListener('click', event => {
  event.preventDefault()
  import(`./section-modules/${ main.dataset.entryModule }.js`)
    .then(module => {
      module.loadPageInto(main)
    })
    .catch(err => {
      main.textContent = err.message
    })
})

从这个例子可以看出当前页面的脚本发生错误,()允许我们动态地引入模块。此外,和 ES6 相比,它还有以下特点:

我们有理由相信,如果这个提案最终被写进标准,对 ES6 来说将是一个很好的补充。

基于 ES6 的 - 尝试

来自挪威奥斯陆的工程师 在一篇博客里结合 ES6 、HTTP/2、 和 Bloom ,进行了从服务器将未经打包的模块推送至客户端的尝试。

他首先列举了现有打包策略的弊病:要么会造成浏览器下载一些用不到的代码,要么会造成同一个模块被多次重复下载。为了达到模块加载的最优解,他进行了以下尝试:

ES6 是可以被静态解析的,这使得服务端能够找到给定模块的所有依赖模块。重复这个过程就可以构建出整个应用的依赖关系树

利用 HTTP/2 的 push,服务端可以在客户端发出请求之前主动向其推送文件。一旦客户端请求了一个模块,服务端就可以将这个模块的所有依赖连同这个模块本身一起推送给客户端。当客户端需要加载某个依赖时,就会发现这个依赖已经存在于它的缓存中

一个潜在的问题是,如果模块 A 和模块 B 都依赖了模块 C,那么当客户端请求模块 A 时,服务端会同时将模块 C 推送;之后若客户端请求模块 B,服务端由于并不知道客户端的缓存中已经存在模块 C,因此会再次推送模块 C,这样就造成了网络资源的浪费

解决方案是,客户端发送请求时在请求头带上一个 Bloom ,它携带了客户端缓存的信息,服务端接收请求后对照依赖关系树和 Bloom ,确定需要推送哪些模块。不在请求头写入完整的已缓存模块的列表的原因是,这样做会导致请求头变得很大,而一个 Bloom 通常只占用 100 字节

客户端如何知道自己缓存了哪些模块?这里需要用到 :当客户端发送一个模块请求时, 首先拦截这个请求,查看缓存中是否有这个模块,如果有就直接返回;如果没有,那么就根据缓存中的已有模块建立一个 Bloom ,并且写入请求头,将请求发送出去;当服务端返回一个模块时, 将其写入缓存并响应给客户端

整个过程的流程图如下:

从这个例子可以看出,ES6 的新特性为前端工程化打开了更多的可能性。

ES6 未来展望

截至目前,在 的各种宿主环境中,只有少数浏览器的技术预览版实现了对 ES6 的支持;即使主流浏览器都支持了,由于要考虑旧浏览器的兼容性问题,在今后的很长一段时间里,开发者们在编写代码时仍然需要像现在一样使用打包工具将模块打包成需要的格式,而不是使用真正的 ES6 。

事实上,浏览器和 Node.js 支持只是 ES6 迈向实用的第一步。除此之外, 生态链上的许多环节都需要进行相应的改变。比如,目前 npm 上的大量模块都是 格式的,它们是不能直接被 ES6 引用的。

由此可见,ES6 离我们还有一段距离。不过,我们相信它终究会到来。因为「一个烂的标准比没有标准好上千万倍」,更何况 ES6 并不是一个烂的标准。

参考文献

ES6 via Their

ES6 : More than you think

--


限时特惠:
本站持续每日更新海量各大内部创业课程,一年会员仅需要98元,全站资源免费下载
点击查看详情

站长微信:Jiucxh

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注