Vue2系からVue3系への移行における苦労話

はじめまして

サービス開発統括本部 ソリューション開発部で農業プロダクトの開発を担当している西村です。初めてのTECH BLOGです。 私は現在、ピンポイントタイム散布 (以下、PTS)というサービスで使用するアプリの開発を担当しています。今回の記事では農業要素はあまりないですが、 農業プロダクトで使用されている技術について触れていきます。

この記事について

我々農業チームではフロントエンドでVue.jsを利用しています。Vue2系は2023 年 12 月 31 日に End of Life に到達します。

Vue.js 公式ドキュメント1より

2022 年 7 月に出荷された Vue 2.7 は、Vue 2 のバージョン範囲における最後のマイナーリリースとなります。Vue 2 は現在メンテナンスモードに移行しており、新しい機能は出荷されませんが、2.7 のリリース日から 18 か月間、重要なバグ修正とセキュリティアップデートが継続されます。これは、Vue 2 が 2023 年 12 月 31 日に End of Life に到達することを意味します。
これは、ほとんどのエコシステムが Vue 3 に移行するための十分な時間を提供するものだと考えています。しかし、セキュリティーおよびコンプライアンス要件を満たす必要がありながら、このスケジュールまでにアップグレードできないチームやプロジェクトがあることも理解しています。私たちは業界の専門家と提携し、そのようなニーズを持つチームのために Vue 2 の拡張サポートを提供しています。もしあなたのチームが 2023 年末以降も Vue 2 を使用する予定であれば、前もって計画を立て、Vue 2 Extended LTS について詳細を学んでください。

これに対応すべく、8月から9月にかけて、Vue2.7からVue3.3に移行しました。その過程での苦労について、チーム全員の意見と共にまとめようと思います。 結論だけ先に伝えると、Vuetify2からVuetify3への移行が最も苦労したとの認識で一致しました!

移行前の環境

  • 移行前の環境は以下の通りです
  • Vue2.7 + Typescript + Vuetify2.6 + Vite3.1 + Pinia2.0で動いていました
  "dependencies": {
    "pinia": "^2.0.22",
    "vue": "^2.7.10",
    "vue-router": "^3.6.5",
    "vuetify": "^2.6.10"
  },
  "devDependencies": {
    "@vue/vue2-jest": "^29.1.1",
    "storybook-builder-vite-vue2": "^0.1.32",
    "vite-plugin-vue2": "^2.0.2",
    "vite": "^3.1.1",
    "vue-template-compiler": "^2.7.10",
    "@vue/eslint-config-prettier": "^6.0.0",
    "@vue/eslint-config-typescript": "^9.1.0",
    "eslint-plugin-vue": "^9.4.0",
    "vuetify-loader": "^1.9.2"
   }
  • プロジェクトの立ち上げ時からComposition API2で書くようにしていました

以下のような書き方です

<script setup>
import { ref, onMounted } from 'vue'

// リアクティブな状態
const count = ref(0)

// 状態を変更し更新トリガーする関数
function increment() {
  count.value++
}

// ライフサイクルフック
onMounted(() => {
  console.log(`The initial count is ${count.value}.`)
})
</script>

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

従来の書き方 (Options API) は以下の通りです

<script>
import { defineComponent } from 'vue';

export default defineComponent({
  data() {
    return {
      count: 1
    }
  },

  methods: {
    // 状態を変更し更新トリガーする関数
    increment() {
      count.value++
    }
  },
  mounted() {
    console.log(`The initial count is ${count.value}.`);
  },
})
</script>

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

移行の流れ

Vue 3.3 移行について

  • 以下を参考にしました
    • Vue 3 移行ガイド3を参考にしました
    • 破壊的変更の対応4
    • 各種関連パッケージのバージョンアップ

破壊的変更の対応について

1. v-model v-bind.syncについて

  • .sync 修飾子 が消えて v-model に変更5
  • v-model の value を modelValueに、更新の emit を update:modelValue に変更6

2. v-on.native 修飾子の削除について

  • v-on の .native 修飾子は削除された7

