LINEヤフー Tech Blog

LINEヤフー株式会社のサービスを支える、技術・開発文化を発信しています。

Vite だけではサポートできないブラウザのための @vitejs/plugin-legacy プラグイン紹介と注意点

LINEヤフー Advent Calendar 2023の18日目の記事です。

こんにちは。UIT開発推進部 Platform開発チームの odan です。LINEログインのフロントエンドおよび LIFF の SDK の開発を担当しています。

背景

LINEログインにはいくつかのバージョンがあり、その中の v2.1 のフロントエンド実装に使用しているフレームワークを Vue2 から Vue3 へのアップグレードを行っています。

現在のプロジェクトは Vue CLI を使用してプロジェクトを構築しており、 Vue3 にアップグレードするにあたって Vite への移行も同時に行っています。

Vue CLI は Webpack をベースとしたビルドシステムです。Vite への移行は Webpack との差分を気をつける必要があります。

この記事ではその差分の中でも Vite が標準でサポートしないブラウザへの対応に焦点を絞って事例を紹介します。

Vite がデフォルトでサポートするブラウザ

本番環境用のビルド | Vite に書かれている通り、Vite はモダンな JavaScript をサポートしているブラウザをターゲットにしています。2023 年 10 月時点のターゲットブラウザは次のとおりです。

  • Chrome >=87
  • Firefox >=78
  • Safari >=14
  • Edge >=88

また、トランスパイル先のターゲットは Vite の設定で変更することができますが、Polyfill の追加はサポートされていません。

LINEログインは様々な LINE 関連サービスに利用されているため、アクセスがあるブラウザも多種多様です。過去に iOS Safari 11 で動作するようにビルドシステムの調整を行ったことがあります。

Vue CLI から Vite への移行が原因で、これまでサービスが動作していたブラウザでサービスが動作しなくなることは避けたいです。ターゲット外のブラウザもサポートするためのプラグインとして @vitejs/plugin-legacy が存在します。このプラグインを採用すればサポートブラウザの要件を満たせるのではないか?と考え、 プラグインのデフォルトの挙動を確かめて、オプションの調査を行いました。

@vitejs/plugin-legacy のデフォルトの挙動

@vitejs/plugin-legacy のオプションを何も指定しないで vite build を実行したときに生成される HTML は次の画像のようになります。

@vitejs/plugin-legacy によって、9行目、10行目、15行目、16行目、17行目の script 要素が生成されます。それぞれの script 要素の挙動を解説します。

type 属性の module

いくつかの script 要素に type="module" 属性が指定されています。これはそのスクリプトがモジュール (ES Modules) であることを宣言するものです。ES Modules を知らない IE などの古いブラウザはこの script 要素を無視します。

nomodule 属性

ES Modules をサポートしているブラウザへ nomodule 属性を指定すると、その script 要素が無視されます。一方でレガシーブラウザは nomodule 属性を知らないので script 要素を実行します。

ES Modules への橋渡しとしての nomodule 属性 | blog.jxck.io で紹介されている通り、この 2 つの属性を組み合わせることで、モダンブラウザとレガシーブラウザで実行する script 要素を切り替えることができます。

9行目の script 要素

以下に script 要素の中身をフォーマットしたコードを示します。

import.meta.url;
import("_").catch(() => 1);
async function* g() {}
if (location.protocol != "file:") {
  window.__vite_is_modern_browser = true;
}

import.meta や Async generator methods や dynamic import() をサポートしているブラウザだった場合、window.__vite_is_modern_browser に true が代入されます。 これは Chromium を採用する前の Edge Legacy などといった、ES Modules をサポートする一方でこれらの API をサポートしていないブラウザを検出するためのコードです。

10行目の script 要素

以下に script 要素の中身をフォーマットしたコードを示します。

!(function () {
  if (window.__vite_is_modern_browser) return;
  console.warn(
    "vite: loading legacy chunks, syntax error above and the same error below should be ignored"
  );
  var e = document.getElementById("vite-legacy-polyfill"),
    n = document.createElement("script");
  (n.src = e.src),
    (n.onload = function () {
      System.import(
        document.getElementById("vite-legacy-entry").getAttribute("data-src")
      );
    }),
    document.body.appendChild(n);
})();

9行目の script 要素で window.__vite_is_modern_browser  true になるモダンブラウザだと判定されたブラウザでは何も処理を行いません。

