jQueryベースのレガシーフロントエンドをモダンにするためのアレコレをまとめます。なお、一足飛びなリプレースの話ではなく地に足ついた一歩ずつの改善です。
不要コードの削除
モダン化のすべての基本ですね!不要なコードを削除していきましょう。バックエンドと違って静的解析で検知することが若干難しいのですが、コードをgrepしたりアクセスログを見て利用していないことを確認すれば比較的安全に削除していくことができます。
なぜ最初にやるかというと、フォーマッタやLinterが全適用になるので無駄な変更になったり認知負荷が上がるためです。
フォーマッタやLinterを入れる
とりあえず何かしらフォーマッタやLinterは入れておきましょう。 フォーマッタは一括適用でき、リスクが低めなので早めに入れましょう。Linterはエラーをすべて解消するのは大変なのでignoreしたり設定を無効化したりして導入しましょう。
使うツールとしては設定が簡単でフォーマッタとLinter両方に対応している Biome がオススメ。どのツールを使うかよりは導入すること自体の価値のほうが圧倒的に高いので、何でも良いのでそれなりに枯れているツールを迷わず入れれば良いと思います。
JavaScriptのビルド環境を整える
ビルド環境が整っていないと、お手製のキャッシュバスティングをしていたり(あるいはキャッシュバスティング忘れてうまく変更が反映されなかったり)、HMRが効かなかったり、minifyなど最適化した状態で配信されていなかったり、Nodeモジュールが使えなかったり、TypeScriptが使えなかったりと色々と不便です。適切にモジュール化できれば、グローバル汚染も防げます。ということでビルド環境を整えましょう。
使うツールとしてはとりあえず Vite を入れておけばOK。Laravelなら Laravel Vite Plugin で良い感じにVite連携できます。
ビルド環境を整えずNative ESMを使う方法もありますが、キャッシュバスティングやHMRなどのメリットを考えると、ちょっと頑張ってビルド環境を整えたほうが良いと思います。
Nodeモジュールを使ったバージョン管理
ダウンロードしたjQueryなどのライブラリをそのままGit管理していたりするのはレガシーフロントあるあるですね。ということで、上記でビルド環境を整えたらライブラリをNodeモジュール(NPM)から使うように変更しましょう。これによってGit管理も軽くなるし、Renovate/Dependabotなどの自動アップデートも機能するようになります。
CDNや type="javascript"
で読み込んでいるライブラリはwindowのグローバルに定義されているため、最初は以下のようにESM内でグローバルに定義することで既存のコードを変更せずにNPM板を利用することができます。
import $ from 'jquery';
globalThis.$ = $;
エラー管理ツールを導入
レガシーフロントエンドを改善するとき、予期せぬエラーが発生するのは常です。というか改善して無くてもエラーが出てます。気づいてないだけ。特に本番環境ではブラウザやネットワークなど様々な要因でエラーが発生しがちです。ということでエラーを早く検知できるようにSentryなどのエラー管理ツールを導入して、検知・管理ができるようにしましょう。
TypeScriptを使った型付け
フロントエンドはバックエンドと比べるとテストが書きづらい、あるいはテストの効果を出しづらいです。ということでTypeScriptを使って型安全にしておくと堅牢性が上がって良いです。関数化された処理では引数や戻り値を型付けしていくと安心です。とはいえ、jQueryなどの直接的なDOM操作だとTypeScriptの型付けの恩恵が受けづらいです。できれば部分的にAlpine.jsやReactなどの宣言的UIに置き換えてTypeScriptの恩恵を受けれるような形に持っていくのがオススメです。
テスト実装
フロントエンドもテストできる部分は関数化するなどしてテストを書いていきましょう。ツールとしてはVitestがオススメで導入が簡単なのと、Vitest Browser Mode を使えばjQueryのテストも可能です。
ライブラリの定期アップデート
ライブラリがNPM管理されていると、ライブラリの定期アップデートができます。ということでRenovateやDependabotによる定期アップデートを入れましょう。個人的にはRenovateがオススメです。よく使う設定は以下のブログ記事を参照。
リファクタリングする
以下のようなリファクタリングをちょっとずつ行います。
var
ではなく let
const
を使う
varは再宣言や巻き上げ(ホスティング)の問題があり、可読性の低いコードや予期せぬ挙動を生む可能性が高いため利用しない方が良いです。 基本的にconstを使い、再代入が必要なものだけletを使うと安全です。
【JavaScript】var / let / const を本気で使い分けてみた
グローバル定義を使わない
<script>
の中で定義した変数・関数や、ESMで window.xxx = ...;
と書いたものはグローバル(windowオブジェクト)に定義されます。 グローバルに定義されると影響範囲がわかりづらくなるので、別ファイルから使う関数は export しておき、利用側で import してください。共通で使いたいモジュールローカルな変数はexportしたsetter/getter経由でアクセスする感じになります。
HTML内に onclick="someFunction()"
といった形で直接JavaScriptを記述する場合はグローバル定義が必要ですが、可能であればonclick属性を直接利用せずにイベントリスナーや宣言的UIなライブラリで処理を置き換えておくと良いです。
async/awaitを使う
Promiseオブジェクトは then() や catch() などの関数が利用できますが、可読性があまりよくないので async
await
を使いましょう。
宣言的UIを使う
やっぱりどんなに改善しても手続き的なDOM操作は可読性が悪かったり型安全にしづらいです。ということで可能であれば宣言的UIにすると良いです。
React.jsやVue.jsなどの有名どころの選択肢がありますが、部分的に使うのは運用的に面倒だったりします。かといって全部置き換えるのはInertia.jsを使っていたとしても結構大変そう。ということで、Alpine.jsあたりがバックエンドのテンプレートエンジンと親和性が高いので置き換えしやすいかと思います。
といいつつ、生成AIを使えばBlade/jQueryからReact/Inertia.jsも簡単にできるのかな…?