GameWith Developer Blog

GameWith のエンジニア、デザイナーが技術について日々発信していきます。

4年運用した Vue2 を幾度の挫折を乗り越えて Vue3 にアップグレードした(vite に移行した)話 #GameWith #TechWith

はじめに

こんにちは。GameWith のエンジニアの tiwu です。

今回は Vue.js, Typescript で開発しているダッシュボードの Vue 3 へのアップグレードについて書いていきたいと思います。

ダッシュボードについては以下の記事を参照してください。

tech.gamewith.co.jp

tech.gamewith.co.jp

tech.gamewith.co.jp

挫折

タイトルに書いてあるとおり Vue3 へのアップグレードを何回かチャレンジしていました。

一気に Vue3 にアップグレードしようとした結果、依存関係の影響で全てを一気にアップグレードする必要があり対応の難易度が跳ね上がって毎回挫折してました・・・。

特に vue-cli のプラグイン周りの依存関係の解消が大変でした。

今回ステップを踏んでアップグレードしたおかげで、無事 Vue3 にアップグレードすることができました!

Vue 3 への移行の手順

Vue 3 へのアップグレードは 3 つのステップで行いました。

  1. Vue 2.6.12 から 2.7.16 にアップグレード
  2. vue-cli から vite に移行
  3. Vue 2.7.16 から から 3.2.25 にアップグレード

それぞれ紹介します。

Vue 2.6.12 から 2.7.16 にアップグレード

まず初めに Vue を2系の最新バージョンにアップグレードしました。

依存関係

アップグレード前

  • vue: "2.6.12"
  • @vue/composition-api: "1.0.0-beta.14"
  • vue-template-compiler: "2.6.12"
  • typescript: "4.6.4"
  • vue-router: "3.4.5"
  • vuedraggable: "2.24.3"
  • vuejs-datepicker: "1.6.2"
  • @vue/test-utils: "1.1.0"
  • @vue/vue2-jest: "27.0.0-alpha.3"
  • bootstrap-vue: "2.21.2"
  • vue-i18n: "8.25.0"
  • @vue/cli-service: "5.0.8"
  • @vue/cli-plugin-babel: "5.0.8"
  • @vue/cli-plugin-eslint: "5.0.8"
  • @vue/cli-plugin-router: "5.0.8"
  • @vue/cli-plugin-typescript: "5.0.8"
  • @vue/cli-plugin-unit-jest: "5.0.8"
  • @vue/cli-service: "5.0.8"

アップグレード後

  • vue: "2.7.16"
  • vue-template-compiler: "2.7.16"
  • typescript: "4.6.4"
  • @vue/cli-service: "5.0.8"
  • vue-router: "3.4.5"
  • vuedraggable: "2.24.3"
  • vuejs-datepicker: "1.6.2"
  • @vue/test-utils: "1.1.0"
  • @vue/vue2-jest: "27.0.0-alpha.3"
  • bootstrap-vue: "2.21.2"
  • vue-i18n: "8.25.0"

@vue/composition-api を削除し、vuevue-template-compiler を 2.7.16 にアップグレードしました。

主な修正箇所

@vue/composition-api の削除

import VueCompositionApi from '@vue/composition-api'
Vue.use(VueCompositionApi)

from '@vue/composition-api'from 'vue' に変更

アップグレード前

import { defineComponent, getCurrentInstance } from '@vue/composition-api';

アップグレード後

import { defineComponent, getCurrentInstance } from 'vue';

root.$routeroot.proxy.$route に変更

アップグレード前

const gameId = Number(root.$route.params.gameId);

アップグレード後

const gameId = Number(root.proxy.$route.params.gameId);

root の型定義を SetupContext['root'] から Vue に変更

アップグレード前

const useHoge = (root: SetupContext['root']) => {
}

const root = getCurrentInstance();
useHoge(root);

アップグレード後

const useHoge = (root: Vue) => {
}

const root = getCurrentInstance();
useHoge(root.proxy);

補足など

パスなどの修正のため対象ファイル数はかなり多かったですが、一括置換で対応を行ったため作業時間はそれほどかかりませんでした。

vue-cli から vite に移行

次に vue-cli から vite に移行しました。

依存関係

アップグレード前

  • vue: "2.7.16"
  • vue-template-compiler: "2.7.16"
  • typescript: "4.6.4"
  • vue-router: "3.4.5"
  • vuedraggable: "2.24.3"
  • vuejs-datepicker: "1.6.2"
  • @vue/test-utils: "1.1.0"
  • @vue/vue2-jest: "27.0.0-alpha.3"
  • bootstrap-vue: "2.21.2"
  • vue-i18n: "8.25.0"
  • @vue/cli-service: "5.0.8"
  • @vue/cli-plugin-babel: "5.0.8"
  • @vue/cli-plugin-eslint: "5.0.8"
  • @vue/cli-plugin-router: "5.0.8"
  • @vue/cli-plugin-typescript: "5.0.8"
  • @vue/cli-plugin-unit-jest: "5.0.8"

アップグレード後

  • vue: "2.7.16"
  • vue-template-compiler: "2.7.16"
  • typescript: "4.6.4"
  • vue-router: "3.4.5"
  • vuedraggable: "2.24.3"
  • vuejs-datepicker: "1.6.2"
  • @vue/test-utils: "1.1.0"
  • @vue/vue2-jest: "27.0.0-alpha.3"
  • bootstrap-vue: "2.21.2"
  • vue-i18n: "8.25.0"
  • vite: "5.2.10"
  • vite-plugin-env-compatible: "2.0.1"
  • @vitejs/plugin-vue2: "2.3.1"
  • @vitejs/plugin-vue2-jsx: "1.1.1"
  • @babel/preset-env: "7.24.5"

@vue/cli 関連を削除し、vite, vite-plugin-env-compatible, @vitejs/plugin-vue2, @vitejs/plugin-vue2-jsx, @babel/preset-env を追加しました。

主な修正箇所

コマンドの変更

アップグレード前

{
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "test:unit": "vue-cli-service test:unit",
    "lint": "vue-cli-service lint --no-fix",
  }
}

アップグレード後

{
  "scripts": {
    "serve": "vite",
    "build": "vite build",
    "test:unit": "jest -- --mode test",
    "lint": "eslint src --no-fix",
  }
}

環境変数の修正

アップグレード前

VUE_APP_HOGE=XXXXX

アップグレード後

VITE_HOGE=XXXXX

babel.config.js の修正

アップグレード前

module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset'
  ]
};

アップグレード後

module.exports = {
  presets: [
    '@babel/preset-env'
  ]
};

jest.config.js の修正

アップグレード前

module.exports = {
  preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel',
};

アップグレード後

module.exports = {
  preset: 'ts-jest',
  moduleNameMapper: {
    '@/(.*)': '<rootDir>/src/$1',
  },
  testEnvironment: 'jsdom',
  transform: {
    '.*\\.(vue)$': '@vue/vue2-jest'
  }
};

tsconfig.json の修正

アップグレード前

{
  "compilerOptions": {
    "types": [
      "webpack-env",
      "jest",
      "jquery",
      "bootstrap-vue/esm",
    ],
  },
}

アップグレード後

{
  "compilerOptions": {
    "types": [
      "vite/client",
      "jest",
      "jquery",
      "bootstrap-vue/esm",
    ],
  },
}

index.html の修正

アップグレード前 public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>hoge</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

アップグレード後 index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>hoge</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

App.vue の修正

アップグレード前

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
});
</script>

アップグレード後

<template>
  <router-view/>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
});
</script>

vue.config.js の削除、vite.config.ts の追加

アップグレード前 vue.config.js

const WebpackAssetsManifest = require('webpack-assets-manifest');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

let publicPath = '/dist/';
if (process.env.IS_BUILD) {
  publicPath = 'https://XXXXX.s3.amazonaws.com/dist/';
}

module.exports = {
  parallel: process.env.CI ? false : require('os').cpus().length > 1,
  publicPath: publicPath,
  outputDir: './dist',
  filenameHashing: true,
  productionSourceMap: false,
  css: {
    extract: false,
    sourceMap: true
  },
  configureWebpack: config => {
    config.plugins = config.plugins.concat(
      new WebpackAssetsManifest({
        output: 'asset-manifest.json'
      })
    );
    config.plugins = config.plugins.concat(
      new CleanWebpackPlugin({
        cleanOnceBeforeBuildPatterns: ['dist/*']
      })
    );
    config.devtool = 'eval-source-map';
    config.resolve.fallback = {
      crypto: false,
    };
  },
  chainWebpack: config => {
    config.plugins.delete('html');
    config.plugins.delete('preload');
    config.plugins.delete('prefetch');
  },
  devServer: {
    host: 'localhost',
    port: 9000,
    hot: true,
  },
  pluginOptions: {
    proxy: {
      context: (pathname) => {
        return !pathname.startsWith('/dist') && pathname.startsWith('/');
      },
      options: {
        target: `https://XXXX.jp`,
        secure: false,
        changeOrigin: true,
        autoRewrite: true,
        cookieDomainRewrite: 'localhost',
        xfwd: true,
        router: {
          '/hoge': `http://YYY.jp/hoge`,
        },
      },
    },
  }
};

アップグレード後 vite.config.ts

import { defineConfig, loadEnv } from 'vite';
import vue from '@vitejs/plugin-vue2';
import vueJsx from '@vitejs/plugin-vue2-jsx';
import envCompatible from 'vite-plugin-env-compatible';
import path from 'path';

export default defineConfig(({ command, mode }) => {
  const env = loadEnv(mode, process.cwd());

  let base = '/';
  if (command === 'build') {
    base = 'https://XXXXX.s3.amazonaws.com/dist/';
  }

  return {
    base,
    outputDir: './dist',
    plugins: [
      vue(),
      vueJsx(),
      envCompatible({
        prefix: 'VITE',
        mountedPath: 'process.env'
      })
    ],
    resolve: {
      alias: {
        '@': path.resolve(__dirname, './src')
      }
    },
    build: {
      rollupOptions: {
        output: {
          entryFileNames: 'assets/[name].js',
          chunkFileNames: 'assets/[name].js',
          assetFileNames: 'assets/[name].[ext]'
        }
      }
    },
    server: {
      port: 9000,
      proxy: {
        '/hoge': {
          target: `http://XXXX.jp/hoge`,
          secure: false,
          changeOrigin: true,
          autoRewrite: true,
          cookieDomainRewrite: 'localhost',
          xfwd: true,
        },
      }
    }
  };
});

補足など

開発しているダッシュボードはローカルでは PHP でサーバーを立ち上げつつ vite(vue-cli-serve) でもサーバーを立ち上げています。 開発・本番環境では PHP でダッシュボードサーバーを立ち上げ、js や css は S3 にアップロードしています。

この変則編成のため、vite への移行の際は config ファイルの設定の移行に手間取りました。

Vue 2.7.16 から から 3.2.25 にアップグレード

最後に Vue 2.7.16 から 3.2.25 にアップグレードしました。

依存関係

アップグレード前

  • vue: "2.7.16"
  • vue-template-compiler: "2.7.16"
  • typescript: "4.6.4"
  • vue-router: "3.4.5"
  • vuedraggable: "2.24.3"
  • vuejs-datepicker: "1.6.2"
  • @vue/test-utils: "1.1.0"
  • @vue/vue2-jest: "27.0.0-alpha.3"
  • bootstrap-vue: "2.21.2"
  • vue-i18n: "8.25.0"
  • vite: "5.2.10"
  • vite-plugin-env-compatible: "2.0.1"
  • @vitejs/plugin-vue2: "2.3.1"
  • @vitejs/plugin-vue2-jsx: "1.1.1"
  • @babel/preset-env: "7.24.5"

アップグレード後

  • vue: "3.2.25"
  • @vue/compiler-sfc: "^3.2.25"
  • typescript: "4.6.4"
  • vue-router: "4.3.2"
  • vuedraggable: "4.1.0"
  • @vue/compat: "3.2.25"
  • @vuepic/vue-datepicker: "8.7.0"
  • @vue/test-utils: "2.4.6"
  • @vue/vue3-jest: "27.0.0"
  • bootstrap-vue: "2.23.1"
  • vue-i18n: "9.13.1"
  • vite: "5.2.10"
  • vite-plugin-env-compatible: "2.0.1"
  • @vitejs/plugin-vue: "^5.0.5"
  • @vitejs/plugin-vue-jsx: "^4.0.0"
  • @babel/preset-env: "7.24.5"

vue, vue-router, vuedraggable, @vue/test-utils, bootstrap-vue, vue-i18n をアップグレードしました。

また、

  • vuejs-datepicker の代わりに @vuepic/vue-datepicker
  • @vue/vue2-jest の代わりに @vue/vue3-jest
  • @vitejs/plugin-vue2 の代わりに @vitejs/plugin-vue
  • @vitejs/plugin-vue2-jsx の代わりに @vitejs/plugin-vue-jsx
  • vue-template-compiler の代わりに @vue/compiler-sfc