それ以外のブラウザの場合、後述するレガシー向けの Polyfill をロード、その後に追加でエントリーポイントの JS をロードするためのコードを実行します。

これにより type="module"  nomodule をサポートしているが、モダンな JavaScript の API や構文をサポートしていない Legacy Edge などのブラウザで、レガシー向けの JavaScript を実行させることができます。

15行目の script 要素

以下に script 要素の中身をフォーマットしたコードを示します。

!(function () {
  var e = document,
    t = e.createElement("script");
  if (!("noModule" in t) && "onbeforeload" in t) {
    var n = !1;
    e.addEventListener(
      "beforeload",
      function (e) {
        if (e.target === t) n = !0;
        else if (!e.target.hasAttribute("nomodule") || !n) return;
        e.preventDefault();
      },
      !0
    ),
      (t.type = "module"),
      (t.src = "."),
      e.head.appendChild(t),
      t.remove();
  }
})();

これは nomodule 属性をサポートしていない Safari 10.1/iOS Safari 10.3 向けの Polyfill です。Safari 10.1 nomodule support にあるコードと同じ内容です。

16行目の script 要素

この要素はレガシーブラウザ向けの Polyfill をロードします。

17行目の script 要素

以下に script 要素の中身をフォーマットしたコードを示します。

System.import(
  document.getElementById("vite-legacy-entry").getAttribute("data-src")
);

これはレガシーブラウザ向けのエントリーポイントの JavaScript を SystemJS を使用してロードするコードです。

@vitejs/plugin-legacy のオプションをカスタマイズする

前述した通り @vitejs/plugin-legacy のオプションを指定しない場合、IE や Legacy Edge 向けの Polyfill やエントリーポイントの JavaScript を生成します。

しかし Internet Explorer 11 は2022年6月※1に、Legacy Edge は2021年3月※2にサポートが終了しています。 そのためプロダクトによってはデフォルトの挙動が適さない場合があります。その場合オプションの renderLegacyChunks  false を渡すと前述した script 要素が生成されなくなります。

また @vitejs/plugin-legacy のオプションには modernPolyfills があります。これはモダンブラウザ向けに Polyfill を追加するオプションです。true を渡すと browserslist の設定を見て Polyfill を生成してくれます。 このオプションは iOS Safari 11 をサポートするプロダクトで Async generator methods を使用する場合に適しています。

ただ、このオプションは @vitejs/plugin-legacy の README の注意書きにある通り、15kb 以上の Polyfill が生成されてしまうので推奨されていません。https://cdnjs.cloudflare.com/polyfill/ などのサービスのようにアクセスしてきたブラウザに応じてオンデマンドに Polyfill を生成する方法が推奨されています。

そのため https://cdnjs.cloudflare.com/polyfill/ などを使用できない場合や、サーバサイドで Polyfill を動的に生成できない場合にのみ modernPolyfills を有効化するのが適しています。

以上を踏まえて IE や Legacy Edge はサポートしないけど 古い iOS Safari をサポートしていて、Polyfill を動的に生成できないプロダクトでは、次のような vite.config.ts を用意します※3

export default defineConfig({
  plugins: [
    legacy({
      modernPolyfills: true,
      renderLegacyChunks: false,
    }),
  ],
});

まとめ

Webpack ベースの Vue CLI から Vite への移行にあたって、サポートブラウザを変化させないために @vitejs/plugin-legacy を利用する事例を紹介しました。

@vitejs/plugin-legacy のデフォルトの挙動について解説して、レガシーブラウザのサポートという前提条件を満たさないプロダクトではデフォルトのオプションは適さないことを説明しました。

また Polyfill を動的に生成できないプロダクト向けに modernPolyfills というオプションが存在することを紹介しました。

※1 https://support.microsoft.com/ja-jp/windows/internet-explorer-%E3%81%AE%E3%83%98%E3%83%AB%E3%83%97-23360e49-9cd3-4dda-ba52-705336cc0de2 

※2 https://learn.microsoft.com/ja-jp/lifecycle/announcements/m365-ie11-microsoft-edge-legacy 

※3トランスパイルのターゲットは build.target に設定する必要があるため、https://www.npmjs.com/package/browserslist-to-esbuild などを用いて別途指定する必要があります。