譯者:supotjavascript
我最近交流過的前端開發人員都喜歡使用 async/awiat
、classes
、箭頭函數這些新特性去編寫他們的JavaScript代碼。儘管全部的現代瀏覽器均可以運行ES2015+的代碼而且原生支持上面提到的新特性,可是絕大多數開發人員仍是會把他們ES2015+的代碼編譯成ES5的格式,而且提供一份polyfill文件,使得不多一部分使用舊瀏覽器的用戶可以正常訪問頁面。前端
這很糟糕。在理想的狀況中,咱們不會發送沒有必須的代碼。java
對於JavaScript和DOM新的API,咱們能夠在運行時去檢測這些API的支持度,而後按需的去引入相應的polyfill。可是使用一些新的語法,這會很是棘手。瀏覽器在遇到未知的語法時,會致使解析錯誤,後面的代碼將沒法執行。node
雖然咱們目前沒有針對特性檢測新語法的解決方案,可是如今咱們有一種方案去檢測ES2015的語法支持。webpack
解決方案就是<script type="module">
。git
大部分開發人員認爲<script type="module">
是加載ES模塊的方式(這固然是對的),可是<script type="module">
還有一個更加直接和實用的使用場景--加載ES2015+的JavaScript文件而且知道瀏覽器可以正確處理這些具備新特性的JavaScript文件。es6
換句話說,每個支持<script type="module">
的瀏覽器也將支持絕大數你熟悉並喜歡的ES2015+特性。例如:github
<script type="module">
的瀏覽器都支持async/await
<script type="module">
的瀏覽器都支持Classes
<script type="module">
的瀏覽器都支持箭頭函數
<script type="module">
的瀏覽器都支持fetch
、Promises
、Map
、Set
,以及更多的ES2015+特性接下來要作的惟一一件事就是爲不支持<script type="module">
的瀏覽器提供一個回退方案。幸運的是,若是你如今已經給你的代碼提供了ES5的版本,那你已經完成了這項工做。你如今須要作的就是爲你的代碼提供一個ES2015+的版本。web
接下來的部分將介紹如何實現這個功能,而且討論發佈ES2015+代碼將如何改變咱們編寫模塊的方式。
若是你已經在使用webpack或者rollup來打包生成你的代碼,那麼你應該繼續這麼作。
接下來,除了當前生成的build文件,你還須要生成第二份build文件,他們惟一的區別就是第二份文件不會編譯爲ES5的格式,而且再也不包含用不到的polyfill(好比Map和Set的polyfill文件)。
若是你正在使用babel-preset-env
(你應該這樣),那麼第二步也很是簡單。你所須要作的就是把目標瀏覽器列表改爲僅支持<script type="module">
的瀏覽器,Babel將不會轉義那些目標瀏覽器已經支持的語法,配合babel-preset-env的一些配置項,也能夠去掉一些已經支持的polyfill文件。
換句話說,它將輸出ES2015+的代碼而不是ES5的代碼。
舉個栗子,若是你正在使用webpack,而且入口文件是./path/to/main.mjs
,那麼當前你的ES5版本的配置項可能像這樣(注意,我將輸出文件命名爲main.es5.js,由於它是ES5版本的代碼):
module.exports = {
entry: './path/to/main.mjs',
output: {
filename: 'main.es5.js',
path: path.resolve(__dirname, 'public'),
},
module: {
rules: [{
test: /\.m?js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
['env', {
modules: false,
useBuiltIns: true,
targets: {
browsers: [
'> 1%',
'last 2 versions',
'Firefox ESR',
],
},
}],
],
},
},
}],
},
};
複製代碼
要生成一份ES2015+的代碼,你須要完成第二份配置單,將目標環境設置爲支持<script type="module">
的瀏覽器。它看起來多是下面的配置項(注意,這裏使用.mjs做爲擴展名,由於它是一個ES6的模塊):
module.exports = {
entry: './path/to/main.mjs',
output: {
filename: 'main.mjs',
path: path.resolve(__dirname, 'public'),
},
module: {
rules: [{
test: /\.m?js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
['env', {
modules: false,
useBuiltIns: true,
targets: {
browsers: [
'Chrome >= 60',
'Safari >= 10.1',
'iOS >= 10.3',
'Firefox >= 54',
'Edge >= 15',
],
},
}],
],
},
},
}],
},
};
複製代碼
構建運行後,這兩個配置將生成兩個用於生產環境的build文件
main.mjs
(ES2015+語法)main.es5.js
(ES5語法)下一步是更新你的HTML模板文件,使得在支持ES6 模塊語法的瀏覽器中能加載ES2015+的文件。你可使用<script type="module">
和<script nomodule>
的組合:
<!-- 支持ES module 的瀏覽器將加載這個文件. -->
<script type="module" src="main.mjs"></script>
<!-- 不支持ES module的瀏覽器將加載這個文件(支持ES module的瀏覽器會忽略這個文件)-->
<script nomodule src="main.es5.js"></script>
複製代碼
Note: 我已經更新了文章中的示例,將全部的模塊的拓展名都改爲了.mjs。由於這種作法比較新,因此,若是我沒有指出使用它的時候會遇到的問題,這將會是個人失職:
你的web服務器須要使用content-type: text/javascript來提供對.mjs文件的支持。若是你的現代瀏覽器沒法加載.mjs文件,可能就是這個緣由形成的。
若是你使用webpack和babel來構建你的項目,你須要對配置項作出一些修改,將正則中的/.js$/改爲 /.m?js$/
webpack比較老的版本不會爲.mjs文件生成sourcemap文件,可是已經在4.19.1的版本中修復了,請使用4.19.1以上的版本
在絕大多數的狀況下,這種方案"只是起做用",在實現落地這個方案以前,咱們須要瞭解一些如何去加載模塊(.mjs文件)的細節信息:
<script defer>
同樣,這意味着這些模塊只有在文檔解析完以後纔會執行。普通的js腳本在加載的時候會阻塞html的解析,可是加了defer之後,腳本的下載會和html解析並行執行。若是你的代碼須要在此以前運行,最好將該代碼拆分並單獨加載。var foo = 'bar';
,能夠經過window.foo
來訪問這個變量,可是在一個模塊中沒法這麼使用。請確保你的代碼中沒有依賴這種行爲。警告! 在Safari 10中不支持nomodule屬性,可是你能夠在全部<script nomodule>以前經過內聯注入這段代碼來解決這個問題。(Safari 11中已經修復這個問題)
我在github建立了一個倉庫webpack-esnext-boilerplate,開發者能夠經過這個例子來了解這個方案的具體實現。
在這個項目中,我有意包含了幾個webpack的高級功能,由於我想證實這個方案是可用於生產環境的。這些高級功能包含了以下的的一些最佳實踐:
由於我永遠不會推薦我本身沒有使用的東西,因此我已經用這個方案對這個博客網站已經了重構。若是你想了解更多信息,能夠查看源碼。
若是你使用webpack以外的打包構建工具,這個改造的過程和上面介紹的不會有很大的出入。在這個示例中,我之因此選擇使用webpack,是由於webpack是如今最流行的構建工具,並且它足夠複雜。我想若是這個方案可以和webpack一塊兒使用,那麼它能夠適用於其餘任何的構建工具。
在我看來,它絕對值得!它帶來了巨大的提高。例如,下面是這個博客網站生成的兩個版本文件大小的比較:
Version | Size (minified) | Size (minified + gzipped) |
---|---|---|
ES2015+ (main.mjs) | 80K | 21K |
ES5 (main.es5.js) | 175K | 43K |
ES5的文件大小是ES2015+版本的兩倍多(甚至是gzip壓縮後)。
更大的文件,須要更長的時間去下載,而且須要更長的時間去解析和執行。兩個版本的文件在個人博客網站中的實際效果,ES5的解析和執行時間也是ES2015+版本的兩倍:
Version | Parse/eval time (individual runs) | Parse/eval time (avg) |
---|---|---|
ES2015+ (main.mjs) | 184ms, 164ms, 166ms | 172ms |
ES5 (main.es5.js) | 389ms, 351ms, 360ms | 367ms |
雖然這些文件的大小不是很大,解析/執行的時間也不是特別長,可是這只是一個博客網站,我不須要加載大量的腳本。對於大部分的網站來講,狀況並不是如此。你用的腳本越多,你使用ES2015+所得到的收益就越大。
若是你任然持懷疑態度,而且認爲文件大小和執行時間的差別主要是由於ES5須要更多的polyfill文件而形成的,那麼你並無徹底錯誤。可是不管好壞,引入大量的polyfill文件已是今天不少網站很是廣泛的作法了。
HTTPArchive收集到的數據顯示,Alexa排名最高的網站中,有85181個網站中包含babel-polyfill、core-js或者regenerator-runtime,而在六個月以前,這個數字是34588!
現實正在轉變,包含polyfill正在迅速成爲新的常態。不幸的是,這意味着數十億的用戶經過網絡去下載數萬億個字節的代碼,而這些瀏覽器原本是能夠直接運行沒有轉義的ES2015+的代碼的。
目前這個方案的主要問題是不少npm包的做者並不發佈ES2015+的代碼,而是發佈了轉義後的ES5的代碼。
既然已經能夠部署ES2015+的代碼,那麼是時候去改變它了。
我徹底明白這對眼前的將來提出了不少挑戰。如今絕大多數的構建工具都會發布文檔,而且建議全部的模塊都是ES5的。這意味着,若是一個包的做者想npm發佈一個ES2015+的代碼,他們可能會破壞用戶的構建任務,而且一般會致使一些混淆。
問題是絕大多數的開發者在使用babel時,會經過配置忽略node_modules裏面的代碼,不對node_modules裏面的代碼進行轉義。可是若是使用ES2015+的代碼進行發佈,這就會產生問題。幸運的是,這個問題很容易修復。你只須要刪除你配置項中的node_modules:
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/, // Remove this line
use: {
loader: 'babel-loader',
options: {
presets: ['env']
}
}
}
]
複製代碼
修改之後帶來的問題是,babel將會轉義因此node_modules裏面的文件,構建速度會變慢。幸運的是,這個問題能夠經過構建工具的本地緩存解決。
不管在ES2015+做爲新的包發佈標準的路上遇到何種困難,我認爲全部的努力都是值得的。若是咱們做爲包的做者,只將ES5的版本發佈到npm上,那麼咱們會強制包的使用者去使用體積更大、執行效率更低的代碼。
經過發佈ES2015的代碼,咱們爲開發者提供了一個選項,而且最終是全部人都會從中受益。
雖然<script type="module">
是爲了在瀏覽器中加載使用模塊,可是它能作的不只僅只有這些。
<script type="module">
能夠在瀏覽器中加載JavaScript文件,這爲開發者提供了一個急需的方法,能夠在支持模塊的瀏覽器中使用一些新的特性。
經過和nomodule屬性的配合,爲咱們在生產環境中使用ES2015+代碼提供了一個方案,咱們終於能夠中止向那些不須要代碼轉義的瀏覽器發送轉換後的代碼了。
編寫ES2015+的代碼對於開發者來講是一個勝利,部署ES2015+的代碼對於用戶來講是一個勝利。