Laravel, Vite, Vueで古いブラウザをサポートする
※ 明確にどうするといった記述はなく、どのような手法が取れるかなるべく網羅的に書くようにしている
ある時、iPadを利用しちるお客さんからサービスが正常に動作しない指摘をいただいた。
聞いてみると詳細な原因については特定には至らなかったが、端末依存の問題らしい。
実機を持っておらず、詳細を調査することが困難であったため、クラウドで実機テストできるサービスを利用してみた。
フリートライアルで利用できるのはわずか30秒〜2分程度であるがその間に「ページが動作しないこと」、「DevToolでエラーが発生していること」を確認しおそらくiOSのバージョン(つまりSafariのバージョン)に依存した問題であることまでわかった。
(本当に便利なサービスなのでもし今後も使うようになりそうなら課金したい($7~40くらいだった))
特に気になったのはDevToolで出力されていた以下のエラー
syntaxError Unexpected token '?'
おそらくであるが、オプショナルチェーンやnull合体演算子が利用できないか何かだろう。
例えばモバイルSafariでは null合体演算子のサポートは 13.4以降 [1][2] のようで、お客様のiOSバージョンよりも高いことからこれらが問題であるとした。
[1] https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing
[2] https://caniuse.com/mdn-javascript_operators_nullish_coalescing
ここまでわかれば、あとは対応を行うのみである。
Vue.js のレガシー環境への対応方法
browserslist を使った対応ブラウザの指定
レガシー環境への対応とはやや異なるかもしれないが、プロジェクトでサポートしているブラウザなどを指定することでビルドの最適化を図ることができる。
browserslist は polyfill系 に用いられるプロジェクトで対応しているブラウザやバージョンを指定するための仕組み?ツール。
browserslist を解釈して対応してくれるようなビルドツールは以下の通りである。
- Autoprefixer
- Babel
- postcss-preset-env
- eslint-plugin-compat
- stylelint-no-unsupported-browser-features
- postcss-normalize
- obsolete-webpack-plugin
ref: https://github.com/browserslist/browserslist
例えば、Autoprefixerはbrowserslistの設定を読んで、CSSにどのベンダープレフィックスを付けるかを決める。
これらの設定は package.json
か .browserslistrc
に指定する。
例えば vue3 ではプロジェクトを作成した段階で、以下のような設定が package.json
に記載されている。
{
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}
またプロジェクトでサポートしているブラウザは以下で確認が行える
npx browserslist
Modern Mode
Vue.js のモダンモードでは最新のブラウザの仕様と古いブラウザの仕様をうまく利用した対応を行なっている。
結論から言うと、このモダンモードを適用した場合、出力されるjsとHTMLに埋め込まれるscriptタグは以下のようになる
<script defer="defer" src="/js/chunk-vendors.b94429f8.js" type="module"></script>
<script defer="defer" src="/js/app.52172fba.js" type="module"></script>
<script defer="defer" src="/js/chunk-vendors-legacy.88d135c4.js" nomodule></script>
<script defer="defer" src="/js/app-legacy.7a0f538a.js" nomodule></script>
主な違いは、app.js
とapp-legacy.js
のような2パターンのjsが出力されており、scriptタグの属性が異なるものが指定されている点にある。
比較的モダンなES2015以上をサポートしたブラウザではモジュールを利用することができ、 script
タグ のtype属性に
moduleプロパティを指定することでモジュールとして読み込みが行われる。 一方でモジュールをサポートしていないレガシーなブラウザでは
type="module"を解釈できず scriptタグの読み込みを行わない。 一方で、モジュールをサポートしているモダンなブラウザは
nomodule` 属性を持った scriptタグの読み込みをスキップするため2重でスクリプトを読み込むことがない。
ref: https://html.spec.whatwg.org/multipage/scripting.html#attr-script-nomodule
通常、Babelを用いることで Polyfill を適用してレガシーなブラウザでも最新の機能を利用できるようにすることができる。
ただしこの方法ではポリフィルのバンドルをリリースすることとなり単にビルドするよりも冗長になるだけでなく、パースや実行速度が遅くなるといったデメリットがある。
上記のように2つのスクリプトをビルドして読み込ませるものをブラウザごとに分けることで非常に簡単にレガシーなブラウザの対応ができるようになるのでかなり面白い方法だと思う。
この方法はPhillip Walton'sさんが提唱したものらしいが内容は読んでいないので下記にLinkを貼っておく。
Laravel, Vite, Vue3 のレガシーブラウザへの対応
上記の話を前提に、Lararvel と Vite, Vue3 を使っているプロジェクトでの対応を行う
主に以下のステップにより完了できる
-
@vitejs/plugin-legacy
とterser
のインストール -
vite.config.js
の修正 - @vite ディレクティブの修正
- ServiceProviderの作成
@vitejs/plugin-legacy
と terser
のインストール
1. ESMに対応していないレガシーなブラウザ向けにビルドを行ってくれるライブラリ
また依存関係として terser
が必要になる
yarn add @vitejs/plugin-legacy --dev
yarn add terser --dev
@vitejs/plugin-legacy
は nodeが ^18.0.0 || >=20.0.0
のみのサポートとなり v19.0.0
などは利用できないので要注意
vite.config.js
の修正
2. @vitejs/plugin-legacy
の基本的な利用方法は以下の通り
どのブラウザをサポートするかターゲットを指定する。
import legacy from '@vitejs/plugin-legacy'
export default defineConfig({
plugins: [
vue(),
laravel(src),
legacy({
targets: ['defaults', 'ios_saf >= 12', 'not IE 11'],
modernPolyfills: true
})
],
})
ここの設定は以下の記事を参考にしており、今回の対応にとても役立った。
vite.config.js
を修正したことにより、app.js
に加えて、 app-legacy.js
のようにレガシーブラウザ向けのjsがビルドされるようになる。
vite build
出力されるファイルやmanifest.json
に -legacy.js
が含まれているか確認する。
出力されていればいったんは大丈夫そう。
3. @vite ディレクティブの修正
*-legacy.js
が生成されたのであれば、 laravelのblade上で読み込みを行えるようにする必要がある。
通常、viteで生成された js や css は@vite
ディレクティブを用いてbladeに埋め込みを行う。
// 例えば app.js を読み込む場合
@vite(['resources/js/app.js'])
これに *-legacy.js
を追加する
// 例えば app.js を読み込む場合
- @vite(['resources/js/app.js'])
+ @vite(['resources/js/app.js', 'resources/js/app-legacy.js'])
この時、manifest.json
に app-legacy.js
が存在しないとエラーになるので要注意
これで app-legacy.js
がbladeに反映されるようになる。
4. ServiceProviderの作成
通常、 @vitejs/plugin-legacy
でレガシーブラウザ向けの対応を行うと、吐き出された html上で type="module"
とnomodule
の対応が行われる。
しかしLaravelを用いたプロジェクトの場合は laravel-vite-plugin
を用いており、 @vite ディレクティブがうまいことスクリプトタグを生成することで動作している。
laravel-vite-plugin
では @vitejs/plugin-legacy
の対応を行う様子がなく、自分でなんとかする必要がある。
driesvints: Right now, we don't aim to be compatible with this library, sorry.
https://github.com/laravel/vite-plugin/issues/114
この対応というのが前述した「Modern Mode」への対応である。
通常の app.js
は type="module
で読み込み、 app-legacy.js
は nomodule
で読み込むように対応するものである。
例えば何も対応しない状態ではどのようになっているか確認してみる。
vite build
viteのビルドを行い、public/build/assets
にビルドファイルが出力されている状態でviteの開発サーバーを起動せずにページを開く。
すると、app.js
もapp-legacy.js
も type="module"
で読み込まれていることがわかる。
さらに <link rel="modulepreload" href="................/app-legacy-0000.js">
のようにプリロードも行われている。
app-legacy.js
は nomodule
属性になるようにしなければならなく、app-legacy.js
をモダンブラウザでプリロードするのはあまり好ましくないので行わないように変更したい。
そこで ServiceProvider で Vite の書き出しを制御する方法をとる。
下記の ServiceProvider を作成して config/app.php
に登録する
<?php
declare(strict_types=1);
namespace App\Providers;
use Illuminate\Support\Facades\Vite;
use Illuminate\Support\ServiceProvider;
class ViteServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
//
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
// -legacy が含まれているもののみ preload しないようにする
Vite::usePreloadTagAttributes(function ($src) {
if (str_contains($src, '-legacy')) {
return false;
}
return [];
});
// -legacy が含まれているもののみ nomodule 属性を付与する
Vite::useScriptTagAttributes(function (string $src, string $url) {
if (str_contains($src, '-legacy')) {
return [
'type' => 'text/javascript',
'nomodule',
];
}
return [
'type' => 'module',
];
});
}
}
この対応を行ったのちに再度確認すると、 app.js
には type="module"
が付与され、 app-legacy.js
には nomodule
が付与される状態となっているはずである。
これで対応がおそらく完了するはず