レガシーなMPAアプリケーションをWebpackからViteに移行する話

2022/10/16に公開約7,800字

どうも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つのステップに分けて作業を進めていきました。

  1. サンプルアプリで Vite Railsを触る
  2. 一つのエントリーポイントでバンドルし画面表示
  3. 全エントリーポイントでバンドルし画面表示
  4. Webpackの設定移行
  5. 開発サーバーで HMR を有効化
  6. ローカル環境と 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'

サンプルアプリの作り方は下記をご覧ください。

https://zenn.dev/oreo2990/articles/fe8160437ed089

3-2 一つのエントリーポイントでバンドルし画面表示

続いて、実際のアプリケーションコードにVite Railsを導入しました。なるべくシンプルな状態から順番に導入を進めていきたかったので、まずは一つのエントリーポイントで、バンドルを行い画面が表示されるまで実装を進めていきました。

config/vite.jsonでエントリーポイントを一つに設定。

"all": {
 "sourceCodeDir": "frontend",
 "entrypointsDir": "entry/home",
 "watchAdditionalPaths": []
 },

Vite Railsが用意しているTag Helpersをテンプレートエンジンで読み込ませてることで、画面の表示が可能となりました。

また、この時、とりあえず画面を表示する為に、requireimportに変更、process.envimport.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 の場合はもう少し単純な問題になるかと思います

【参考】

https://twitter.com/mizdra/status/1393887457674862598

https://github.com/rollup/rollup/issues/2756

https://aligach.net/diary/2021/0529/

https://ja.vitejs.dev/guide/build.html

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

ログインするとコメントできます