Parcel から Vite へ: 100K LOC の移行に関する短編
3 つのフロントエンドプロジェクトを Parcel から Vite に移行しましたが、そのプロセスは… 滞りなく進みました。
背景
私たちは Logto で 3 つの主要なフロントエンドプロジェクトを持っています: サインイン エクスペリエンス、コンソール、およびライブ プレビューです。これらのプロジェクトはすべて TypeScript、React、および SASS モジュールで構築されており、合計で約 100K 行のコードを持っています。
私たちは、Parcel のシンプルさとゼロ構成のセットアップを気に入っていました。Parcel を使って新しいプロジェクトをセットアップするのがいかに簡単であるかに驚いた日をまだ覚えています。parcel index.html
を実行するだけで、必要な依存関係がすべてインストールされ、プロジェクトが実行されるのです。「経験豊富な」開発者であれば、Gulp や Webpack を使ってセットアップしていた昔と比較して同じように感じるかもしれません。Parcel はまるで魔法の杖のようでした。
Parcel のシンプルさは、時には気まぐれなこともありましたが、私たちがそれに長く固執していた主な理由です。例えば:
- Parcel がプロジェクトをバンドルできないことがありました。これは、実際には存在するチャンクファイルを見つけられなかったためです。
- Monorepo 設定で動作させるには一部のハック的な構成が必要でした。
- MDX 3 をネイティブにサポートしていないため、カスタムのトランスフォーマーを作成する必要があ りました。
- 手動チャンクをサポートしておらず(この記事を書いている時点では、手動チャンク機能はまだ実験段階です)、たいていの場合これで十分ですが、時にはこれが必要になることもあります。
それでは、なぜ何か別のものに移行することを決めたのでしょうか?
- 我々は 2023 年 6 月にリリースされた Parcel 2.9.3 で立ち往生していました。その後、新しいバージョンがリリースされるたびにアップグレードを試みましたが、常にビルドエラーで失敗していました。
- Parcel の最新バージョンは 2024 年 2 月にリリースされた 2.12.0 でしたが、ほぼ毎日のようにコミットが行われているにもかかわらず、その後新しいリリースはありませんでした。
誰かがParcel が死んでいるかどうかを尋ねるディスカッションを開いてさえいました。公式の回答は「いいえ、Parcel はまだ生きている。ただし、大規模なリファクタリングに取り組んでおり、マイナーリリースの時間がない状態です。」というものでした。私たちにとってこれは「アヒルの死」と同じようなものです: 私たちが使用できる最新バージョンは 1 年以上前のものであり、次のバージョンがいつリリースされるのかもわかりません。それは死んでいるように見え、死んでいるように振る舞っているため、私たちにとっては死んでいるのです。
なぜ Vite?
私たちは Vitest から Vite を知りました。数か月前、Jest の ESM サポート (テスト時) に疲れて何か新しいものを試したいと思いました。Vitest はネイティブの ESM サポートと Jest 互換性で私たちの心をつかみました。これは驚くべき開発者体験であり、Vite によって支えられています。
現状
プロジェクトによって設定が異なるかもしれませんが、通常、Vite エコシステムが成長しているため、プラグインの代替を見つけることができます。移行時の私たちのセットアップは次のとおりです:
- Monorepo: 私たちは monorepo を管理するために PNPM (v9) ワークスペースを使用しています。
- モジュール: すべてのプロジェクトで ESM モジュールを使用しています。
- TypeScript: すべてのプロジェクトでパスエイリアスを使用して TypeScript (v5.5.3) を使用しています。
- React: すべてのフロントエンドプロジェクトで React (v18.3.1) を使用しています。
- スタイリング: スタイリングには SASS モジュールを使用しています。
- SVG: SVG を React コンポーネントとして使用しています。
- MDX: GitHub Flavored Markdown や Mermaid のサポート付きで MDX を使用しています。
- 遅延読み込み: ページやコンポーネントをいくつか遅延読み込みする必要があります。
- 圧縮: 本番ビルド用に圧縮されたアセット (gzip と brotli) を生成します。
移行について
最初に新しい Vite プロジェクトを作成し、それがどのように動作するかを確認することで、移行を始めました。プロセスはスムーズで、実際の移行には数日しかかかりませんでした。
即時サポート
Vite は monorepo、ESM、TypeScript、React、SASS を即時サポートしており、必要なプラグインや設定をインストールするだけで動作させることができました。
パスエイリアス
Vite にはパスエイリアスのサポートが組み込まれています。例えば、私たちの tsconfig.json
では次のようになっています:
私たちの vite.config.ts
でも同じ解決法を追加するだけです:
代替パスはプロジェクトルートに対して相対的であるため、絶対パスである必要があることに注意してください。代わりに、vite-tsconfig-paths プラグインを使用して、tsconfig.json
からパスエイリアスを読み取ることもできます。
React Fast Refresh と HMR
Vite には HMR のサポートが組み込まれていますが、React Fast Refresh を有効にするにはプラグインをインストールする必要があります。Vite チームが提供する @vitejs/plugin-react プラグインを使用しました。このプラグインは Fast Refresh のような React 機能を良好にサポートしています:
SVG を React コンポーネントとして使用する
vite-plugin-svgr プラグインを使用して、SVG を React コンポーネントに変換します。Vite 設定にプラグインを追加するだけで簡単です:
ただし、SVG を React コンポーネントに変換する条件を指定しなかったため、すべてのインポートが変換されました。このプラグインはより良いデフォルト設定を提供しています:.svg?react
拡張子でインポートされた SVG のみを変換します。私たちはインポートをそれに応じて更新しました。
SASS モジュール
Vite には SASS モジュールのサポートが組み込まれていますが、1 つだけ注意が必要な点があります:クラス名のフォーマット方法です。クラス名が一貫していない場合、ユーザーや私たちの統合テストにとって厄介になることがあります。vite.config.ts
の 1 行の設定でこの問題を解決できます:
ところで、Parcel と Vite は SASS ファイルのインポート方法に違 いがあります:
* as
構文は Vite でも機能しますが、styles
オブジェクトに動的キーを使用してアクセスするとモジュール化されたクラス名が失われます。例えば:
MDX サポート
Vite は内部で Rollup を活用しているため、公式の @mdx-js/rollup プラグインを使用して MDX をサポートし、そのプラグインも使用できます。設定は次のようになります:
remarkGfm
プラグインは GitHub Flavored Markdown をサポートするために使用され、rehypeMdxCodeProps
プラグインは MDX ファイルのコードブロックに対して props を渡すために使用されます。Docusaurus が行っているように行います。
MDX 内の Mermaid サポート
他のプログラミング言語と同様に、私たちは MDX ファイル内で Mermaid ダイアグラムを使用したいと考えました。使用法は他のコードブロックと同様にシンプルであるべきです:
次のようにレンダリングされるべきです:
私たちのアプリがライトテーマとダークテーマをサポートしているため、ダークテーマで Mermaid ダイアグラムを機能させるために少しコーディングしました。次のように React コンポーネントを作成しました:
useTheme
はコンテキストから現在のテーマを取得するカスタムフックです。mermaid
ライブラリは、初期ページロードの読み込みサイズを減らすために非同期でインポートされます。
MDX ファイルのコードブロックには、一貫したコンポーネントを使用します:
最後に、MDX プロバイダを次のように定義します:
遅延ロード
これは Vite に特有のものではありませんが、移行中にページを遅延読み込みするように更新し、その後何も壊れなかったので言及する価値があります。
React にはコンポーネントを遅延ロードするためのビルトイン関数 React.lazy
があります。ただし、素早く反復する場合に問題が発生することがあります。 私たちはこれらの問題を解決するために react-safe-lazy という小さなライブラリを作成しました。これは React.lazy
のドロップイン置き換えであり、このブログ記事に詳細な説明が記載されています。
圧縮
圧縮されたアセットを生成するために、vite-plugin-compression という便利なプラグインがあります。これは gzip と brotli 圧縮の両方をサポートしており、設定は簡単です:
手動チャンク
Vite (またはその基盤となる Rollup) の素晴らしい機能の 1 つに手動チャンクがあります。React.lazy
はコンポーネントを遅延読み込みするために使用されますが、手動チャンクを指定してどのコンポーネントやモジュールをバンドルするか制御することで、チャンクをより細かく制御できます。
例えば、vite-bundle-visualizer を使用してバンドルサイズと依存関係を分析し、次に適切な関数を書いてチャンクを分割します:
開発サーバー
本番ビルドとは異なり、Vite は開発モードではソースコードをバンドルせず、同じ monorepo のリンクされた依存関係を含め、すべてのモジュールをファイルとして扱います。私たちにとっては、最初にブラウザが数百のモジュールを読み込むことになり、これは一見すると狂気のように思えますが、実際にはほとんどの場合大丈夫です。詳細はこちらのディスカッションで確認できます。
もしこれがあなたにとって問題なら、完全に解決策ではありませんが、vite.config.ts
の optimizeDeps
オプションにリンクされた依存関係をリストアップすることが代替手段として考えられます:
これにより「事前バンドル」されたリンク依存関係が作成され、開発サーバーが高速化されます。ただし、この問題点は HMR が期待通りに動作しない場合があることです。
さらに、私たちは本番環境で静的ファイルを提供し、開発環境ではリクエストを Vitest サーバーにプロキシするプロキシを使用しています。競合を避けるために特定のポートを設定しており、これは vite.config.ts
で簡単に設定できます:
環境変数
Parcel とは異なり、Vite は import.meta.env
を使用して環境変数を処理するモダンなアプローチを採用しています。これにより、.env
ファイルが自動的に読み込まれ、コード内の変数が置換されます。ただし、すべての環境変数には VITE_
接頭辞が必要です (設定可能)。
私たちが Parcel を使用していた時は、接頭辞を確認することなく process.env
変数を単純に置換していました。したがって、移行を容易にするために define
フィールドを使用した回避策を考えました:
これにより、環境変数に徐々に接頭辞を追加し、define
フィールドを削除することができます。
結論
以上です!3 つのフロントエンドプロジェクトを Parcel から Vite に無事に移 行しました。この短編が皆さんの移行の助けになれば幸いです。最終的な設定は次のようになります: