既存の Vue2 プロダクトを Vue3 に移行した話

この記事は Vue Advent Calendar 2022 4日目の記事です。

みなさんは Vue.js 使っているでしょうか。
私は2017年頃から jQuery の代わりに使い始めました。
当時、移行先として Angular や React も考えなかったわけではないものの、以下が最終的な決め手になった気がします。

  • 既存の html をほぼそのまま使えること
  • MPA に段階的に導入できること
  • 宣言的にコードを書けること

導入当初は 2.x だった Vue のバージョンですが、先日 3.x へのバージョンアップを無事果たしました。
どういう対応をしたかを振り返りがてらまとめておこうと思います。
「今 Vue 2.x を使っているが、3.x にアップデートすることを検討している」という方の参考になれば幸いです。

基本方針

基本方針は以下のとおりです。

  1. Vue およびそれに関するライブラリのバージョンアップのみ行う
  2. 利用しているビルドツールなどには可能な限り手を加えない
  3. コードもなるべく変更しなくて良い方に倒す

一言で言えば「今回問題になる箇所以外は触らない」です。

Vue およびそれに関するライブラリのバージョンアップのみ行う

Vue に限った話ではありませんが、「このライブラリもついでにバージョン上げよう」みたいなことをするとハマったときに原因が特定しづらくなります。
そのため、あくまでも Vue / Vuex など、Vue に関するライブラリのみバージョンアップすることとしました。

利用しているビルドツールなどには可能な限り手を加えない

該当の Web アプリは MPA で動作していることもあり、カスタマイズのしやすさからビルドツールは Webpack を利用していました。
3.x へのバージョンに伴い設定を見直しても良かったんですが、そうすると途端にやることが増える可能性があったので、ビルドツールは一旦現状のまま動作検証することとしました。

ちなみに、Vue 3.x への移行後に Webpack から Vite への移行も終わらせました。
直接依存するライブラリもかなり減りましたし、何よりビルドがめっちゃ早いです。
これについてもいずれ書きたいと思います。

コードもなるべく変更しなくて良い方に倒す

3.x の特徴のひとつに Composition API があると思います。
現在は Options API を利用しているため、ここを書き換えるべきかも悩みました。
ただ、書き換えは移行後でも出来ると思ったため一旦やらないことにしました。

いざバージョンアップ

  1. 必要なパッケージをアップグレードする
  2. new XXX をファクトリメソッドに変更する
  3. Vue の型定義ファイルを修正する
  4. 各コンポーネントの Vue.extend を defineComponent に変更
  5. 細かい調整

必要なパッケージをアップグレードする

当時は npm i vue@latest では 2.x がインストールされていました。
そのため、アップデートしたいライブラリごとにコマンドをチェックしつつインストールしていきました。

  • 追加
    • @vue/compiler-sfc
    • vue-style-loader
  • 更新
    • vue: 2.x -> 3.x
    • vuex: 3.x -> 4.x
  • 削除
    • vue-template-compiler

vue 系のライブラリは、大体メジャーバージョンを上げれば利用できるようになりそうです。
バージョンを指定せずとも、npm i {library_name}@next とすれば大体最新バージョンをインストールできるようになっています。

new XXX をファクトリメソッドに変更する

これは移行ガイドを見ながらやればすんなり終わりました。

  • new Vue -> createApp
  • new Vuex.Store -> createStore

合わせて、store の登録やマウント先の指定には createApp の戻り値が持つ関数を利用するよう修正しました。

  • 修正前
const App = new Vue({
  store,
  el: '.container',
  // 中略
});
  • 修正後
const app = createApp({
  // 中略
});

app.use(store);
app.mount('.container');

ちなみに、この書き方は Vue 関連の他ライブラリも大体同じでした。
グローバルコンポーネントなら Vue.component ではなく app.component でコンポーネントを登録し、vue-router を使っている場合は new Router ではなく createRouter を使うといった感じです。

Vue の型定義ファイルを修正する

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

Store についても型エラーが出たので、型定義ファイルを追加しました。

import { Store } from 'vuex';

declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    $store: Store<T>;
  }
}

各コンポーネントの Vue.extend を defineComponent に変更

前述のとおり Options API を使っていたものの、事前に TypeScript 化は終わらせていました。
そのため Vue.extend を使ってコンポーネントを定義していたのですが、これの代わりに defineComponent 関数を利用するようにしました。

  • 修正前
import Vue from 'vue';

export default Vue.extend({
  // 中略
});
  • 修正後
import { defineComponent } from 'vue';

export default defineComponent({
  // 中略
});

細かい調整

あとは実際にビルドして出たエラーを1つずつ潰していきました。
といっても webpack.config.js の不要になった alias を削除したぐらいです。

      resolve: {
        extensions: ['.ts', '.vue', '.js'],
        alias: {
-          vue$: 'vue/dist/vue.common',
          '@': scriptsDir,
        },
      },

まとめ

いかがだったでしょうか。
今回のプロダクトでは依存ライブラリもそこまで多くなかったので、1日ぐらいで対応できました。
依存パッケージが多かったりバージョンが古いとその分大変になるとは思います。
とはいえ、今後 3.x へのアップデートを検討されている方の参考になれば幸いです。