啟用新型 JavaScript 依附元件和輸出功能來改善效能。
雖然超過 90% 的瀏覽器能夠執行新式 JavaScript,但目前市面上仍有許多舊版 JavaScript 出現,目前仍是網路效能問題的一大原因。
新型 JavaScript
新型 JavaScript 的特徵並非為特定 ECMAScript 規格版本編寫的程式碼,而是所有新式瀏覽器支援的語法。Chrome、Edge、Firefox 和 Safari 等新式網路瀏覽器佔了超過 90% 的瀏覽器市場,而使用相同基礎轉譯引擎的不同瀏覽器所佔的比重又是 5%。也就是說,全球有 95% 的網路流量來自支援過去 10 年內最廣泛使用的 JavaScript 語言功能的瀏覽器,包括:
- 類別 (ES2015)
- 箭頭函式 (ES2015)
- 產生器 (ES2015)
- 區塊範圍 (ES2015)
- 解構 (ES2015)
- 休息與傳播參數 (ES2015)
- 物件簡寫 (ES2015)
- Async/await (ES2017)
一般來說,新版語言規格中的功能對新式瀏覽器的支援程度較低。舉例來說,許多 ES2020 和 ES2021 功能僅支援 70% 的瀏覽器市場 (仍是大多數瀏覽器),但直接依賴這些功能並不安全。這表示雖然「新式」JavaScript 會不斷變動,但 ES2017 的瀏覽器相容性範圍最廣,同時還納入了多數常用的新型語法功能。換句話說,ES2017 是現今最接近現代語法。
舊版 JavaScript
舊版 JavaScript 會特別避免使用上述所有語言功能。多數開發人員使用新型語法來編寫原始碼,但會將所有內容編譯為舊版語法,藉此增加對瀏覽器的支援。編譯舊版語法確實會增加瀏覽器支援程度,但效果通常不如我們所預期。多數情況下,支援會從約 95% 增加至 98%,並會產生高額費用:
舊版 JavaScript 通常比新型程式碼大 20% 大、更慢。工具缺陷和設定錯誤通常能進一步拉高差距。
已安裝程式庫佔一般實際工作環境 JavaScript 程式碼的 90%。由於 polyfill 和輔助程式的重複作業,發布新程式碼可避免程式庫程式碼的重複作業,因此程式庫程式碼會產生更高的舊版 JavaScript 負擔。
npm 上的新型 JavaScript
Node.js 最近將 "exports"
欄位標準化,以定義套件的進入點:
{
"exports": "./index.js"
}
"exports"
欄位參照的模組意味著節點版本至少為 12.8,可支援 ES2019。這表示使用 "exports"
欄位參照的任何模組都可以使用新型 JavaScript 編寫。套件取用者必須假設具有 "exports"
欄位的模組含有新型程式碼,並視需要使用轉譯功能。
僅限現代
如果想發布含有新式程式碼的套件,並讓用戶端在將其做為依附元件使用時處理轉譯作業,請僅使用 "exports"
欄位。
{
"name": "foo",
"exports": "./modern.js"
}
使用舊版備用方案進行現代化
使用 "exports"
欄位和 "main"
即可以新式程式碼發布套件,同時加入適用於舊版瀏覽器的 ES5 + CommonJS 備用內容。
{
"name": "foo",
"exports": "./modern.js",
"main": "./legacy.cjs"
}
提供舊版備用和 ESM 套件組合最佳化服務
除了定義備用的 CommonJS 進入點,"module"
欄位也可用於指向類似的舊版備用組合,但使用 JavaScript 模組語法 (import
和 export
) 的組合。
{
"name": "foo",
"exports": "./modern.js",
"main": "./legacy.cjs",
"module": "./module.js"
}
許多套件 (例如 Webpack 和 Rollup) 都會依賴這個欄位來利用模組功能並啟用樹狀結構。這仍是舊版套件,除了 import
/export
語法之外,不含任何現代化程式碼,因此請使用這個方法,發布包含仍已針對套裝組合進行最佳化的舊版備用程式碼。
在應用程式中使用新型 JavaScript
網頁應用程式中的大部分一般實際工作環境 JavaScript 程式碼都是由第三方依附元件組成。雖然 npm 依附元件過去是以舊版 ES5 語法發布,但已不適用安全假設,且風險依附元件更新破壞應用程式的瀏覽器支援。
隨著 npm 套件遷移至新式 JavaScript 的增加,您必須確保建構工具妥善設定可以處理這些套件。您依附的 npm 套件很有可能已經開始使用新型語言功能。有多種選項可使用 npm 中的新式程式碼,且不會在舊版瀏覽器中破壞應用程式。不過,一般而言,建構系統會將依附元件轉換成與原始碼相同的語法目標。
Webpack
從 Webpack 5 開始,現在已經可以設定 Webpack 產生套件和模組程式碼時要使用的語法。系統不會轉譯程式碼或依附元件,只會影響 webpack 產生的「glue」程式碼。如要指定瀏覽器支援目標,請在專案中新增瀏覽器清單設定,或直接在 Webpack 設定中新增:
module.exports = {
target: ['web', 'es2017'],
};
也可以設定 Webpack 產生最佳化套件,以在針對新型 ES 模組環境為目標時省略不必要的包裝函式。這也會將 Webpack 設為使用 <script type="module">
載入程式碼分割組合。
module.exports = {
target: ['web', 'es2017'],
output: {
module: true,
},
experiments: {
outputModule: true,
},
};
有許多 Webpack 外掛程式可供編譯及推送新型 JavaScript,同時仍支援舊版瀏覽器,例如最佳化工具外掛程式和 BabelEsmPlugin。
最佳化工具外掛程式
最佳化工具外掛程式是 Webpack 外掛程式,可將最終封裝程式碼從現代 JavaScript 轉換為舊版 JavaScript,而非每個個別來源檔案。這是一種獨立設定,可讓您的 webpack 設定假設所有項目都是新式 JavaScript,且沒有用於多個輸出或語法的特殊分支版本。
由於最佳化工具外掛程式是在套件 (而非個別模組) 上運作,因此會平均處理應用程式的程式碼和依附元件。這樣就能安全地使用 npm 中的新 JavaScript 依附元件,因為其程式碼會封裝並轉譯為正確的語法。與採用兩個編譯步驟的傳統解決方案相比,這種做法速度也可能更快,同時仍可為新版和舊版瀏覽器分別產生套件。這兩組套件都設計為使用模組/nomodule 模式載入。
// webpack.config.js
const OptimizePlugin = require('optimize-plugin');
module.exports = {
// ...
plugins: [new OptimizePlugin()],
};
Optimize Plugin
比自訂 Webpack 設定的速度更快,效率也更高,因為這類設定通常結合了新型和舊版程式碼。該程式庫也能為您處理執行 Babel 的程序,並使用 Terser 來壓縮套件,並為新版和舊版輸出提供獨立的最佳設定。最後,系統會將產生的舊版套件所需的 polyfill 擷取到專屬指令碼中,這樣新版瀏覽器就不會重複或不必要的載入。
BabelEsmPlugin
BabelEsmPlugin 是可與 @babel/preset-env 的 Webpack 外掛程式,產生現有套件的較新版本,將較不易轉譯的程式碼提供給新型瀏覽器。這是模組/無模組最常用的現成解決方案,由 Next.js 和 Preact CLI 使用。
// webpack.config.js
const BabelEsmPlugin = require('babel-esm-plugin');
module.exports = {
//...
module: {
rules: [
// your existing babel-loader configuration:
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
},
],
},
plugins: [new BabelEsmPlugin()],
};
BabelEsmPlugin
支援各種 Webpack 設定,因為這個外掛程式會執行兩個大不相同的應用程式版本。對於大型應用程式,編譯兩次作業可能需要一點時間才能完成,不過這項技術可讓 BabelEsmPlugin
完美整合至現有的 Webpack 設定,成為最便利的選項之一。
將 babel-loader 設為 transpile node_modules
如果您使用的不是前兩個外掛程式的其中一個,就必須要有 babel-loader
這個重要步驟,才能使用新型 JavaScript npm 模組。定義兩個獨立的 babel-loader
設定後,系統就能將 node_modules
中的新型語言功能自動編譯為 ES2017,同時仍使用 Babel 外掛程式和在專案設定中定義的預設設定,編譯您自己的第一方程式碼。這不會為模組/無模組設定產生新型和舊版套件,但可以安裝並使用包含新式 JavaScript 的 npm 套件,而不會中斷舊版瀏覽器。
webpack-plugin-modern-npm 使用這項技術來編譯 package.json
中具有 "exports"
欄位的 npm 依附元件,因為這些依附元件可能包含新型語法:
// webpack.config.js
const ModernNpmPlugin = require('webpack-plugin-modern-npm');
module.exports = {
plugins: [
// auto-transpile modern stuff found in node_modules
new ModernNpmPlugin(),
],
};
或者,您也可以在問題解決時檢查模組 package.json
中的 "exports"
欄位,以手動在 webpack 設定中實作該技術。如果您為了保持精簡而省略快取,自訂實作可能會像這樣:
// webpack.config.js
module.exports = {
module: {
rules: [
// Transpile for your own first-party code:
{
test: /\.js$/i,
loader: 'babel-loader',
exclude: /node_modules/,
},
// Transpile modern dependencies:
{
test: /\.js$/i,
include(file) {
let dir = file.match(/^.*[/\\]node_modules[/\\](@.*?[/\\])?.*?[/\\]/);
try {
return dir && !!require(dir[0] + 'package.json').exports;
} catch (e) {}
},
use: {
loader: 'babel-loader',
options: {
babelrc: false,
configFile: false,
presets: ['@babel/preset-env'],
},
},
},
],
},
};
使用這個方法時,您必須確保縮減器支援新型語法。Terser 和 uglify-es 都可選擇指定 {ecma: 2017}
來保留,並在某些情況下產生 ES2017 語法。
匯總
Rollup 內建支援功能,可在單一建構作業中產生多組套件,並預設產生新型程式碼。因此,您可以將 Rollup 設定為使用您可能已使用的官方外掛程式產生新型和舊版套件。
@rollup/plugin-babel
如果使用 Rollup,getBabelOutputPlugin()
方法 (由 Rollup 的官方 Babel 外掛程式提供) 會將程式碼轉換為產生的套件,而非個別來源模組。Rollup 內建支援功能,可在單一建構作業中產生多組套件,每個組合都有自己的外掛程式。您可以使用這個運算子,透過不同的 Babel 輸出外掛程式設定,為現代和舊版產生不同套件:
// rollup.config.js
import {getBabelOutputPlugin} from '@rollup/plugin-babel';
export default {
input: 'src/index.js',
output: [
// modern bundles:
{
format: 'es',
plugins: [
getBabelOutputPlugin({
presets: [
[
'@babel/preset-env',
{
targets: {esmodules: true},
bugfixes: true,
loose: true,
},
],
],
}),
],
},
// legacy (ES5) bundles:
{
format: 'amd',
entryFileNames: '[name].legacy.js',
chunkFileNames: '[name]-[hash].legacy.js',
plugins: [
getBabelOutputPlugin({
presets: ['@babel/preset-env'],
}),
],
},
],
};
其他建構工具
Rollup 和 webpack 可高度設定。一般來說,每個專案都必須更新設定,在依附元件中啟用新型 JavaScript 語法。另外,還有採用慣例與預設值的較高層級建構工具,例如 Parcel、Snowpack、Vite 和 WMR。這些工具大多假設 npm 依附元件可能包含新式語法,並在建構實際工作環境時,將其轉換為適當的語法層級。
除了 Webpack 和 Rollup 的專屬外掛程式之外,您也可以使用 devolution,將含有舊版備用項的新 JavaScript 套件加入任何專案。Devolution 是一項獨立工具,可轉換建構系統的輸出內容,以產生舊版 JavaScript 變化版本,進而建立繫結和轉換作業,以假設現代的輸出目標。