🦋

主力製品の Vue 3 & Vuetify 3 へのマイグレーション全記録

nuichi2022/12/09に公開

ロンラン株式会社 CEO 兼 CTO の武部です。

Vue 2 & Vuetify 2 で開発していた当社の主力製品のひとつを Vue 3 & Vuetify 3 へと移行しました。


記念すべき CLOSE済み PR

今現在は、最新の Vuetify 3.0.4 で安定動作しており、開発も Vue 3 & Vuetify 3 を前提としたワークフローへ移行し、快適です。

コード変化量は +21,471 行、-14,585 行。修正ファイル数は 183 ファイルで収まりました。
リードタイムとしては 2 週間程度、作業時間としては、うーん...合計 60 時間ぐらいかな?

フロントエンドエンジニア二名で対応しましたが、95% ぐらいはひとり(私)が対応しています。

事前に、移行を見据えた情報キャッチアップやできる範囲での移行は進めていたため、割とスピーディに対応できた方だと思います。

このページの情報があれば、みなさんのプロジェクトではもっとスムーズにマイグレーションが進むはず!参考にしていただければと思います。

事前準備

製品マイルストーン計画面での調整

当たり前のことではあるのですが、マイグレーション作業中に並行で新機能を追加するのを止めましょう。

『Vue 2, Vuetify 2 側に新機能を追加してそれを都度 Vue 3, Vuetify 3 側へも反映』 ...この作業のためには 2 チーム必要ですし、移行作業の効率はただただ下がります。フロントエンドエンジニア全員を Vue 3 移行に全振りする方がよいと私は思います。

PdM やマネジメントとは事前に「マイグレーション期間中の新機能開発は原則できない」と認識をすり合わせたうえで、製品戦略上いつ移行するのがベストタイミングかを決めましょう。

となると「本当に移行しきれるか」が重要です。たとえば v-data-table に依存している機能があり、代替手段も無い場合は、時期尚早です。ぐっとこらえましょう。

逆に、そういった要素が見つからない場合は、可能な限り早く移行してはいかがでしょうか。 Vue 2 & Vuetify 2 版に機能を追加したら、その分、将来の移行コストが増えるだけですので...

「代替手段」とは

「代替手段も無い場合は」と書いた点について。Vuetify 3 が提供する新しい API やスタイルでは「リリースができなさそうな場合」は、別のコンポーネントを模索する必要が出てくると思います。

怪しいものがある場合は、事前に Awesome Vue サイトなどでアタリを付けておくことをお勧めします。

https://next.awesome-vue.js.org/

もう一点。「リリースができなさそうな場合」と書きましたが、この移行ではリリースブロッカーの見極めも重要になってきます。

多少スタイルが変化したり、やや操作性が劣化したとて、ユーザー体験やビジネスに影響がないなら、リリースブロッカーにすべきではないでしょう。

「Vuetify 2の完全再現」を目指すのは正直難しいと思いますし、長期化します。そもそもこだわる必要もないと思います。このあたりは、UI デザイナーと事前に手を握り合いながら話しておきましょう。

対応を済ませておくべき改修対応

すでに私の過去記事で取り上げていますが、次のこれらは Vue 3 化以前に対応できるものなので、先に終わらせておくことを強くお勧めします。今回の本丸の移行作業をなるべく短時間で終わらせるためです。

  • Vue 2.7, Composition API
  • vue-router/composables(useRouter, useRoute 利用)
  • vue-i18n-bridge(useI18n 利用)
  • beforeRouteEnter, afterRouteEnter を使っている場合、なくなることへの対策
  • ESLint エラー解消

自分の場合は上記に加えて、Options API スタイルをほぼ廃止して setup script スタイルへの移行も済ませておきました。

ESLint エラー解消について

事前のチェックリストに記載していますが、ESLint エラーが出ていないことを最終チェックしておきます。この後の修正中に出てくる ELint エラーには散々助けてもらうことになります。Vuetify 3 向けの修正も、完全ではないにしろ ESLint が代行してくれる部分が多くあります。

その確認・対応中に既知の未解決のエラーがあると、解消すべきエラーなのか、放置してよい既知のエラーなのかの区別が付かなくなってしまいます。

ですので、事前に ESLint エラーは極力解消しておきましょう。

担当分担の戦略

ローカルで Dev サーバが起動できるようになるまでは、また、一律一括置換で沢山のファイルを触るうちは、ひとりで対応を進める方がよいです。中途半端に分業すると Conflict 地獄が待っています...!

