在本程式碼研究室中,使用者可以提高這個簡易應用程式的效能,讓使用者為隨機貓評分。瞭解如何盡量減少傳輸的程式碼量,藉此最佳化 JavaScript 套件。
在範例應用程式中,您可以選取特定字詞或表情符號,藉此傳達每隻貓的喜好程度。您點選按鈕後,應用程式會在目前的貓咪圖片下方顯示按鈕的值。
測量
建議您一開始先檢查網站,再加入任何最佳化作業:
- 如要預覽網站,請按下「View App」,然後按下「Fullscreen」。
- 按下 Control+Shift+J 鍵 (或在 Mac 上為 Command+Option+J 鍵) 開啟開發人員工具。
- 按一下 [網路] 分頁標籤。
- 勾選「停用快取」核取方塊。
- 重新載入應用程式。
此應用程式已使用超過 80 KB!現在,要確認是否未使用套裝組合的某些部分:
按下
Control+Shift+P
(在 Mac 上則為Command+Shift+P
) 開啟「Command」選單。輸入
Show Coverage
並點選Enter
,即可顯示「涵蓋範圍」分頁。在「Coverage」分頁中,按一下「Reload」,在擷取涵蓋率時重新載入應用程式。
請查看已使用的程式碼數量與主要套件載入的資料量:
超過一半的套裝組合 (44 KB) 甚至未使用。這是因為其中有許多程式碼是由 polyfill 組成,可確保應用程式在舊版瀏覽器中運作。
使用 @babel/preset-env
JavaScript 語言的語法符合稱為 ECMAScript (或稱為 ECMA-262) 的標準。每年都會發布較新版本的規格,內含已通過提案程序的新功能。每個主要瀏覽器都在支援這些功能的不同階段。
應用程式使用下列 ES2015 功能:
我們也會使用下列 ES2017 功能:
您可以在 src/index.js
中深入瞭解原始碼,瞭解上述所有功能的使用方式。
最新版本的 Chrome 支援這些功能,但其他瀏覽器是否支援這些功能?應用程式中包含的 Babel 是最常用的程式庫,可用於將包含較新語法的程式碼編譯成舊版瀏覽器和環境能理解的程式碼。執行此操作的方式有兩種:
- 內含 Polyfill 可用來模擬較新的 ES2015+ 函式,因此就算瀏覽器不支援,還是可以使用相關的 API。以下是
Array.includes
方法的 polyfill 範例。 - 外掛程式可用來將 ES2015 程式碼 (或之後版本) 轉換為舊版 ES5 語法。由於這些是語法相關變更 (例如箭頭函式),因此無法使用 polyfill 模擬。
查看 package.json
以瞭解其中包含哪些 Babel 程式庫:
"dependencies": {
"@babel/polyfill": "^7.0.0"
},
"devDependencies": {
//...
"babel-loader": "^8.0.2",
"@babel/core": "^7.1.0",
"@babel/preset-env": "^7.1.0",
//...
}
@babel/core
是核心 Babel 編譯器。如此一來,所有 Babel 設定都會在專案根目錄的.babelrc
中定義。babel-loader
包含 webpack 建構程序中的 Babel。
現在,請查看 webpack.config.js
,瞭解 babel-loader
如何納入做為規則:
module: { rules: [ //... { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" } ] },
@babel/polyfill
會為任何較新的 ECMAScript 功能提供所有必要的 polyfill,以便應用程式在不支援這些功能的環境中運作。資料已匯入至src/index.js.
的最頂端
import "./style.css";
import "@babel/polyfill";
@babel/preset-env
可識別在選擇做為目標的任何瀏覽器或環境上需要哪些轉換和 polyfill。
請查看 Babel 設定檔 .babelrc
,瞭解其如何加入:
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions"
}
]
]
}
這是 Babel 與 webpack 的設定。如果您使用與 webpack 不同的模組整合工具,請參閱這篇文章,瞭解如何在應用程式中加入 Babel。
.babelrc
中的 targets
屬性可識別指定的瀏覽器。@babel/preset-env
與瀏覽器清單整合,這表示您可以在瀏覽器清單說明文件中找到可用於這個欄位的相容查詢完整清單。
"last 2 versions"
值會針對每個瀏覽器的最後兩個版本,反對應用程式中的程式碼。
偵錯
如要全面瞭解瀏覽器的所有 Babel 目標,以及所有包含的轉換和 polyfill,請在 .babelrc:
中新增 debug
欄位
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true
}
]
]
}
- 按一下「工具」。
- 點選「記錄」。
重新載入應用程式,然後查看編輯器底部的 Glitch 狀態記錄檔。
指定瀏覽器
Babel 將多項有關編譯程序的詳細資料記錄到主控台,包括已編譯程式碼的所有目標環境。
請注意,停用的瀏覽器 (例如 Internet Explorer) 如何納入這份清單。這會造成問題,因為不支援的瀏覽器將不會加入新功能,且 Babel 會持續為其轉譯特定的語法。如果使用者未使用這個瀏覽器存取您的網站,組合大小就不會增加。
Babel 也會記錄使用的轉換外掛程式清單:
清單很長!以下是 Babel 將所有 ES2015+ 語法轉換為舊語法時必須使用的所有外掛程式,適用於所有目標瀏覽器。
不過,Babel 不會顯示任何使用的特定 polyfill:
這是因為要直接匯入整個 @babel/polyfill
。
個別載入 polyfill
根據預設,Babel 包含將 @babel/polyfill
匯入檔案時,完整 ES2015+ 環境所需的每個 polyfill。如要匯入目標瀏覽器所需的特定 polyfill,請在設定中加入 useBuiltIns: 'entry'
。
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true
"useBuiltIns": "entry"
}
]
]
}
重新載入應用程式。您現在可以看到所有指定的 polyfill:
雖然現在只加入 "last 2 versions"
所需的 polyfill,但它仍是一個非常長的清單!這是因為目標瀏覽器所需的「所有」新功能都仍需使用 polyfill。請將屬性值變更為 usage
,只加入程式碼中用到的功能所需的值。
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"debug": true,
"useBuiltIns": "entry"
"useBuiltIns": "usage"
}
]
]
}
因此,系統會在需要時自動加入 polyfill。
這表示您可以在 src/index.js.
中移除 @babel/polyfill
匯入
import "./style.css";
import "@babel/polyfill";
現在只包含應用程式所需的 polyfill。
應用程式套件的大小已大幅縮減。
縮小支援的瀏覽器清單
其中所含的瀏覽器目標數量仍然很大,也沒有太多使用者使用停用的瀏覽器 (例如 Internet Explorer)。請將設定更新為以下內容:
{
"presets": [
[
"@babel/preset-env",
{
"targets": "last 2 versions",
"targets": [">0.25%", "not ie 11"],
"debug": true,
"useBuiltIns": "usage",
}
]
]
}
請查看擷取的套件詳細資料。
由於應用程式相當小,因此這些變更實際上沒有太大差異。不過,我們建議您採用瀏覽器市佔率百分比 (例如 ">0.25%"
),並排除您確信使用者不會使用的特定瀏覽器。請參閱 James Kyle 撰寫的「最近 2 個版本」一文,進一步瞭解相關資訊。
使用 <script type="module">
仍然還有進步空間。雖然已移除許多未使用的 polyfill,但其中有許多瀏覽器不需要運送。只要使用模組,就能直接編寫較新的語法並運送至瀏覽器,不需使用任何不必要的 polyfill。
JavaScript 模組是所有主要瀏覽器支援的相對新功能。您可以使用 type="module"
屬性建立模組,藉此定義從其他模組匯入及匯出的指令碼。例如:
// math.mjs
export const add = (x, y) => x + y;
<!-- index.html -->
<script type="module">
import { add } from './math.mjs';
add(5, 2); // 7
</script>
支援 JavaScript 模組的環境已支援許多新版 ECMAScript 功能,無須使用 Babel。也就是說,您可以修改 Babel 設定,將兩個不同版本的應用程式傳送至瀏覽器:
- 這個版本必須在支援模組的新版瀏覽器中運作,且內含的模組大多未轉譯,但檔案大小較小
- 包含較大且轉譯後指令碼的版本,可在所有舊版瀏覽器中使用
搭配 Babel 使用 ES 模組
如要為兩個應用程式版本使用不同的 @babel/preset-env
設定,請移除 .babelrc
檔案。如要將 Babel 設定新增至 Web Pack 設定,可以為每個應用程式版本指定兩種不同的編譯格式。
請先在 webpack.config.js
中新增舊版指令碼的設定:
const legacyConfig = {
entry,
output: {
path: path.resolve(__dirname, "public"),
filename: "[name].bundle.js"
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: [
["@babel/preset-env", {
useBuiltIns: "usage",
targets: {
esmodules: false
}
}]
]
}
},
cssRule
]
},
plugins
}
請注意,與其使用 "@babel/preset-env"
的 targets
值,而是改用值為 false
的 esmodules
。這表示 Babel 包含所需的所有必要轉換和 polyfill,可用於指定尚未支援 ES 模組的每個瀏覽器。
在 webpack.config.js
檔案的開頭新增 entry
、cssRule
和 corePlugins
物件。這些模組和提供給瀏覽器的舊版指令碼會共用。
const entry = {
main: "./src"
};
const cssRule = {
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader"
})
};
const plugins = [
new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
new HtmlWebpackPlugin({template: "./src/index.html"})
];
現在,在定義 legacyConfig
的下方模組指令碼中,以類似方式為模組指令碼建立設定物件:
const moduleConfig = {
entry,
output: {
path: path.resolve(__dirname, "public"),
filename: "[name].mjs"
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: [
["@babel/preset-env", {
useBuiltIns: "usage",
targets: {
esmodules: true
}
}]
]
}
},
cssRule
]
},
plugins
}
這裡的主要差異在於輸出檔案名稱是使用 .mjs
副檔名。這裡的 esmodules
值會設為 True,表示輸出至這個模組的程式碼是較小、編譯較少的指令碼,不會執行本範例中的任何轉換,因為支援模組的瀏覽器已支援所有所使用的功能。
在檔案最末端,將兩個設定匯出到單一陣列中。
module.exports = [
legacyConfig, moduleConfig
];
現在,這會為支援該模組的瀏覽器建構較小的模組,並為舊版瀏覽器建構更大的轉譯指令碼。
支援模組的瀏覽器會忽略具有 nomodule
屬性的指令碼。相反地,不支援模組的瀏覽器會忽略具有 type="module"
的指令碼元素。這表示您可以納入模組和編譯的備用方案。在理想情況下,兩個應用程式版本應位於 index.html
中,如下所示:
<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js"></script>
支援模組擷取及執行 main.mjs
並忽略 main.bundle.js.
的瀏覽器,則不支援模組。
請注意,與一般指令碼不同,模組指令碼一律會根據預設延遲。如果您希望對等的 nomodule
指令碼也延遲,並且只在剖析後執行,則必須新增 defer
屬性:
<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js" defer></script>
最後,您需要分別在模組和舊版指令碼中新增 module
和 nomodule
屬性,並在 webpack.config.js
最頂端匯入 ScriptExtHtmlWebpackPlugin:
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");
現在,請更新設定中的 plugins
陣列,加入這個外掛程式:
const plugins = [ new ExtractTextPlugin({filename: "[name].css", allChunks: true}), new HtmlWebpackPlugin({template: "./src/index.html"}), new ScriptExtHtmlWebpackPlugin({ module: /\.mjs$/, custom: [ { test: /\.js$/, attribute: 'nomodule', value: '' }, ] }) ];
這些外掛程式設定會為所有 .mjs
指令碼元素新增 type="module"
屬性,以及所有 .js
指令碼模組的 nomodule
屬性。
在 HTML 文件中提供模組
最後,您需要將舊版和新型指令碼元素輸出至 HTML 檔案。可惜的是,用來建立最終 HTML 檔案的外掛程式 HTMLWebpackPlugin
,目前不支援模組和 nomodule 指令碼的輸出內容。雖然目前有解決方法和獨立外掛程式可以解決這個問題 (例如 BabelMultiTargetPlugin 和 HTMLWebpackMultiBuildPlugin),但為了本教學課程,這個做法較適合手動新增模組指令碼元素。
請將下列程式碼新增至檔案結尾的 src/index.js
中:
...
</form>
<script type="module" src="main.mjs"></script>
</body>
</html>
現在在支援模組的瀏覽器上載入應用程式,例如最新版的 Chrome。
系統只會擷取模組,組合大小較小,因為系統大量未轉譯模組!瀏覽器會完全忽略其他指令碼元素。
如果您在舊版瀏覽器中載入應用程式,則只會擷取較大且轉譯的指令碼,其中包含所有必要的 polyfill 和轉換。這張螢幕截圖顯示在舊版 Chrome (38 版) 上提出的所有要求。
結論
現在您已經瞭解如何使用 @babel/preset-env
,只提供目標瀏覽器所需的 polyfill。您也會知道如何藉由推送兩個不同的應用程式傳輸版本,讓 JavaScript 模組進一步改善效能。熟悉這兩種技術如何大幅縮減套裝組合大小,接著進行下一步吧!