Vue 2 から Vue 3 への移行手順まとめ
本記事では、既存のVue 2プロジェクトをVue 3へ移行する際に実施した作業手順と、各種依存関係やツールのアップグレード内容についてまとめます。実際に簡易的なテストアプリを用意し移行をしたおおまかなメモです。
移行前の作業の見積もり
各項目ごとに影響度や作業量の見積もりを行い、全体の工数やリスクがより正確に把握するためにリスト化。
-
画面数・UIコンポーネントの数
- 扱う画面・ページ数
- 各画面で使用されているコンポーネントの数と複雑さ
-
機能数・業務ロジックの複雑度
- 実装されている機能の総数
- 各機能の内部ロジックや相互依存関係
-
利用しているフレームワーク、ライブラリ、プラグインの洗い出し
- 現在使用している主要なフレームワーク・ライブラリのバージョン
- サードパーティのプラグインや独自ライブラリ
-
破壊的変更の度合い
- Vue2からVue3への破壊的変更の影響(Options API→Composition API、ライフサイクルフックの変更など)
- jQuery依存部分の完全な削除と代替実装の必要性
-
依存関係の影響範囲
- jQuery廃止に伴う影響範囲(DOM操作、イベント処理、アニメーション等)
- その他の外部APIやサードパーティサービスとの連携状況
-
既存コードの規模と技術的負債
- コードベースの規模、構造、リファクタリングの必要性
- 技術的負債がどれだけ存在するか
-
テスト環境と自動化テストの整備状況
- 単体テスト、統合テスト、E2Eテストの有無とカバレッジ
- テスト環境が整っているかどうか(移行後のリグレッションテストの容易さ)
-
依存ライブラリ・APIの深さと更新頻度
- 利用している外部ライブラリの更新状況と互換性
- サードパーティAPIとの連携部分の影響
-
チームのスキルセットと学習コスト
- Vue3、TypeScript、Piniaなど新技術への習熟度
- チーム内でのドキュメント整備や知識共有の必要性
-
バックエンドやインフラとの連携
- フロントエンドとバックエンド間の契約や通信方法(Fetch APIへの移行など)
- CI/CDパイプライン、デプロイ設定の変更の必要性
移行した順番(作業順)
-
Vue CLIからViteへの移行
- ビルドツールを従来のVue CLIから高速なViteへ変更
-
vite.config.ts
の作成 -
package.json
のスクリプト更新 -
index.html
の移動と更新
-
TypeScript環境の整備
-
tsconfig.json
とtsconfig.node.json
の設定 -
env.d.ts
の作成 - ファイル拡張子を
.js
から.ts
に変換 - 型定義の追加
-
-
Vue2からVue3への移行
- Vue 3 のインストールと設定
- Options API から Composition API への移行
- コンポーネントの更新
- Vue Router のバージョンアップ
-
状態管理の移行
- Vuex から Pinia への移行
- Pinia ストアの型定義追加
- ストアの実装
-
jQueryの依存関係削除と機能移行
- jQuery UI Datepicker から Vue Datepicker への移行
- fadeIn/fadeOut から Vue Transition への移行
- jQuery イベント処理から Vue ネイティブへの移行
-
AjaxからFetch APIへの移行と非同期処理の更新
- jQuery Ajaxの削除
- Fetch APIの実装
- TypeScriptでの型安全な実装
-
ESLint/Prettierの設定更新
-
.eslintrc.js
の更新 -
prettier.config.js
の作成 - Vite環境に合わせた設定調整
-
最終的な技術スタック
-
Vue 3
Vue 2から移行した最新のVueフレームワーク -
TypeScript
型安全な開発を実現するために新規導入 -
Pinia
Vuexから移行した状態管理ライブラリ -
Vue Router
Vue2版から移行したルーティングライブラリ -
Vue Transition
jQueryのアニメーション処理から移行 -
Fetch API
Ajaxから移行したデータ取得手法 -
Vite
Vue CLIから移行した次世代のビルドツール -
ESLint + Prettier
コード品質とフォーマット管理のための設定をVite版に移行
移行で苦戦しそうな箇所を挙げるとするなら
-
Vue2からVue3への移行
<!-- Options API から Composition API への書き換え --> <script setup lang="ts"> import { ref } from "vue"; import { useMainStore } from "../stores/main"; const store = useMainStore(); const isVisible = ref<boolean>(false);
- 破壊的変更が多い
- コンポーネントの書き方が大きく変更
- ライフサイクルフックの変更
-
VuexからPiniaへの移行
// Vuexの書き方から import { mapState, mapActions } from "vuex"; // Piniaの書き方へ import { defineStore } from "pinia"; interface State { message: string; }
- ストア構造の完全な書き換え
- TypeScriptの型定義追加
-
jQueryからVueネイティブへの移行
<!-- jQueryのDOM操作から --> $("#myDiv").fadeIn(); <!-- Vue Transitionへ --> <Transition> <div v-if="isVisible" class="fade-box">
- DOM操作の考え方の変更
- アニメーション実装の書き換え
-
TypeScript対応
- 型定義の追加
- 既存コードの型付け
- エラー解決
-
Vue CLIからViteへの移行
- ビルド設定の変更
- 開発環境の再構築
特に1-3は、コードの考え方自体を変更する必要があるため、単なる書き換えではなく、アプリケーションの設計思想の変更が必要になる。
資料を読むだけではなく、実際にコードやエラーに触れることで問題を確認する試みとなりました。
移行のためのテストアプリの画像
移行に関するより詳しいメモ
実際の移行時には環境依存のプラグインや各ライブラリの互換性、型定義の細かい調整、ライフサイクルやルーターの挙動に注意が必要。
1.Vue CLIからViteへの移行
- package.jsonのスクリプトを更新:
{
"scripts": {
// Vue CLI
- "serve": "vue-cli-service serve",
- "build": "vue-cli-service build",
- "lint": "vue-cli-service lint"
// Vite
+ "dev": "vite",
+ "serve": "vite",
+ "build": "vue-tsc && vite build",
+ "preview": "vite preview",
+ "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
+ "format": "prettier --write src/"
}
}
- Vite設定ファイルの作成:
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import path from "path";
export default defineConfig({
plugins: [vue()],
base: "/",
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
server: {
port: 5173,
open: true,
},
});
- index.htmlの移動と更新:
- Vue CLI:
public/index.html
- Vite: プロジェクトルートの
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue 3 App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
- 依存関係の更新:
{
"devDependencies": {
// Vue CLI関連を削除
- "@vue/cli-plugin-babel": "~5.0.0",
- "@vue/cli-plugin-eslint": "~5.0.0",
- "@vue/cli-plugin-typescript": "~5.0.0",
// Vite関連を追加
+ "@vitejs/plugin-vue": "^3.2.0",
+ "vite": "^3.2.7"
}
}
- vue.config.jsの削除:
- Vue CLI用の設定ファイルを削除し、Vite用の設定に移行
この変更により:
- より高速な開発サーバー
- より効率的なHMR(ホットモジュールリロード)
- モダンなビルドツールチェーンへの移行
が実現された。
2.TypeScript環境の整備
- tsconfig.jsonの設定:
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"moduleResolution": "node",
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"useDefineForClassFields": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"node",
"vite/client",
"vue"
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}
- env.d.tsの型定義:
/// <reference types="vite/client" />
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<
Record<string, never>, // props
{ // methods
fetchData: () => void;
showDiv: () => void;
hideDiv: () => void;
handleClick: () => void;
},
{ // data
store: ReturnType<typeof import('./stores/main').useMainStore>;
date: Date;
isVisible: boolean;
isHighlighted: boolean;
}
>;
export default component;
}
- Vueコンポーネントの型定義:
<script setup lang="ts">
import { ref } from "vue";
import { useMainStore } from "../stores/main";
const store = useMainStore();
const date = ref<Date>(new Date());
const isVisible = ref<boolean>(false);
const isHighlighted = ref<boolean>(false);
const fetchData = () => {
store.fetchMessage();
};
</script>
- Piniaストアの型定義:
import { defineStore } from "pinia";
interface State {
message: string;
}
export const useMainStore = defineStore("main", {
state: (): State => ({
message: "",
}),
actions: {
async fetchMessage() {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts/1"
);
const data = await response.json();
this.message = data.title;
} catch (error) {
console.error("Error:", error);
}
},
},
});
主なポイント:
-
厳格な型チェック:
-
strict: true
の設定 -
noImplicitAny
の有効化
-
-
Vue3の型サポート:
-
DefineComponent
の使用 -
ref
やreactive
の型定義
-
-
コンポーネントの型安全性:
-
props
の型定義 -
methods
の戻り値の型定義 -
data
の型定義
-
-
ストアの型安全性:
-
State
インターフェースの定義 - アクションの戻り値の型定義
-
これらの設定により、型安全な開発環境が整備された。
3.Vue2からVue3への移行
- Vue 3のインストールと設定:
{
"dependencies": {
"@vue/runtime-dom": "^3.3.0",
"vue": "^3.3.0",
"vue-router": "4.2.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^3.2.0",
"@vue/tsconfig": "^0.5.1"
}
}
- Options APIからComposition APIへの移行:
Before(Vue2 Options API):
<script>
export default {
data() {
return {
isVisible: false,
date: new Date()
}
},
methods: {
showDiv() {
$("#myDiv").fadeIn();
},
hideDiv() {
$("#myDiv").fadeOut();
}
},
computed: {
...mapState(["message"])
}
}
</script>
After(Vue3 Composition API):
<script setup lang="ts">
import { ref } from "vue";
import { useMainStore } from "../stores/main";
import VueDatePicker from "@vuepic/vue-datepicker";
const store = useMainStore();
const date = ref<Date>(new Date());
const isVisible = ref<boolean>(false);
const showDiv = () => {
isVisible.value = true;
};
const hideDiv = () => {
isVisible.value = false;
};
</script>
- テンプレートの更新:
<template>
<div>
<h2>Test Vue3 + TypeScript + Pinia + Vue Transition + Fetch API</h2>
<h1>{{ store.message }}</h1>
<!-- Vue Datepickerの使用 -->
<div>
<label for="datepicker">Select Date:</label>
<VueDatePicker v-model="date" />
</div>
<!-- Vue Transitionの使用 -->
<Transition>
<div v-if="isVisible" class="fade-box">This is a div</div>
</Transition>
</div>
</template>
- Vue Routerのバージョンアップ:
import type { RouteRecordRaw } from "vue-router";
import * as VueRouter from "vue-router";
import HomeView from "../views/HomeView.vue";
const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "home",
component: HomeView,
},
{
path: "/about",
name: "about",
component: () => import("../views/AboutView.vue"),
},
];
const router = VueRouter.createRouter({
history: VueRouter.createWebHistory("/"),
routes,
});
export default router;
主な変更点:
-
Composition API
-
setup
スクリプトの使用 -
ref
やreactive
による状態管理 - コンポーネントのインポート方法の変更
-
-
TypeScript統合
- 型定義の追加
- 型安全なコンポーネント設計
-
新機能の活用
-
<Transition>
コンポーネント - Composition APIのユーティリティ
- TypeScriptのサポート
-
-
ルーティング
- Vue Router 4への更新
- 型安全なルート定義
これらの変更により、より型安全で保守性の高いコードベースとなった。
4.状態管理の移行
1. VuexからPiniaへの移行
Before(Vuex
// store/index.ts
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
message: ''
},
mutations: {
setMessage(state, message) {
state.message = message
}
},
actions: {
async fetchMessage({ commit }) {
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1')
const data = await response.json()
commit('setMessage', data.title)
}
}
})
After(Pinia)
import { defineStore } from "pinia";
interface State {
message: string;
}
export const useMainStore = defineStore("main", {
state: (): State => ({
message: "",
}),
actions: {
async fetchMessage() {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts/1"
);
const data = await response.json();
this.message = data.title;
} catch (error) {
console.error("Error:", error);
}
},
},
});
2. コンポーネントでのストア使用
Before(Vuex)
<script>
import { mapState, mapActions } from 'vuex'
export default {
computed: {
...mapState(['message'])
},
methods: {
...mapActions(['fetchMessage'])
}
}
</script>
After(Pinia)
<script setup lang="ts">
import { useMainStore } from "../stores/main";
const store = useMainStore();
const fetchData = () => {
store.fetchMessage();
};
</script>
<template>
<div>
<h1>{{ store.message }}</h1>
<button @click="fetchData">Fetch Data</button>
</div>
</template>
主な変更点
-
ストア定義の変更
-
defineStore
を使用し、Vuexのような煩雑な設定が不要になりました。 - TypeScriptインターフェース (
State
) を導入して型安全性を向上。
-
-
型安全性の向上
-
State
インターフェースの定義により、ストア内の状態管理が明確になりました。 - アクションの戻り値にも型定義を適用できるため、コンポーネント側での型推論が可能に。
-
-
シンプルな実装
- ミューテーションが不要となり、直接状態を変更できます。
- コンポーネントでの使用が簡潔になり、コードの可読性が向上。
-
開発体験の改善
- 自動補完の強化や型チェックの強化により、開発効率が向上。
- デバッグが容易になりました。
これらの変更により、より型安全で保守性の高い状態管理となった。
5.jQueryの依存関係削除と機能移行
- jQuery UI DatepickerからVue Datepickerへの移行:
Before(jQuery UI):
<template>
<div>
<label for="datepicker">Select Date:</label>
<input type="text" id="datepicker" />
</div>
</template>
<script>
export default {
mounted() {
$("#datepicker").datepicker({
dateFormat: "yy-mm-dd",
changeMonth: true,
changeYear: true,
});
}
}
</script>
After(Vue Datepicker):
<template>
<div>
<label for="datepicker">Select Date:</label>
<VueDatePicker v-model="date" />
</div>
</template>
<script setup lang="ts">
import VueDatePicker from "@vuepic/vue-datepicker";
import "@vuepic/vue-datepicker/dist/main.css";
const date = ref<Date>(new Date());
</script>
- fadeIn/fadeOutからVue Transitionへの移行:
Before(jQuery):
<template>
<div>
<button @click="showDiv">Show</button>
<button @click="hideDiv">Hide</button>
<div id="myDiv" class="fade-box">This is a div</div>
</div>
</template>
<script>
export default {
methods: {
showDiv() {
$("#myDiv").fadeIn();
},
hideDiv() {
$("#myDiv").fadeOut();
}
}
}
</script>
After(Vue Transition):
<template>
<div>
<button @click="showDiv">Show</button>
<button @click="hideDiv">Hide</button>
<Transition>
<div v-if="isVisible" class="fade-box">This is a div</div>
</Transition>
</div>
</template>
<script setup lang="ts">
const isVisible = ref<boolean>(false);
const showDiv = () => {
isVisible.value = true;
};
const hideDiv = () => {
isVisible.value = false;
};
</script>
<style scoped>
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>
- jQueryイベント処理からVueネイティブへの移行:
Before(jQuery):
<template>
<div id="newElement">Click!</div>
</template>
<script>
export default {
methods: {
handleClick() {
$("#newElement")
.css("color", "red")
.addClass("highlight")
.animate({ fontSize: "20px" }, 500);
}
}
}
</script>
After(Vueネイティブ):
<template>
<div :class="{ highlight: isHighlighted }" @click="handleClick">Click!</div>
</template>
<script setup lang="ts">
const isHighlighted = ref<boolean>(false);
const handleClick = () => {
isHighlighted.value = !isHighlighted.value;
};
</script>
<style scoped>
.highlight {
border: 2px solid blue;
}
</style>
主な変更点:
-
宣言的なUI
- DOMの直接操作からデータ駆動の実装へ
-
v-if
やv-show
による表示制御 - クラスバインディングによるスタイル制御
-
型安全性
-
ref
による状態管理 - イベントハンドラの型定義
- コンポーネントのprops型定義
-
-
パフォーマンス改善
- Virtual DOMの活用
- 最適化されたトランジション
- 効率的なイベント処理
これらの変更により、より保守性が高く、パフォーマンスの良いコードベースとなった。
6.AjaxからFetch APIへの移行と非同期処理の更新
- jQuery Ajaxから Fetch APIへの移行:
Before(jQuery Ajax):
// jQuery Ajax
$.ajax({
url: 'https://jsonplaceholder.typicode.com/posts/1',
method: 'GET',
success: function(data) {
this.message = data.title;
},
error: function(error) {
console.error('Error:', error);
}
});
After(Fetch API with TypeScript):
interface State {
message: string;
}
export const useMainStore = defineStore("main", {
state: (): State => ({
message: "",
}),
actions: {
async fetchMessage() {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts/1"
);
const data = await response.json();
this.message = data.title;
} catch (error) {
console.error("Error:", error);
}
},
},
});
- コンポーネントでの使用:
<template>
<div>
<h1>{{ store.message }}</h1>
<button @click="fetchData">Fetch Data</button>
</div>
</template>
<script setup lang="ts">
import { useMainStore } from "../stores/main";
const store = useMainStore();
const fetchData = () => {
store.fetchMessage();
};
</script>
主な変更点:
-
モダンな非同期処理
- コールバックからasync/awaitへ
- Promiseベースの実装
- エラーハンドリングの改善
-
型安全性の向上
- レスポンスデータの型定義
- エラーの型定義
- ストアの状態の型定義
-
コードの簡潔化
- jQueryの依存関係削除
- ネイティブAPIの活用
- より読みやすい非同期処理
-
エラーハンドリングの改善
- try/catchによる明示的なエラー処理
- 型安全なエラーハンドリング
- デバッグのしやすさ
これらの変更により、より保守性が高く、型安全な非同期処理となった。
7.ESLint/Prettierの設定更新
- ESLintの設定:
module.exports = {
root: true,
env: {
node: true,
},
extends: [
"plugin:vue/vue3-essential", // Vue3のルール
"eslint:recommended",
"@vue/typescript/recommended", // TypeScript用のルール
"plugin:prettier/recommended", // Prettierとの連携
],
parserOptions: {
ecmaVersion: 2020,
parser: "@typescript-eslint/parser", // TypeScriptパーサー
},
rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
"@typescript-eslint/no-explicit-any": "off",
"vue/multi-word-component-names": "off",
},
};
- Prettierの設定:
module.exports = {
semi: true,
singleQuote: false,
tabWidth: 2,
trailingComma: "es5",
};
- package.jsonのスクリプト:
{
"scripts": {
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"@vue/eslint-config-typescript": "^9.1.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.0.3",
"prettier": "^2.4.1"
}
}
主な変更点:
-
Vue3対応
-
plugin:vue/vue3-essential
の使用 - Vue3の新しい構文のサポート
- Composition APIのルール
-
-
TypeScript対応
- TypeScriptパーサーの設定
- TypeScript特有のルール
- 型チェックの強化
-
Prettier連携
- ESLintとPrettierの競合解消
- 一貫したコードフォーマット
- 自動修正の設定
-
カスタムルール
- プロジェクト固有のルール設定
- 開発環境に応じた警告レベル
- TypeScriptの厳格度調整
これらの設定により、より一貫性のある、型安全なコードベースが維持できるようになった。