ブランチ戦略

この改修作業自体、小さなバッチ方式の意識で進めることをお勧めします。

ブランチについては、main から今回の対応用の branch を切り、GitHub 設定で保護設定しておきます。

ここでは仮に next という名の branch とします。
main <- next の DRAFT PR も作成しておきます。

以後は、git switch -c 作業用branch next で作業用ブランチを作成し、これを次々と next に マージしながら作業を進めてゆきます。

CI/CD 設定は適宜調整・設定してください。エラーがうるさいかもしれませんが、その場合は一次的に無効化しておくとよいでしょう。

さあ、改修対応をはじめよう

っとその前に!この移行作業中に「修正しない方がよい」ことをひとつ。

Vue 2 までは .vue ファイル内の <template> の直下に複数のタグを書けなかったため、適当に wrap するための div などを書いていました。次のようなコード例です。

<template>
  <div>
  ...
  </div>
</template>

「やったこれをやめられるぜ」と修正したくなりますが、こらえましょう。ここを修正すると、ブロック全体が差分となり、修正行差分を確認できなくなってしまいます。wrap しているブロックの削除対応は、すべて終わった後に別作業でちまちまとリファクタリングしましょう。

それと、Vuetify 3 公式のガイドどおりに、空の Vuetify 3 プロジェクトをどこか適当な場所に作成しておくと、主要な設定ファイル(package.json, tsconfig.json, vite.config.ts, index.html など)がどのように変わるか把握できてよいので、お勧めします。

https://next.vuetifyjs.com/en/getting-started/installation/

主要パッケージのアップデート

もちろんプロジェクトによって違いますので、参考情報としてください。当社製品の場合は下記でした。

Vue 2 & Vuetify 2 Vue 3 & Vuetify 3
vue-i18n-bridge 削除
@vitejs/plugin-vue2 @vitejs/plugin-vue
vue-template-compiler 削除
@vue/test-utils@1 @vue/test-utils@2
vue-i18n@8 vue-i18n@9
vue-router@3 vue-router@4
vuex@3 vuex@4
vue-navigation-bar@4 vue-navigation-bar@5
vue2-masonry-wall vue-masonry-wall

これらは、package.json を元に最初に整理しておいた上で、えいやっと変えてしまいましょう(yarn remove してからの yarn add、あるいは npm にて)。

主要なファイルの修正

main.ts 修正

待ちに待ったこの日。main.ts(main.js)内のコードを createApp, app.use() に書き換えてゆきます。

Vue 3 公式ドキュメントの次のページを参考に対応します。私から詳解することはないかなと思いますので割愛します。

https://v3.ja.vuejs.org/guide/instance.html

vue-i18n, vue-router, pinia, vuex など、ご自身が利用しているライブラリについても、公式ドキュメントを読みながら createXxx へ書き換えてゆきます。コードの作りによっては、多少リファクタリングしながらになる場合もあるでしょう。

vue-i18n 修正

vue-i18n に関するその他の修正です。

  • vue-i18n-bridgevue-i18n に一括置換
  • dateTimeFormats -> datetimeFormats に修正必要です

vue-router 修正

vue-router に関するその他の修正です。

  • vue-router/composables は vue-router に一括置換

history モードと scrollBehavior を修正します。 createWebHistory() を import して使う方式に変わっています。次の公式ドキュメントも参考にしてください。

https://router.vuejs.org/guide/essentials/history-mode.html#different-history-modes

参考コードです。

Before

mode: 'history',
scrollBehavior: (to, from, savedPosition) => {
    if (to.hash) return { selector: to.hash }
    if (savedPosition) return savedPosition
    return { x: 0, y: 0 }
},
routes: [
...
]

After

history: createWebHistory(),
scrollBehavior(to, from, savedPosition) {
    if (to.hash) {
        return {
            el: to.hash,
            behavior: 'smooth',
        }
    }
    if (savedPosition) return savedPosition
    return { top: 0 }
},
routes: [
...
]

router 設定において、修正が必要な場所はわずかです。

  • Before: path: '/*'
  • After: path: '/:pathMatch(.*)*'

vite.config.ts 修正

@vitejs/plugin-vue2@vitejs/plugin-vue に変更します。

Before

import vue from "@vitejs/plugin-vue2";

After

import vue from "@vitejs/plugin-vue";

src/vue-shims.d.ts 修正

まるっと書き換えます。

Before

declare module "*.vue" {
  import Vue from "vue";
  export default Vue;
}

