最近一段日子,編寫高效的 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、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃。