みなさまご機嫌よう、OLTA株式会社でフロントエンドエンジニアをしている林です。
OLTAは請求書や見積書などの帳票を簡単に作成・管理できるINVOYというサービスを提供しています。INVOYのフロントエンドにはNuxt2が使われていましたがEOLに伴いNuxt3に移行したので、移行においてうまくいったことや苦労したことなどについて共有します。
移行方針
Next.jsには移行しない
Nuxt3への移行計画を立て始めていた2023年夏頃には、Next.jsは既にフロントエンドのフレームワークとしてデファクトスタンダードになっていたため、Nuxt2からNext.jsへのフルリプレイスを検討したこともありました。
しかし、TypeScriptとの親和性に加えて、INVOYの今後のグロースを見据えた時にほしいハイブリッドレンダリングなどの機能はNuxt3にも概ね追加されていたことから、
Next.jsへフルリプレイスを行うメリットがそのコストを上回らないと判断し、後述の通りNuxt3への移行を行うこととしました。
Nuxt2からNuxt3へ一気に移行
下記の観点から、Nuxt Bridgeを挟まずにNuxt2からNuxt3へ一気に移行しました。
- Nuxt Bridgeには細かいバグが多くあり、これに1つ1つ対処するのは非効率(公式でも安定性がsemi-stable扱いだった)
- Nuxt2からNuxt Bridgeで1回、Nuxt BridgeからNuxt3で1回の計2回の全体的なテストをする必要があり、テストコストが大きい
ライブラリは積極的にリプレース
下記の観点から、コアとなるようなライブラリは推奨されているものにリプレースしました。(何を何にリプレースしたのかは後述します)
- 推奨されているライブラリは利用者が多いため、情報を得やすい
- リプレースを後でやろうとするとテストコストが大きい(都度全体的なテストが必要)
可能な範囲でComposition API化
下記の観点から、可能な範囲でComposition API化を行いました。
- 移行前から、Composition APIへの移行を進めていきたいと考えていた(よりよい型推論、論理的関心の分離)
- 参考にできるコードを存在させておくことで、今後のOptions APIからComposition APIの移行をスムーズにしたい
移行規模
移行規模はおおよそ下記のようになりました。
主担当者 | 2人 |
---|---|
期間 | 4か月 |
ページ数 | 150ページ |
コンポーネント数 | 300個 | コード差分行 | ±40000行 |
移行前後の環境の違い
アプリケーションのコアとなるライブラリは全てVue / Nuxtの推奨 or 組込のものに移行できました!
ライブラリ種別 | 移行前 | 移行後 |
---|---|---|
Nuxt | v2.15.8 | v3.8.1 |
バンドラ*1 | Webpack | Vite |
HTTPクライアント | axios | oFetch |
ストア | Vuex | Pinia |
技術的に嬉しいこと
Composition APIによる関心の分離
Options APIは設計に関する負担を軽減できる記法ですが、トレードオフとしてコンポーネントが持つ責務の定義が曖昧になりやすいという側面もあるかと思います。
これに対する解決策として、Composition APIは有用な手段だと感じています。
現状、INVOYチームではComposition APIを書く時はロジックがどの関心ごとについてのものかをコメントで残すようにしています。*2
INVOYは帳票管理を行うことができるプロダクトなので、請求書の詳細コンポーネントをComposition API / Options APIで実装したと仮定してサンプルコードを示します。
<script setup> // 請求書の取得 const invoice = ref({}); const fetchInvoice = async () => { // 略 }; // 請求書の編集 const editInvoice = async () => { // 略 }; // 請求書のアーカイブ const archiveInvoice = async () => { // 略 }; // 見積書の取得 -> このコンポーネントにいるべきではない const estimate = ref({}); const fetchEstimate = async () => { // 略 }; // ライフサイクルフック onMounted(async () => { await fetchInvoice(); await fetchEstimate(); }); </script>
これをOptions APIで書くとこうなります。
<script> export default { data() { return { invoice: {}, estimate: {}, }; }, async mounted() { await this.fetchInvoice(); await this.fetchEstimate(); }, methods: { fetchInvoice() { // 略 }, editInvoice() { // 略 }, archiveInvoice() { // 略 }, fetchEstimate() { // 略 }, }, }; </script>
Composition APIのコードの方が、コンポーネントが持つべき責務から逸脱しているロジックがどれなのかが分かりやすくないでしょうか。
このサンプルコードの場合、estimateやfetchEstimateはこのコンポーネントにいるべきではないと考えられます。
依存ライブラリ数の削減
下記の通り、Vue3 / Nuxt3の新機能もしくはVueUse*3を用いることによりプロダクトが依存しているライブラリを減らすことができました。
- モーダルコンポーネントの実装においてPortalVueを利用していたが、Vue3の機能であるTeleportに置き換え
- HTTPクライアントとしてaxiosを利用していたが、Nuxt3に組み込まれているoFetchに置き換え
- cookieに関する処理のためにjs-cookieを、ストアの値の永続化のためにvuex-persistedstateを利用していたが、VueUseに一本化
うまくいったこと
端的に言うと、Nuxt2時点の技術選定が今回のNuxt3への移行コストを下げたと考えています。
具体例としてClass API(vue-class-component*4)を挙げます。
Class APIは、主にVue2 / Nuxt2が主流であった時代にOptions APIとTypeScriptとの親和性の低さをカバーする目的で利用されていたAPIスタイルです。*5
これを採用しているプロジェクトは一定数あり、INVOYでもClass APIを利用するかどうかという議論がありました。
この議論の時点でVue3がリリースされており、Vue3がTypeScriptで書き直されたことを考えるとNuxt2の時点ではOptions APIとJavaScriptで記述しておき、Nuxt3になってからTypeScript化していくのが筋が良いのではないかと結論付けました。
もしこのときにClass APIを利用していたら、今回のNuxt3への移行の際にClass APIからComposition APIへの移行が必要になっていたと考えられます。*6
今回はClass APIを利用していなかったため、Options APIのものはOptions APIのまま移行して、その後徐々にOptions APIからComposition APIへ移行するという戦略を取ることができました。
苦労したこと
Nuxt3がstableになってから1年ほど経ってから移行作業を開始したため、幸いなことに技術的に大きなハードルはありませんでした。
ただし、Nuxt2の時に便利だった機能がいくつかなくなっていたため、少しトリッキーな実装をしないといけなくなったシーンはありました。
例えば、Nuxt2ではfooレイアウトでauthミドルウェアを実行したい場合はこのように記述することができました。
<script> export default { middleware: ['auth'], // 略 }; </script>
Nuxt3ではレイアウトファイルにミドルウェアを記述することはできなくなっているため、期待するレイアウトではない場合はグローバルミドルウェアの中で弾くようにする必要があります。*7
export default defineNuxtRouteMiddleware(async (to, from) => { const isFooLayout = to.meta.layout === 'foo'; if (!isFooLayout) return; });
といったように一部トリッキーな実装は必要でしたが、概ね必要な情報は存在しており大きな障害なくNuxt3への移行を完了することができました。
最後に
ここまで読んでいただきありがとうございます!
ここで耳寄りな情報が2つあります!
OLTAでは一緒に働く仲間を募集しています。下のリンクから応募すると何かいいことがあるとかないとか...
さらに、下のリンクからINVOYの会員登録をすると何かいいことがあるとかないとか... www.invoy.jp
*1:Viteを単にバンドラと表現するのは微妙な気がしますが、Webpackと比較する意味でそう表現しています
*2:Vue.jsの作者であるEvan YouがOptions APIのコンポーネントをComposition APIを使ってリファクタリングしたサンプルコードに影響を受けています FileExplorer.vue · GitHub
*3:VueUse自体はNuxt2の時から利用可能でしたが、ネイティブにComposition APIを利用できるようになったので、VueUseを利用しやすくなりました
*4:Overview | Vue Class Component
*5:Composition API に関するよくある質問 | Vue.js
*6:Nuxt3でClass APIを利用し続けることは可能なのですが、Class APIはもはや推奨されておらず現在ではComposition APIを利用する方がベターだと思われます(Composition API に関するよくある質問 | Vue.js)
*7:Extend Nuxt 3 Middleware usage. · Issue #21454 · nuxt/nuxt · GitHub