After

declare module "*.vue" {
  import type { DefineComponent } from "vue";
  const component: DefineComponent;
  export default component;
}

その他一括置換

beforeDestroybeforeUnmount に一括置換できます。

vue-demi コンテキストスイッチ切り替え

vue-demi を使っているライブラリのコンテキストスイッチを切り替えます。

npx vue-demi-switch 3

Vuetify 3 向けの修正、引き続きひとりで。

Vuetify 3 向けの修正に入ってゆきます。

次の Vuetify 3 ドキュメントのアップグレードガイドをざっくり読みます。どうせこのあと、ESLint エラーが出たり、実行時エラーが出たらこのページを何度も参照することになるので、ざっくりで大丈夫です。

https://next.vuetifyjs.com/en/getting-started/upgrade-guide/

API ページへのリンクがすべて 404 Not Found で臆しますが、次のページにヒントを記しましたので参考にしてください。

https://zenn.dev/nuichi/articles/474b77fd8afd92

【すごく重要】 Vuetify 3 向けの ESLint 設定

eslint-plugin-vuetify、これは移行対応の最重要ツール(設定)です。これがなくては今回の Vuetify 3 アップグレードは成功しないです。

https://github.com/vuetifyjs/eslint-plugin-vuetify

README のとおりに導入しておきます。

yarn add eslint-plugin-vuetify -D

eslinrc.json の extends に設定を追加します。

extends: [
    "plugin:vuetify/base"
]

勇気があれば、ESLint で fix 掛けてしまってもよいですね。私は普段 fix を使っておらず、ちょっと不安だったので、ESLint エラーが出ているファイル(ほぼ全部ですが...)を開いて、確認→保存→確認の作業を繰り返しました。

これによって、アップグレードガイドに書かれていた変更点の多くが自動修正されます。例えば次のようなものです(多すぎて挙げきれない!)。

  • primary--text => text-primary への変換
  • small, large といったコンポーネントプロパティの size="small", size="large" 修正

いろいろ一括置換

ESLint では解決できない仕様変更のうち、文字列の一括置換や、正規表現での一括変換で対応できる箇所をやっつけます。

文字列の一括置換

  • $vuetify.breakpoint.$vuetify.display. に一括置換 ※正規表現置換しないように
  • v-slide-itemv-slide-group-item に一括置換

正規表現での一括置換

  • .sync の修正を正規表現の一括置換で。:([^.]+).sync=v-model:$1= へ置換します。
  • スタイルの darken, lighten のスタイル仕様変更もこれでいけました→ [\s](darken|lighten)- => -$1-
    • これは少し乱暴なので、手修正が必要な箇所もあるでしょう

確認しながらの修正

  • @input@update:Variable (Variable は適切な変数) に修正してゆきます。※ESLint がスタイルだけは修正してくれていますが、Variable は自身でコーディングします

Vuetify 3 向け対応、分担作業開始

このあたりで、Dev サーバ起動してみましょう。スムーズに起動しなかった場合は、ターミナルに出ているエラー関連情報を拾いながら地道に修正を続けます。

「見た目はまだまだ壊れているし、いろいろ JS console にエラー出てるけど Dev サーバが起動した!」これがリード担当者のゴールです🎉

ここからは、分担して作業してゆきます。

テーマ設定

createVuetify() 時のテーマ設定は Vuetify 3 と Vuetify 2 で仕様変化がありますので、見比べながら修正します。

https://next.vuetifyjs.com/en/features/theme/

アイコンフォントの設定

アイコンフォントについては、当社製品においてはインパクトがありました。

当社製品では Line Awesome と Material Design Icons を併用しており、前者を多く使いながら、ないものは後者を使っています。

ですが、Vuetify 3 ではこういったアイコンフォントの併用が暗黙的にできなくなっています。

https://next.vuetifyjs.com/en/features/icon-fonts/

最終的には、上記ページに掛かれている情報を駆使して、カスタムアイコンセットを用意するなどで解決しているものの、Vuetify 3 内部が直接 mdi フォントを使って表示している箇所など、手が出せない箇所もありました。。

ただ、複数のアイコンフォントを併用しているケースが稀だったかもしれないので、ここでは詳しく触れないことにします。

Vuetify 2 の内部 CSS を hack していたような箇所

Vuetify 2 の内部 CSS を hack していたような箇所があれば、表示が崩れているケースが多い... というかほとんど全滅なので、頑張ってください。