3. vue-i18n 8.x → 9.x の破壊的変更対応

  • 多言語対応で使用するvue-i18nの使い方が変更された
   // vue-i18n 8.x まで
   import VueI18n from 'vue-i18n';

   const vueI18n = new VueI18n();
   const hoge1 = vueI18n.t('hoge.message').toString();
   const hoge2 = vueI18n.tc('hoge.message');

   // vue-i18n 9.x から
   import { createI18n } from 'vue-i18n';
   const { t } = createI18n();
   const hoge1 = t('hoge.message');

移行時の苦労

担当者曰く

  • Vue 3 のバージョンアップ自体は正直そこまで大変な部分はなかった
    • 下記の点から移行が容易だった
      • PTS は2022年に Vue 2.6 → Vue 2.7 へのバージョンアップ + Vite にビルドツールへの移行を実施したこと
      • 全て Composition API で書くことを徹底したこと
    • ほとんどの対応は公式の移行ガイドに従いつつ、 ESLint で指摘されたことを修正することで移行作業が進む
    • vue-router や vue-i18n なども破壊的変更が入っているが、それも各公式の移行ガイドを見れば基本的に問題ない
  • 農業プロダクトで利用しているGoogle Maps API との相性の悪さが目立つ

    • Google Maps API を使いマップを表示しながらVue Devtools を操作すると、タブがほぼ 100 % フリーズするようになった
      • こちらの問題に関しては、ユーザーの利用には影響がありません。
      • 開発者がデバッグする際に使用するツール (Vue Devtools)を使う場合は注意が必要である
    • ポリゴンやマーカーなどの Google Maps API のオブジェクト8 をリアクティブにすると、オブジェクトをマップ上から消せない場合がある
      • Vue2 → Vue3 でオブジェクトのリアクティビティーの仕組みが Proxy 使う形になったことが影響していると考えられる
      const polygon = ref(new google.maps.Polygon());
      // ポリゴン をマップ上に表示
      polygon.value.setMap(map);
      // ポリゴン をマップ上から削除
      // => 本来これで消えるが、ref() でラップしリアクティブオブジェクトになっていると消えない場合がある
      polygon.value.setMap(null);
    
  • 一番苦労したのは Vuetify2系 → Vuetify3系 へのバージョンアップである

とのことで、次章に続きます

Vuetify2 から Vuetify3への移行について

  • 以下の記事を参考に基本的な移行作業を進めました
    • Upgrade Guide9
    • 主力製品の Vue 3 & Vuetify 3 へのマイグレーション全記録10
    • Trying to use VDataTable from labs but getting 'VDataTable' is not exported by node_modules/vuetify/lib/components/index.mjs error11
    • SFC CSS Features12

全体的な変更

  • Upgrade Guideを参考に修正を行いました
    • 対応内容やコンポーネントについて以下に記載します
      • 共通
        • props の valuemodel-value に変更
        • @input@update:model-value に変更
        • background-colorの prop がリネームして bg-color に変更
        • 一部コンポーネントの dense の prop が density="'default' | 'comfortable' | 'compact'" に変更
        • activator={ attrs, on }#activator={ props } に変更され、v-on="on" が削除 & v-bind="attrs" が v-bind="props" に変更
        • filled / outlined / solo の prop が variant の props に統合
        • success と success-messages props は廃止
        • validate-on-blur の prop が validate-on="blur" に変更
      • PTSの中で使っているコンポーネントの中で破壊的変更が入っているもの
        • v-icon
        • v-btn
        • v-menu
        • v-alert
        • v-checkbox
        • v-radio
        • v-switch
        • v-form
        • v-list
        • v-select
        • v-combobox
        • v-autocomplete
        • v-tabs
        • v-img
        • v-menu
        • v-dialog
      • PTSでは使用していないが破壊的変更が入っているもの
        • v-form
        • v-simple-table
        • v-slider
        • v-range-slider
        • v-skeleton-loader
        • v-snackbar
        • v-expansion-panel

完了後に各画面の表示崩れや対応漏れがないかを確認し、仕様書通りに動くように修正しました