を追加しました。

主な修正箇所

jest.config.js の修正

アップグレード前

module.exports = {
  transform: {
    '.*\\.(vue)$': '@vue/vue2-jest'
  }
};

アップグレード後

module.exports = {
  transform: {
    '.*\\.(vue)$': '@vue/vue3-jest'
  }
};

vite.config.ts の修正

アップグレード前

import vue from '@vitejs/plugin-vue2';
import vueJsx from '@vitejs/plugin-vue2-jsx';

export default defineConfig(({ command, mode }) => {
  return {
    plugins: [
      vue(),
      vueJsx(),
    ],
    resolve: {
      alias: {
        '@': path.resolve(__dirname, './src')
      }
    },
  };
});

アップグレード後

import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';

export default defineConfig(({ command, mode }) => {
  return {
    plugins: [
      vue({
        template: {
          compilerOptions: {
            compatConfig: {
              MODE: 2
            }
          }
        }
      }),
      vueJsx(),
    ],
    resolve: {
      alias: {
        '@': path.resolve(__dirname, './src'),
        vue: '@vue/compat'
      }
    },
  };
});

vue-router を利用したテストの修正

アップグレード前

import { mount } from '@vue/test-utils';
import List from '@/components/pages/list.vue';

describe('list.vue', () => {
  afterEach(() => {
    jest.resetAllMocks();
  });

  it('一覧が表示されている', async() => {
    const $route = {
      query: {
        startDate: '2020-03-25',
        endDate: '2020-04-01'
      },
    };
    const wrapper = mount(List, {
      mocks: {
        $route,
      },
    });
    expect(wrapper.html()).toMatchSnapshot();
  });
});

アップグレード後

import { mount } from '@vue/test-utils';
import { createRouter, createWebHistory } from 'vue-router';
import List from '@/components/pages/list.vue';

const routes = [
  { path: '/',
    name: 'list',
    component: List
  }
];
const router = createRouter({
  history: createWebHistory(),
  routes,
});

describe('list.vue', () => {
  afterEach(() => {
    jest.resetAllMocks();
  });

  it('一覧が表示されている', async() => {
    router.push({ path: '/', query: { startDate: '2020-03-25', endDate: '2020-04-01' } });
    await router.isReady();
    const wrapper = mount(List, {
      global: {
        plugins: [router]
      },
    });
    expect(wrapper.html()).toMatchSnapshot();
  });
});

jsx の修正:イベントハンドラ

アップグレード前

export default defineComponent({
  render() {
    return (
      <div>
        <form vOn:submit_stop_prevent={this.onSubmit}>
          <input type="text" />
        </form>
        <button vOn:click={this.onClick}>button</button>
      </div>
    )
  }
});

アップグレード後

export default defineComponent({
  render() {
    return (
      <div>
        <form onSubmit={this.onSubmit}>
          <input type="text" />
        </form>
        <button onClick={this.onClick}>button</button>
      </div>
    )
  }
});

jsx の修正:スロット

アップグレード前

export default defineComponent({
  render() {
    return (
      <custom-modal>
        <div>
          <p>hoge</p>
          <p>fuga</p>
        </div>
        <template slot="modal-footer">
          <p>piyo</p>
        </template>
      </custom-modal>
    )
  }
});

アップグレード後

export default defineComponent({
  render() {
    return (
      <custom-modal>
       {{
          default: () => {
            return (
              <div>
                <p>hoge</p>
                <p>fuga</p>
              </div>
            )
          }
          'modal-footer': () => {
            return (
              <p>piyo</p>
            )
          }
       }}
      </custom-modal>
    )
  }
});

補足など

2020年に Vue を導入した際に、Vue3 に備えて @vue/composition-api を導入していたため Vue 自体の修正箇所は少なかったです。

一番苦労した点としては jsx + vuedraggable の組み合わせ箇所の修正でした。

jsx のスロットは vite + vue3 + jsx の例が世の中的に少なく、実装にかなり時間がかかりました。

さらにこの jsx 内で vuedraggable を利用しており、vuedraggable のアップグレードの修正にも時間がかかりました。

おわりに

Vue3 にアップグレード後 eslint のアップグレードや jest から vitest 移行なども実施しており、これらの対応のおかげで Circle CI の時間がかなり短縮されました。

こちらについてはまた別のブログで紹介しようと思います!