最近一段日子,编写高效的 JavaScript 应用变得愈来愈复杂。早在几年前,你们都开始合并脚原本减小 HTTP 请求数;后来有了压缩工具,人们为了压缩代码而缩短变量名,甚至连代码的最后一字节都要省出来。javascript
今天,咱们有了 tree shaking 和各类模块打包器,咱们为了避免在首屏加载时阻塞主进程又开始进行代码分割,加快交互时间。咱们还开始转译一切东西:感谢 Babel,让咱们可以在如今就使用将来的特性。html
ES6 模块由 ECMAScript 标准制定,定稿有些时日了。社区为它写了不少的文章,讲解如何经过 Babel 使用它们,以及 import
和 Node.js 的 require
的区别。可是要在浏览器中真正实现它还须要一点时间。我惊喜地发现 Safari 在它的 technology preview 版本中第一个装载了 ES6 模块,而且 Edge 和 Firefox Nightly 版本也将要支持 ES6 模块——虽然目前还不支持。在使用 RequireJS
和 Browserify
之类的工具后(还记得关于 AMD 与 CommonJS 的讨论吗?),至少看起来浏览器终于能支持模块了。让咱们来看看明朗的将来带来了怎样的礼物吧!🎉前端
构建 web 应用的经常使用方式就是使用由 Browserify、Rollup、Webpack 等工具构建的代码包(bundle)。而不使用 SPA(单页面应用)技术的网站则一般由服务端生成 HTML,在其中引入一个 JavaScript 代码包。java
<html>
<head> <title>ES6 modules tryout</title> <!-- defer to not block rendering --> <script src="dist/bundle.js" defer></script> </head>
<body>
<!-- ... --> </body>
</html>复制代码
咱们使用 Webpack 打包的代码包中包括了 3 个 JavaScript 文件,这些文件使用了 ES6 模块:react
// app/index.js
import dep1 from './dep-1';
function getComponent () {
var element = document.createElement('div');
element.innerHTML = dep1();
return element;
}
document.body.appendChild(getComponent());
// app/dep-1.js
import dep2 from './dep-2';
export default function() {
return dep2();
}
// app/dep-2.js
export default function() {
return 'Hello World, dependencies loaded!';
}复制代码
这个 app 将会显示“Hello world”。在下文中显示“Hello world”即表示脚本加载成功。android
配置使用 Webpack 建立一个代码包相对来讲比较直观。在构建过程当中,除了打包和使用 UglifyJS 压缩 JavaScript 文件以外并无作别的什么事。webpack
// webpack.config.js
const path = require('path');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
entry: './app/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new UglifyJSPlugin()
]
};复制代码
3 个基础文件比较小,加起来只有 347 字节。ios
$ ll app
total 24
-rw-r--r-- 1 stefanjudis staff 75B Mar 16 19:33 dep-1.js
-rw-r--r-- 1 stefanjudis staff 75B Mar 7 21:56 dep-2.js
-rw-r--r-- 1 stefanjudis staff 197B Mar 16 19:33 index.js复制代码
在我经过 Webpack 构建以后,我获得了一个 856 字节的代码包,大约增大了 500 字节。增长这么些字节仍是能够接受的,这个代码包与咱们日常生产环境中作代码装载没啥区别。感谢 Webpack,咱们已经可使用 ES6 模块了。git
$ webpack
Hash: 4a237b1d69f142c78884
Version: webpack 2.2.1
Time: 114ms
Asset Size Chunks Chunk Names
bundle.js 856 bytes 0 [emitted] main
[0] ./app/dep-1.js 78 bytes {0}[built]
[1] ./app/dep-2.js 75 bytes {0}[built]
[2] ./app/index.js 202 bytes {0}[built]复制代码
如今,咱们获得了一个“传统的打包代码”,如今全部还不支持 ES6 模块的浏览器都支持这种打包的代码。咱们能够开始玩一些有趣的东西了。让咱们在 index.html
中加上一个新的 script 元素指向 ES6 模块,为其加上 type="module"
。es6
<html><head><title>ES6 modules tryout</title><!-- in case ES6 modules are supported --><script src="app/index.js"type="module"></script><script src="dist/bundle.js"defer></script></head><body><!-- ... --></body></html>复制代码
而后咱们在 Chrome 中看看,发现并无发生什么事。
代码包仍是和以前同样加载,“Hello world!” 也正常显示。虽然没看到效果,可是这说明浏览器能够接受这种它们并不理解的命令而不会报错,这是极好的。Chrome 忽略了这个它没法判断类型的 script 元素。
接下来,让咱们在 Safari technology preview 中试试:
遗憾的是,它并无显示另外的“Hello world”。形成问题的缘由是构建工具与原生 ES 模块的差别:Webpack 是在构建的过程当中找到那些须要 include 的文件,而 ES 模块是在浏览器中运行的时候才去取文件的,所以咱们须要为此指定正确的文件路径:
// app/index.js
// 这样写不行
// import dep1 from './dep-1';
// 这样写能正常工做
import dep1 from './dep-1.js';复制代码
改了文件路径以后它能正常工做了,但事实上 Safari Preview 加载了代码包,以及三个独立的模块,这意味着咱们的代码被执行了两次。
这个问题的解决方案就是加上 nomodule
属性,咱们能够在加载代码包的 script 元素里加上这个属性。这个属性是最近才加入标准中的,Safari Preview 也是在一月底才支持它的。这个属性会告诉 Safari,这个 script 是当不支持 ES6 模块时的“退路”。在这个例子中,浏览器支持 ES6 模块所以加上这个属性的 script 元素中的代码将不会执行。
<html>
<head> <title>ES6 modules tryout</title> <!-- in case ES6 modules are supported --> <script src="app/index.js" type="module"></script> <!-- in case ES6 modules aren't supported --> <script src="dist/bundle.js" defer nomodule></script> </head>
<body>
<!-- ... --> </body>
</html>复制代码
如今好了。经过结合使用 type="module"
与 nomodule
,咱们如今能够在不支持 ES6 模块的浏览器中加载传统的代码包,在支持 ES6 模块的浏览器中加载 JavaScript 模块。
你能够在 es-module-on.stefans-playground.rocks 查看这个尚在制定的规范。
这儿有几个问题。首先,JavaScript 在 ES6 模块中运行与日常在 script 元素中不一样。Axel Rauschmayer 在他的探索 ES6一书中很好地讨论了这个问题。我推荐你点击上面的连接阅读这本书,可是在此我先快速地总结一下主要的不一样点:
use strict
了)。this
指向 undefined
(而不是 window)。我认为,这些特性是巨大进步。模块是局部的——这意味着咱们再也不须要处处使用 IIFE 了,并且咱们不用再担忧全局变量泄露。并且默认在严格模式下运行,意味着咱们能够在不少地方抛弃 use strict
声明。
译注:IIFE 全称 immediately-invoked function expression,即当即执行函数,也就是你们熟知的在函数后面加括号。
从改善性能的观点来看(多是最重要的进步),模块默认会延迟加载与执行。所以咱们将再也不会不当心给咱们的网站加上了阻碍加载的代码,使用 type="module"
的 script 元素也再也不会有 SPOF 问题。咱们也能够给它加上一个 async
属性,它将会覆盖默认的延迟加载行为。不过使用 defer
在如今也是一个不错的选择。
译注:SPOF 全称 Single Points Of Failure——单点故障
<!-- not blocking with defer default behavior -->
<script src="app/index.js" type="module"></script>
<!-- executed after HTML is parsed -->
<script type="module"> console.log('js module'); </script>
<!-- executed immediately -->
<script> console.log('standard module'); </script>复制代码
若是你想详细了解这方面内容,能够阅读 script 元素说明,这篇文章简单易读,而且包含了一些示例。
还没完!咱们如今能为 Chrome 提供压缩过的代码包,可是还不能为 Safari Preview 提供单独压缩过的文件。咱们如何让这些文件变得更小呢?UglifyJS 能完成这项任务吗?
然而必须指出,UglifyJS 并不能彻底处理好 ES6 代码。虽然它有个 harmony
开发版分支(地址)支持ES6,但不幸的是在我写这 3 个 JavaScript 文件的时候它并不能正常工做。
$ uglifyjs dep-1.js -o dep-1.min.js
Parse error at dep-1.js:3,23
export default function() {
^
SyntaxError: Unexpected token: punc (()
// ..
FAIL: 1复制代码
可是如今 UglifyJS 几乎存在于全部工具链中,那所有使用 ES6 编写的工程应该怎么办呢?
一般的流程是使用 Babel 之类的工具将代码转换为 ES5,而后使用 Uglify 对 ES5 代码进行压缩处理。可是在这篇文章里我不想使用 ES5 翻译工具,由于咱们如今是要寻找面向将来的处理方式!Chrome 已经覆盖了 97% ES6 规范 ,而 Safari Preview 版自 verion 10 以后已经 100% 很好地支持 ES6了。
我在推特中提问是否有可以处理 ES6 的压缩工具,Lars Graubner 告诉我可使用 Babili。使用 Babili,咱们可以轻松地对 ES6 模块进行压缩。
// app/dep-2.js
export default function() {
return 'Hello World. dependencies loaded.';
}
// dist/modules/dep-2.js
export default function(){return 'Hello World. dependencies loaded.'}复制代码
使用 Babili CLI 工具,能够轻松地分别压缩各个文件。
$ babili app -d dist/modules
app/dep-1.js -> dist/modules/dep-1.js
app/dep-2.js -> dist/modules/dep-2.js
app/index.js -> dist/modules/index.js复制代码
最终结果:
$ ll dist
-rw-r--r-- 1 stefanjudis staff 856B Mar 16 22:32 bundle.js
$ ll dist/modules
-rw-r--r-- 1 stefanjudis staff 69B Mar 16 22:32 dep-1.js
-rw-r--r-- 1 stefanjudis staff 68B Mar 16 22:32 dep-2.js
-rw-r--r-- 1 stefanjudis staff 161B Mar 16 22:32 index.js复制代码
代码包仍然是大约 850B,全部文件加起来大约是 300B。我没有使用 GZIP,由于它并不能很好地处理小文件。(咱们稍后会提到这个)
对单个 JS 文件进行压缩取得了很好的效果。文件大小从 856B 下降到了 298B,可是咱们还能进一步地加快加载速度。经过使用 ES6 模块,咱们能够装载更少的代码,可是看看瀑布图你会发现,request 会按照模块的依赖链一个一个连续地加载。
那若是咱们像以前在浏览器中对代码进行预加载那样,用 <link rel="preload" as="script">
元素告知浏览器要加载额外的 request,是否会加快模块的加载速度呢?在 Webpack 中,咱们已经有了相似的工具,好比 Addy Osmani 的 Webpack 预加载插件能够对分割的代码进行预加载,那 ES6 模块有没有相似的方法呢?若是你还不清楚 rel="preload"
是如何运做的,你能够先阅读 Yoav Weiss 在 Smashing Magazine 发表的相关文章:点击阅读
可是,ES6 模块的预加载并非那么简单,他们与普通的脚本有很大的不一样。那么问题来了,对一个 link 元素加上 rel="preload"
将会怎样处理 ES6 模块呢?它也会取出全部的依赖文件吗?这个问题显而易见(能够),可是使用 preload
命令加载模块,须要解决更多浏览器的内部实现问题。Domenic Denicola 在一个 GitHub issue 中讨论了这方面的问题,若是你感兴趣的话能够点进去看一看。可是事实证实,使用 rel="preload"
加载脚本与加载 ES6 模块是大相径庭的。可能之后最终的解决方案是用另外一个 rel="modulepreload"
命令来专门加载模块。在本文写做时,这个 pull request 还在审核中,你能够点进去看看将来咱们可能会怎样进行模块的预加载。
仅仅 3 个文件固然无法作一个真正的 app,因此让咱们给它加一些真实的依赖。Lodash 根据 ES6 模块对它的功能进行了分割,并分别提供给用户。我取出其中一个功能,而后使用 Babili 进行压缩。如今让咱们对 index.js
文件进行修改,引入这个 Lodash 的方法。
import dep1 from './dep-1.js';
import isEmpty from './lodash/isEmpty.js';
function getComponent() {
const element = document.createElement('div');
element.innerHTML = dep1() + ' ' + isEmpty([]);
return element;
}
document.body.appendChild(getComponent());复制代码
在这个例子中,isEmpty
基本上没有被使用,可是在加上它的依赖后,咱们能够看看发生了什么:
能够看到 request 数量增长到了 40 个以上,页面在普通 wifi 下的加载时间从大约 100 毫秒上升到了 400 到 800 毫秒,加载的数据总大小在没有压缩的状况下增长到了大约 12KB。惋惜的是 WebPagetest 在 Safari Preview 中不可用,咱们无法给它作可靠的标准检测。
可是,Chrome 收到打包后的 JavaScript 数据比较小,只有大约 8KB。
这 4KB 的差距是不能忽视的。你能够在 lodash-module-on.stefans-playground.rocks 找到本示例。
若是你仔细看上面 Safari 开发者工具的截图,你可能会注意到传输后的文件大小其实比源码还要大。在很大的 JavaScript app 中这个现象会更加明显,一堆的小 Chunk 会形成文件大小的很大不一样,由于 GZIP 并不能很好地压缩小文件。
Khan Academy 在前一段时间探究了一样的问题,他是用 HTTP/2 进行研究的。装载更小的文件可以很好地确保缓存命中率,但到最后它通常都会做为一个权衡方案,并且它的效果会被不少因素影响。对于一个很大的代码库来讲,分解成若干个 chunk(一个 vendor 文件和一个 app bundle)是理所固然的,可是要装载数千个不能被压缩的小文件可能并非一种明智的方法。
必需要说:感谢很是新潮的 tree shaking 技术,经过它,构建进程能够将没有使用过以及没有被其它模块引用的代码删除。第一个支持这个技术的构建工具是 Rollup,如今 Webpack 2 也支持它——只要咱们在 babel 中禁用 module
选项。
咱们试着改一改 dep-2.js
,让它包含一些不会在 dep-1.js
中使用的东西。
export default function() {
return 'Hello World. dependencies loaded.';
}
export const unneededStuff = [
'unneeded stuff'
];复制代码
Babili 只会压缩文件, Safari Preview 在这种状况下会接收到这几行没有用过的代码。而另外一方面,Webpack 或者 Rollup 打的包将不会包含这个 unnededStuff
。Tree shaking 省略了大量代码,它毫无疑问应当被用在真实的产品代码库中。
ES6 模块即将到来,可是直到它最终在各大主流浏览器中实现前,咱们的开发并不会发生什么变化。咱们既不会装载一堆小文件来确保压缩率,也不会为了使用 tree shaking 和死码删除来抛弃构建过程。前端开发如今及未来都会一如既往地复杂。
不要把全部东西都进行分割而后就假设它会改善性能。咱们即将迎来 ES6 模块的浏览器原生支持,可是这不意味着咱们能够抛弃构建过程与合适的打包策略。在咱们 Contentful 这儿,将继续坚持咱们的构建过程,以及继续使用咱们的 JavaScript SDKs 进行打包。
然而,咱们必须认可如今前端的开发体验仍然良好。JavaScript 仍在进步,最终咱们将可以使用语言自己提供的模块系统。在几年后,原生模块对 JavaScript 生态的影响以及最佳实践方法将会是怎样的呢?让咱们拭目以待。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、React、前端、后端、产品、设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划。