レガシーなMPAアプリケーションをWebpackからViteに移行する話
どうもoreoです。
今回は、Ruby on RailsのMPAアプリケーションにおけるWebpackからViteへの移行を通じて得た知見を記載します。同じような技術スペックでVite移行を考えられている方の参考になれば幸いです!
1 結論
- まだリリースできていない(2022/10/16時点。リリース時期調整中。)
- 歴史あるアプリでの移行は非常に大変
- しかし、開発体験向上やアプリケーションの課題発見など得られる恩恵は大きい
2 Vite移行を検討するに至った背景
弊社アプリは、Vue2.7+Composition APIで開発を行っており、vue2系では比較的モダンな構成です。一方、Ruby on RailsのView毎にVueインスタンスを生成するMPAであり、アプリケーション全体の構成としては、レガシーな構造になっています。
エントリーポイントが多いことによる弊害で、サービス拡大に伴いビルド時間は長くなり、最近では開発サーバーの立ち上げに約2分も掛かるようになりました。この開発体験が良いとは言えない状態を改善するため、viteへの移行を進めていきました
3 移行手順
RailsアプリへのVite導入が比較的簡単なVite Railsを使用して、以下のような6つのステップに分けて作業を進めていきました。
- サンプルアプリで Vite Railsを触る
- 一つのエントリーポイントでバンドルし画面表示
- 全エントリーポイントでバンドルし画面表示
- Webpackの設定移行
- 開発サーバーで HMR を有効化
- ローカル環境と qa 環境でテスト
3-1 サンプルアプリで Vite Railsを触る
まずは、いきなりアプリケーションにVite Railsを導入すると辛くなるだけなので、まっさらな状態でVite Railsが動いているアプリを作成し、Vite Railsの基本的な設定方法や動作を知りました。
$ bundle exec rails new .
$ bundle install
$ bundle exec vite install
# Gemfile
gem 'rails', '~> 6.0.4'
gem 'vite_rails'
サンプルアプリの作り方は下記をご覧ください。
3-2 一つのエントリーポイントでバンドルし画面表示
続いて、実際のアプリケーションコードにVite Railsを導入しました。なるべくシンプルな状態から順番に導入を進めていきたかったので、まずは一つのエントリーポイントで、バンドルを行い画面が表示されるまで実装を進めていきました。
config/vite.json
でエントリーポイントを一つに設定。
"all": {
"sourceCodeDir": "frontend",
"entrypointsDir": "entry/home",
"watchAdditionalPaths": []
},
Vite Railsが用意しているTag Helpersをテンプレートエンジンで読み込ませてることで、画面の表示が可能となりました。
また、この時、とりあえず画面を表示する為に、require
をimport
に変更、process.env
をimport.meta.env
に変更などの対応が必要が必要でした。
3-3 全エントリーポイントでバンドルし画面表示
続いて全てのエントリーポイントを対象にバンドルを行い、画面が表示されるまで実装を進めました。
この時の主なポイントは下記です。
ポイント① SimpackerのメソッドをVite用に置き換える
もともとSimpackerのメソッドをテンプレートエンジンで呼び出すことでJSやCSSを読み込んでいたので、そのメソッドをVite用に置き換えることで、テンプレートエンジンの変更を最小限にすることができました。
before
module SimpackerHelper
def javascript_packs_with_chunks_tag(*names, **options)
paths = names.flat_map { |name| simpacker_context.manifest.lookup!("entrypoints", name, "assets", "js") }.uniq
javascript_include_tag(*paths, **options)
end
def stylesheet_packs_with_chunks_tag(*names, **options)
paths = names.flat_map { |name| simpacker_context.manifest.lookup!("entrypoints", name, "assets", "css") }.uniq
stylesheet_link_tag(*paths, **options)
rescue Simpacker::Manifest::MissingEntryError
end
end
after
module ViteRailsHelper
def javascript_packs_with_chunks_tag(*names, **options)
paths = names.map { |name| "entry/#{name}" }
vite_javascript_tag(*paths, **helperOptions(options))
end
def stylesheet_packs_with_chunks_tag(*names, **options)
unless ViteRuby.new.dev_server_running?
paths = names.map { |name| "entry/#{name}" }
vite_stylesheet_tag(*paths, **helperOptions(options))
end
rescue
end
def helperOptions(options)
if ViteRuby.new.dev_server_running?
{ host: ENV.fetch("VITE_DEV_SERVER_HOST")}.merge(options)
else
options
end
end
end
ポイント② コード中の依存関係の明示
ESMの場合、モジュールが非同期で読み込まれるので、依存関係がコード中に明示されていないと実行順序が変わって壊れることがありました。弊社は、グローバルオブジェクトに値やメソッドを読み込ませる処理があったので、ビルドのチャンク設定やコード上での調整が必要になりました。
ポイント③ CSS ModulesのICSS
JSとCSS間での変数共有のために、ICSSの:export
を利用していたが、うまく動作せず、暫定的に変数をハードコーディングし対応しました。
3-4 Webpackの設定移行
こちらが一番大変な作業でしたが、Webpackの設定項目に関してViteとの対応表を作成し、それらをViteで再現できるように移行しました。
具体的には、Webpackの設定項目に対応するオプションなどをVite、Vite Rails、Rollupのdocumentで調べ、下記のような方法で移行ができているか確認しながら進めていきました。
- PostCSSが読み込まれているか、サンプルプラグインで検証
- CSS中のurl()が読み込まれるかダミー画像で検証
- WebpackとVite Railsでのmanifest.jsonにおけるエントリーポイント数の比較…etc
Webpackの設定項目の大部分はViteで組み込みサポートされており、最終的な設定関連のコードは517行から約100行へと大幅に削減することができました。組み込みサポートされていない設定に関しても、プラグインなどを用いて設定を移行することができました。
WebpackとViteの対応表
Vite組み込みサポートなどで設定したもの
Webpack | Vite | |
---|---|---|
TSファイルのトランスパイル | babel-loader | 組み込みサポート |
cssの分離 | MiniCssExtractPlugin | 組み込みサポート |
scssファイルなどのトランスパイル、DOMへのCSS注入 | sass-loader、postcss-loader、style-loaderなど | 組み込みサポート |
静的アセットのインポート | 組み込みサポート | 組み込みサポート |
モジュール間の依存関係 | resolve.modulesオプション | 組み込みサポート |
CSS圧縮 | CssMinimizerPlugin | 組み込みサポート ※esbuild、terserを選択可 |
JS圧縮 | TerserPlugin | 組み込みサポート ※esbuild、terserを選択可能 |
環境変数の利用 | DotenvWebpack、EnvironmentPluginなど | 組み込みサポート |
環境変数注入 | DefinePluginなど | 組み込みサポート ※vite.config.tsで設定可 |
Tree Shaking | 組み込みサポート | 組み込みサポート |
Split Chunk | optimization.splitChunksオプションなど | build.rollupOptionsオプション (「Production ビルド時のチャンクの設定について」ご参照) |
dev server | devServerオプション | 組み込みサポート |
HMR | 組み込みサポート | 組み込みサポート |
ソースマップ | devtoolオプション | build.sourcemapオプション |
バンドル情報の表示 | statsオプション | logLevelオプション |
マルチエントリーポイント対応 | entryオプション | (Vite Railsの場合)vite.jsonで設定 ※entrypointsDir |
ビルドファイルの出力先 | outputオプション | (Vite Railsの場合)vite.jsonで設定 ※publicOutputDir、assetsDirなど |
プラグインで設定したもの
Webpack | Vite | |
---|---|---|
Vueファイルのトランスパイル | vue-loader | @vitejs/plugin-vue2(Vue2.7のため) |
レガシーブラウザ対応 | babel-loaderなど | @vitejs/plugin-legacy |
静的アセットのgzip圧縮 | compressionPlugin | vite-plugin-compression |
バンドルサイズの分析 | BundleAnalyzerPluginなど | rollup-plugin-visualizerなど |
その他
Webpack | Vite | |
---|---|---|
moduleのpathチェック | CaseSensitivePathsPluginなど | eslintで対応 (eslint-import-resolver-typescript) |
Production ビルド時のチャンクの設定について
ViteではProductionビルドにRollupが使用しており、Rollupでは複数ファイルから参照されるモジュールは個別のESmodulesとして出力されます。MPAでは共通関数などが複数のエントリーポイントから参照されるため、ファイル分割数が増加しリクエスト数も肥大化します。このためmanualChunksで共通ファイルのディレクトリに対して、一つのチャンクファイルが生成されるように設定しました。
本来であればエントリーポイントごとに standalone build を行い、それぞれで利用しているモジュールのみが tree shaking されたファイルを出力してほしいですが、現状ではそれは実現できていない模様です(下記【参考】ご参照)。
元々のWebpackでのビルドファイルのサイズが大きかったこともあり、今回の移行では巨大な共有チャンクファイルを許容しました。本来であれば re-export やファイル名の命名規則などを使って、どのような単位でチャンクファイルが作られるかも考慮に入れて開発する必要になるかと思います。
※SPA の場合はもう少し単純な問題になるかと思います
【参考】
3-5 開発サーバーで HMR を有効化
サンプルアプリで動いていたHMRが動作せず、原因を確認したところ開発環境ではDockerを使用していたのでIPアドレスのマッピングが必要になりました。
Vite Railsでは開発サーバーが動いている場合にHMRを有効にするため、hostとportの指定などをすることで、HMRを動かすことができました
(※)「SimpackerのメソッドをVite用に置き換える」のTag Helpersでパスを注入する
3-6 ローカル環境と qa 環境でテスト
最後に細かいエラーを修正しながら、全体の動作確認テストを行いました。
4 移行作業で得た個人的な学び
この移行作業を経て得た私の学びですが、「エンジニアになって1年も経っていないから、、」などと自分にフタをせずに、ストレッチしたチャレンジをした結果、下記のような貴重な経験を積むことができ、アプリケーションの動作に対する理解を深めることができました。
「移行することができるのか?」という不安が常にありましたが、結果はどうなろうと、エンジニアとしての視野が広がりました。
- サンプルアプリと本番プロジェクトでのVite Railsの動作比較
- Vite、Vite Railsのソースコードリーディング
- DevTool、Linuxコマンドなどを用いた問題特定(何回もしました)
- 置き換え前のWebpack設定の理解
- Webpack設定移行
- Vite、Vite Rails、RollupなどのDocument確認.....etc
5 まとめ
検証結果としては、下記のように運用歴の長いアプリでのVite移行は大変な一方で、開発体験は大幅に向上します。
- 新規PJへのVite導入は比較的簡単だが、運用歴の長いアプリでの移行は大変
- 開発サーバー起動時間の改善(2分弱→約3秒)、HMRの即時反映など開発者体験は大幅に向上
また、移行作業を通して、下記のようなアプリ全体の課題を発見できたのが、この作業で獲得した大きなメリットかと考えています。
- Railsのテンプレートエンジンに依存した密結合な設計
- コンポーネントのスクリプト言語、エイリアス記法などが不統一
- ICSSなどのようなwebpackに依存した標準的ではない書き方
- グローバルオブジェクトを使って、依存関係が明示されていない書き方
今後、Viteのリリースを行い、またこれらの課題も改善することで、より良いアプリケーションにしていきたいと思います。
Discussion