ES6 modules 即將到來,現在該考慮新的打包方案了嘛?
前言
震驚,也就這幾年的時候,這迭代變更的時候驚嘆了。今日早讀文章由@趙飛翻譯授權分享。
正文從這開始~
近年來,構建高性能JavaScript應用是一個複雜的工程。幾年前,從為了節省HTTP開銷做代碼合并開始到壓縮混淆變數名來擠出最後一bit的代碼放在我們的工程里。
現在我們需要tree shaking我們的代碼以及打包我們的模塊,然後回過頭來,為了不阻塞主進程加快首屏載入速度做代碼拆分。我們同時也更換了所有的東西:使用上未來的一些特性?答案是肯定的,這得歸功於Babel!
ES6 modules(以下稱ESM)已經定義到了ECMAScript規範裡面有段時間了。社區的人們也寫了很多文章佈道怎麼配合Babel來使用以及說明了import和node里的require的區別。但是在瀏覽器端完整的實現還需要一點時間。
很欣喜的看到Safari第一個在其工程預覽版搭載ESM),現在Edge 和 Firefox Nightly也搭載了這個功能。在經歷過使用像RequireJs Browserify(還記得AMD和common js之爭嗎)似乎modules終將到達瀏覽器領地,那麼就讓我們來看看未來將會帶給我們什麼吧
常規的方案
通常構建web應用的方式是載入Browserify, Rollup 和 Webpack (or 或者市面上的其他工具)構建出來的bundle。一個經典網站(非SPA)就是由一個服務端渲染的HTML裡面載入了一個單文件的JS bundle。
ES6 modules tryout
上面個包含了三個js文件的bundle是有Webpack打包,這些文件都使用了ESM(ES6 modules)
// app/index.js
importdep1from ./dep-1 ;
functiongetComponent(){
varelement=document.createElement( div );
element.innerHTML=dep1();
returnelement;
}
document.body.appendChild(getComponent());
// app/dep-1.js
importdep2from ./dep-2 ;
exportdefaultfunction(){
returndep2();
}
// app/dep-2.js
exportdefaultfunction(){
return Hello World, dependencies loaded! ;
}
app運行的結果是 『Hello world』,說明所有的文件已經載入了。
裝載bundle
// webpack.config.js
constpath=require( path );
constUglifyJSPlugin=require( uglifyjs-webpack-plugin );
module.exports={
entry: ./app/index.js ,
output:{
filename: bundle.js ,
path:path.resolve(__dirname, dist )
},
plugins:[
newUglifyJSPlugin()
]
};
這三個文件實際上很小總共才347 個位元組。
$ ll app
total24
-rw-r--r--1stefanjudis staff 75B Mar1619:33dep-1.js
-rw-r--r--1stefanjudis staff 75B Mar721:56dep-2.js
-rw-r--r--1stefanjudis staff 197B Mar1619:33index.js
當我幫用Webpack跑一遍後我得到的bundle大小是865位元組,裡面大概有500位元組的母版。這些額外的位元組是可以接受的,因為和我們平時在大多數生產環境放的代碼比不算什麼。感謝Webpack,我們能提前用上 ESM。
$ webpack
Hash:4a237b1d69f142c78884
Version:webpack2.2.1
Time:114ms
Asset Size Chunks Chunk Names
bundle.js856bytes[emitted]main
[]./app/dep-1.js78bytes{}[built]
[1]./app/dep-2.js75bytes{}[built]
[2]./app/index.js202bytes{}[built]
新方案,使用原生支持的ESM
對於所有的不支持ESM的瀏覽器我們有「傳統的bundle」,但是現在我們要玩一些更酷的東西了。首先在index.html裡面添加一個新的script標籤指定其類型為ESM:type="module"
ES6 modules tryout
但我們打開Chrome的時候,我們看到好像沒有什麼事情發生。
bundle首先載入了,展示出了「Hello World」,僅僅這樣而已。但是這就是瀏覽器的高明之處,他會忽略掉他不理解的標記類型而不是拋出錯誤。Chrome也一樣他忽略了他不知道類型的script標籤。
現在,我們來看看Safari預覽版的效果:
很遺憾,沒有展示出「Hello World」。原因在於webpack打包的文件和ESM的不同:webpack能在構建時候就為我們找到文件依賴,但是ESM需要我們手動的去定義準確的文件目錄。
// app/index.js
// This needs to be changed
// import dep1 from ./dep-1 ;
// This works
importdep1from ./dep-1.js ;
調整之後工作正常了,期望的是Safari 預覽版,載入bundle和三個獨立的modules,也就是說程序會執行兩次。
解決這個問題可以使用 nomodule屬性,它可以在script標籤上控制bundle的載入。
這個屬性的細則很快也會出來,,一月底Safari預覽版已經支持了。它會告訴不支持ESM的Safari在ESM不能執行時才執行回退的bundle。
ES6 modules tryout
很好,有了這個組合我們既可以在不支持的瀏覽器里載入經典的bundle也可以在支持的瀏覽器載入ESM。
modules和script的不同
這裡有一些陷阱。首先跑在ESM下的JS和一般的script標籤裡面的些許不同。Axel Rauschmayer總結了幾點不同在他的新書 《探索ES6》裡面。我推薦大家去看看,在這裡我列舉一下主要的不同。
ESM 默認在嚴格模式下運行(無須指明『use strict』)
頂級This是undefined
頂級變數是相對於module的局部變數
ESM是在瀏覽器解析萬HTML載入並且非同步執行的。
我覺得這些特點有很大的優勢,Modules是局部的,就不需要在外麵包一層IIFE,也不用擔心全局變數污染,還能少些很多『use strict』 聲明。。
從頁面性能來看(這個可能是最重要的點)模塊默認是懶載入和懶執行。這樣當我們使用type="module"時候,就避免了意外加入一段阻塞頁面的script的可能並且也將不會有SPOF問題。當然我們也可以設置非同步屬性:async,他將替代默認的延遲屬性。但是還是得強調,延遲是一個好的選擇。
console.log( js module );
console.log( standard module );
壓縮 ES6代碼
還沒完,我們剛剛給出了一個壓縮後的bundle給Chrome,也給Safari提供了獨立的沒有壓縮的文件。但是我們怎麼讓他們體積變小呢?UglifyJS可以勝任這份工作嗎?
事實證明不行,它還不能完全處理ES6的代碼。UglifyJS有一個harmony版本,但是在發稿前我試了還不能很好的壓縮我的那仨個文件。
$ uglifyjs dep-1.js-o dep-1.min.js
Parse error at dep-1.js:3,23
exportdefaultfunction(){
^
SyntaxError:Unexpected token:punc(()
// ..
FAIL:1
但是,如今UglifyJS幾乎在每一種工具鏈裡面都能看到,怎麼就不能處理用ES6寫的代碼呢?
通常的工作流是先用Babel這樣的工具將其轉化為ES5代碼,然後再由UglifyJS來壓縮ES5的代碼。但是在這裡,我想跳過這個步驟,我們在處理未來的問題。Chrome的ES6覆蓋率達到97% Safari 預覽版已經 100%了。
後來,我在Twitter圈裡面問有沒有好的辦法來壓縮ES6代碼。Lars Graubner 給我介紹了Babili,使用它我們很容易壓縮我們的ES6 模塊。
// app/dep-2.js
exportdefaultfunction(){
return Hello World. dependencies loaded. ;
}
// dist/modules/dep-2.js
exportdefaultfunction(){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--1stefanjudis staff 856B Mar1622:32bundle.js
$ ll dist/modules
-rw-r--r--1stefanjudis staff 69B Mar1622:32dep-1.js
-rw-r--r--1stefanjudis staff 68B Mar1622:32dep-2.js
-rw-r--r--1stefanjudis staff 161B Mar1622:32index.js
可以看到,壓縮後的bundle依然有850B,但是單獨三個文件加起來才300B。這裡我忽略了GZIP壓縮,因為這對於小文件來說影響甚微。
用rel=preload? 加速ES6 modules
壓縮單文件取得成功,這是一個 298B vs. 856B的優化。但是,我們還可以做得更好,讓其速度更快。使用ESM我們可以載入更少的代碼,但是當我們打開調試面板的瀑布圖,我們可以看到文件請求是根據模塊定義的依賴鏈循序的載入。
我們可不可以添加一個標籤 用來提前告訴瀏覽器未來會發出一些額外的請求? Addy Osmani 的 Webpack preload plugin就是干這個的。那麼有沒有類似的東西應用到ESM來呢?以防你不知道rel="preload"怎麼回事,可以看看Yoav Weiss 在 Smashing Magazine的文章
很不幸的是, 預載入的功能造ESM不容易實現,因為他們不像一般的script。問題在於一個 帶有preload屬性link元素怎麼處理一個ESM?是不是要載入所有的以來文件?答案很明顯的,但是如果要加入預載入指令瀏覽器端實現也會有很多問題需要解決。
如果你對這個話題感興趣可以Domenic Denicola在github inssue關於這個問題的討論。但至少我們知道rel="preload"指令在處理一般script和ESM之間有很多不同。就在我寫這篇問章的時候一個社區提了一個規範來解決這些問題,就是用一個新的rel="modulepreload"指令。未來我們怎麼預載入,讓我們拭目以待。
引入真實的依賴
三個文件不能構成真正的App,我們加入真正的依賴。剛好,Lodash提供了他函數所有的ESM實現,我用Babili壓縮了他們。接下來我們修改index.js,讓其引入Lodash 方法。
importdep1from ./dep-1.js ;
importisEmptyfrom ./lodash/isEmpty.js ;
functiongetComponent(){
constelement=document.createElement( div );
element.innerHTML=dep1()+ +isEmpty([]);
returnelement;
}
document.body.appendChild(getComponent());
關於isEmpty使用在這裡不重要了,我們來看一下加入真實依賴發生了生什麼。
The use of isEmpty is trivial in this case, but let』s see what happens now after adding this dependency.
請求一下子增長到了超過40個,在一般的WiFi環境下頁面載入速度從100ms漲到400ms到800ms,並且所有裝載文件加起來達到了大約12KB,沒有被壓縮。很不幸Safari不支持WebPagetest來跑分。
Chrome接收到的bundle文件差不多為 8KB。
4KB的差距,足以讓我們去查一下原因在哪。
只有在大文件的壓縮效果好
如果你細心的話可以發現在Safari開發者工具的那張截圖裡面,transferred的文件大小實際上比source文件還要大。特別是在引入很多小的文件塊的大型JS App裡面這個變化更加明顯,究其原因就是因為,GZIP只有在大文件的壓縮效果好。
不久前Khan Academy 也發現了這個問題 當他在使用 HTTP/2做實驗時. 載入更小的文件的方案是為了提高緩存的命中率,但是到頭來,總是需要一個權衡並且取決於很多因素。
對於大型代碼庫,將其拆分成很多文件是有必要的,但是裝載上千個不能更好的壓縮的小文件的確不是一個好的辦法。
Tree shaking 是個好孩子
還有一個事情值得一提,那就是得益於相對新穎的Tree shakin解決方案,在構建過程中可以去除屌那些沒有使用或者被引用的代碼。第一個支持這個方案的是Rollup,現在Webpack2.0也已經支持了——只要我們在bable裡面禁止掉module選項
舉個例子,我們修改dep-2.js 加入一個在dep-1.js 沒有被調用的函數
exportdefaultfunction(){
return Hello World. dependencies loaded. ;
}
exportconstunneededStuff=[
unneeded stuff
];
對於Babili來說,它將會簡單的壓縮文件然後在Safari 裡面將接收帶很多行沒有被用到的代碼。但是一個webpack或者Rollup的bundle文件將不會包含unneededStuff.Tree shaking提供了很強大的好處,絕對應該被用在真實的生產環境中來。
未來看似光明, 但是構建過程停滯在這裡
所以,ESM將要到來,但是似乎不是所有的事情都會改變。我們為了保證壓縮效果不希望裝載數千個小文件,我們也不會拋棄這些帶有可以剔除殭屍代碼Tree shaking方案的構建方案。前端開發始終會是一個複雜的工程。
最終要記住的是權衡是成功的關鍵。不要拆分所有的事情並期望它會帶來很多改進。不要因為我們將要支持ESM就以為我們可以擺脫構建步驟和「打包策略」。在這裡我們將會堅持使用我們的構建方案,並且繼續使用「打包策略」來載入我們的文件以及我們的Javascript SDK。
到目前為止,我不得不承認前端開發依然偉大。JS在進化,我們最終將有一個方案來解決Module融入到這個語言的問題。我等不及想看到它帶給JS生態的影響,未來一兩年內會出現最佳實踐。
關於本文
譯者:@趙飛
作者:@Stefan Judis
原文:https://www.contentful.com/blog/2017/04/04/es6-modules-support-lands-in-browsers-is-it-time-to-rethink-bundling/
點擊展開全文
※第三方Javascript開發系列之前後端介面協議
※理解Node事件驅動架構
※【第957期】JavaScript 模塊現狀
※「零廣告,全乾貨」iWeb峰會上海站,最後500位參會名額限免來襲!
※代碼審查應該關注什麼:數據結構
TAG:前端早讀課 |
※Loda:為了Alliance的未來,我會考慮退出
※買不到Virgil Abloh x Air Jordan 1的你,應該考慮這雙灰藍無鞋帶版了!
※蘋果新機即將發布,正在使用iPhone6s/iPhone6sp的你是否考慮更換?
※想換iPhone XS Max,但考慮到 5G,該換嗎?
※實在買不到Virgil Abloh x Nike?這雙Air Jordan 1出了6款配色考慮一下?
※現在入手iPhone 7 Plus過時了嗎?不妨試著從這三個方面考慮
※新ipad發布,這次我終於考慮換 iPad 了!
※還有壓歲錢的你,考慮下 Vans Vault Slip-On?
※還有壓歲錢的你,考慮下 Vans Vault Slip-On ?
※Air Jordan 1看的有點疲?那這款Nike SB 「Dog Walker」可以考慮下!
※iPhone8 plus又又又降價,這次你會考慮嗎?
※官方聯名看膩了?Virgil Abloh x Air Jordan 1 雙勾版考慮一下?
※iOS 12即將推送,還在考慮您的iPhone是否升級嗎?看完你就懂了!
※發售在即!顏值堪比Supreme聯乘的Nike Air Zoom Streak新配色不考慮下?
※今天Game Royal戰績如何?不如考慮一下Virgil Abloh x Air Jordan 1黑白版?
※質感黑灰的Air Jordan 10 「Dark Shadow」即將登場,你考慮入手嗎?
※重磅聯名即將登場! Supreme x Nike Air Max Tailwind 4 你考慮入手嗎?
※彩色版iPhone 8s來了 嫌iPhone X太貴的不妨考慮一下
※Theshy與Khan几几開?李哥考慮了30秒,回答令人慌了
※Winter is Coming 不來雙禦寒靴怎麼行!Timberland x The North Face 你不先留著考慮?