發布、提供及安裝新式 JavaScript,加快應用程式速度

啟用新型 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 模組語法 (importexport) 的組合。

{
  "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.jsPreact 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'],
          },
        },
      },
    ],
  },
};

使用這個方法時,您必須確保縮減器支援新型語法。Terseruglify-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 語法。另外,還有採用慣例與預設值的較高層級建構工具,例如 ParcelSnowpackViteWMR。這些工具大多假設 npm 依附元件可能包含新式語法,並在建構實際工作環境時,將其轉換為適當的語法層級。

除了 Webpack 和 Rollup 的專屬外掛程式之外,您也可以使用 devolution,將含有舊版備用項的新 JavaScript 套件加入任何專案。Devolution 是一項獨立工具,可轉換建構系統的輸出內容,以產生舊版 JavaScript 變化版本,進而建立繫結和轉換作業,以假設現代的輸出目標。