当社の場合は Vuetify 3 化を見越してなるべく局所的に使っていたので、修正影響は少なかったり、比較的容易な対応で解決できました。

ヤバすぎる修正量の場合は、UI デザイナーに、デザインシステム自体を修正できないか相談してみては...。

そうはいかないという場合は、あらためてビルトインの SASS variables 活用ができないかや、その部分だけ Vuetify 以外のコンポーネントを使う、あるいは自作するなども視野に検討する必要があるでしょう。

コンポーネントやディレクティブごとの個別対応

v-bottom-sheet はまだない

v-bottom-sheet まだありません。経緯こちらです。

https://github.com/vuetifyjs/vuetify/issues/13466

v3.2.0 で来そうですね。待ってられないので、他のコンポーネントで代替しました。当社製品の場合は、ユースケースごとに次のような設定の v-navigation-drawerv-dialog でまかないました。

<v-navigation-drawer
  v-model="searchDrawer"
  left
  temporary
  floating
  width="100%"
></v-navigation-drawer>
<v-dialog
  v-model="bottomSheet"
  fullscreen
  persistent
  no-click-animation
  transition="dialog-bottom-transition"
></v-dialog>

v-list-item-avatar, v-list-item-content がない

廃止かもしれない?適当に div などに置き換えたり、コード調整をしました。

このタイミングで m3 Material Design サイトのコンポーネント仕様を確認して、本来どういった余白が必要なのかも確認しながら修正しましたが、そもそもいらない要素だったのかもしれないです。

directive の v-intersect の引数の仕様が変化

Intersection Observer を使っている場合、引数が変化しているので修正が必要です。この手のものは、動かしてみて console エラーなどで拾ってゆきます。

Vuetify 2 の intersect API 仕様

onIntersect (entries, observer, isIntersecting) {}

Vuetify 3 の intersect API 仕様

(
  isIntersecting: boolean,
  entries: IntersectionObserverEntry[],
  observer: IntersectionObserver,
)

その他、手ごわいコンポーネント

v-overlay は、今ちょっと壊れているような気もします。結構トライしたのですが無駄な時間に感じてきたので、CSS コードを直接書くなどで回避しました。

v-autocomplete はがらっと変わりましたかね...。少し時間が掛かりました。

v-slider も、仕様変更への追従でいろいろとコード修正が必要でした。

テスト(UT)

もともと vitest でコンポーネントのテストを書いていましたが、router や i18n の注入などをローラー対応修正し、比較的スムーズに終わりました。

動作確認

小さなバッチ方式でどんどん改修してゆきながら、あわせて動作も確認してゆきますが、注意点があります。

動作速度が遅い場合はビルドしてみる

何気ない画面遷移(ルーティング)や、 v-carousel 中の v-img はやたら表示が遅いなどの症状に焦りますが、Dev サーバモードだと動作が遅く、ビルドした後は軽快ということがあります。

ですので、動作速度が気になる場合は、ビルド版でチェックしましょう

当社ではこれがリリースブロッカーだったので、しばらくコンポーネントのコードを追ったり、解析して時間を無駄にしてしまいました👻

事後対応の判断

vue-meta@vueuse/head へ移行するとか、一部シャドウが出ていないスタイル上の問題などは、リリース後でも許容できたので後回しにして、マージとリリースを優先しました。

このあたりの「見切り」ができないと、ズルズルと移行作業が続くため、判断大事です。

おわりに

... なんか振り返ると、半分ぐらいは eslint-plugin-vuetify が修正してくれたような気もします笑

冒頭に書いた通りで、今は本当に快適です。ついに本格的に Vue 3 で楽しく開発できるようになりました。

Vuetify 3 についても、実は心の中には品質懸念を持っていたのですが(爆)、かなり品質高く仕上げてきたなと感心しています。開発チームの努力を労いたいです👏

ちなみに当社は、複数の SPA 製品を開発しており、それらをワークスペース(具体的には yarn workspaces)で管理しています。
今回はそのうちのひとつ、優先度が高く、移行が可能なものだけを対象にしました。
ですので、まだ Vue 2 & Vuetify 2 のままの製品もあります。

このあたり、Vue2, Vuetify 2 と Vue 3, Vuetify 3 をひとつのワークスペースで管理し、動作させることも問題なく出来ているので、必要な方は是非トライしてみてください。

ロンラン Tech Zenn

ロンランからの Zenn 的技術発信。誰かにとって役に立ちそうな気づきやノウハウをどんどん共有します。

Discussion

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