移行時、移行後の苦労

  • 既存で使用していたコンポーネントがなくなる
    • 使っていた v-calendarv-datepicker(将来追加予定らしい) が無くなった
    • 他のプラグイン等を調べてもしっくりくるものがないので、自前で作ることになった
  • デザインが崩壊していた
    • v-selectv-autocomplete など、置換しただけではデザインが壊れてまともに使えないコンポーネントが多かった
  • デザインの崩壊を deep セレクタで無理やり直す
    • 壊れている部分を治すために、無理やり css で修正しているので、自前で作り直したくなった
  • linter に任せて修正しても漏れが多い + そもそもVuetifyのコンポーネントの内部実装が丸がわりしていて同じ意味の props を指定しても元のような見た目になっていない
    • 結局移行ガイドを見ながら一つずつ確認して修正していた
    • PTS はまだそこまで大きくないからできたけど、大規模なプロジェクトだと無理だと思う
  • そもそもまだ Vuetify3 に移行完了していないコンポーネントも多くて、自作する羽目になったコンポーネントもあった

移行後の環境

  • 移行後の環境は以下の通りです。
  • Vue3.3 + Typescript + Vuetify3.3 + Vite4.4 + Pinia2.1
  "dependencies": {
    "pinia": "^2.1.4",
    "vue": "^3.3.4",
    "vue-router": "^4.2.4",
    "vuetify": "^3.3.15"
  },
  "devDependencies": {
    "@vue/vue2-jest": "^29.1.1",
    "storybook-builder-vite-vue2": "^0.1.32",
    "@vitejs/plugin-vue": "^4.2.3",
    "vite": "^4.4.1",
    "@vue/compiler-sfc": "^3.1.0"
    "@vue/eslint-config-prettier": "^7.1.0",
    "@vue/eslint-config-typescript": "^11.0.3",
    "eslint-plugin-vue": "^9.15.1",
    "eslint-plugin-vuetify": "^2.0.3",
    "vite-plugin-vuetify": "^1.0.2",
   }

移行後の使用感

  • メンバーからのヒアリングおよび自身の感想です

    • script setupタグを使うことで、コードの記載が楽になりました
      • setup関数ではtemplateタグ内で使用するデータやメソッドをreturnしなければならなかったので、使用されているメソッドが何であるかわかりやすかった。一方で不要なデータやメソッドをreturnしても気づかないため、保守のしにくさにつながる恐れがある
      • script setupタグは上記の問題を解消してくれます。returnしなくてもtemplateタグ内で使用するデータやメソッドを認識してくれるので、何が必要で何が不要かがわかりやすいです。
    • emit処理の定義がわかりやすくなった
      • defineEmitsで定義することで、emitで親に渡すデータの型がわかりやすくなりました。
        const emit = defineEmits<{
          (e: 'change', id: number): void
          (e: 'update', value: string): void
        }>()
      
  • 研修中はOptionsAPIで書いていたので最初はComposition APIのscript setupの書き方に戸惑いましたが、慣れればこちらのほうがわかりやすいと感じました。 特に、optionsだとdataやmoutedごとに書いていたので1つのロジックに関するコードが分散していたのに対し、compositionだとロジックごとに書くことができるので書きやすいです。

  • 純粋に Vue 2 系からの脱却ができた点は嬉しい

    • 調べて出てくる情報は Vue 3系のものが多い
    • Vue 関連パッケージで Vue2 だと対応してないなどを気にしなくて済むようになる
  • Teleport13、Suspense14、トップレベル await15 が使えるように
  • Map や Set でもリアクティビティーが効くようになった
  • 今後 Vue 2 系だからで悩むことが無くなるのは嬉しい

このように移行後のメリットは様々な点でメンバーが感じているようですし、私も感じます。

最後に

我々はVue2からVue3への移行で苦労したというよりは、Vuetify2からVuetify3への移行に苦労しました。ですが、皆様の協力もあって、なんとか対応しました。 メンバーの中には、Vuetify3移行に伴って使えなくなったコンポーネントを自作してくれた方もいます。また、挙動が期待通りにならないコンポーネントを別のコンポーネントで代用してくれた方もいます。技術力が高いと思う方々に囲まれて幸せです。

OPTiMにはフロントエンドだけなく、多方面で高い技術力を持つエンジニアが多く在籍しています。そんな方々と共に「未来により良い影響を与える」プロダクトの開発を進めていきませんか? ご興味のある方は、ぜひ一度ご連絡ください。