- Vue.js
Vue2からVue3への移行手順|2.7→移行ビルド→3.5を実測
Vue 2からVue 3への移行とは、書き方の一部が変わった箇所(破壊的変更)を直しながら、Vue本体と周辺ライブラリ(Vuex・Vue Router・ビルドツール)を新しい版へ入れ替える作業です。 一度に書き換える必要はなく、Vue 2.7でComposition APIを先に取り入れ → 移行ビルド(@vue/compat)で警告を見ながら直す → 通常のVue 3.5に切り替える、という段階的な進め方が公式に用意されています。
この記事では、Vue 2.7.16 + Vuex 3 + Vue Router 3 + Vue CLI 5(webpack) で作った小さなタスク管理アプリを、Vue 3.5.43 + Pinia 4.0.3 + Vue Router 5.3.1 + Vite 8.3.1 へ実際に移行しました。途中で出た警告・ビルドエラーの実際の文面と直し方、移行前後のバンドルサイズとビルド時間の実測値を載せています。
Vue 2は長く現場を支えてきたフレームワークで、今も多くのシステムで安定して動いています。すぐに移行しない判断にも、稼働の安定や改修コストといった理由があります。 そのため、サポート終了後の選択肢も中立に整理しました。
破壊的変更の早見表(Vue 2 → Vue 3)
「移行ビルドの警告」は、@vue/compat が出す警告コードです。今回のサンプルで実際に出たものに ✓ を付けています。
| Vue 2 の書き方 | Vue 3 の書き方 | 移行ビルドの警告 | 今回 |
|---|---|---|---|
new Vue({ render }).$mount('#app') |
createApp(App).mount('#app') |
GLOBAL_MOUNT |
✓ |
Vue.use(Plugin) / Vue.component() |
app.use() / app.component() |
GLOBAL_... |
|
Vue.prototype.$xxx |
app.config.globalProperties.$xxx |
GLOBAL_PROTOTYPE |
✓ |
フィルター {{ v | dateJa }} |
関数 {{ dateJa(v) }} |
FILTERS / COMPILER_FILTERS |
✓ |
イベントバス($on / $off) |
mitt などのライブラリ、またはストア | INSTANCE_EVENT_EMITTER |
✓ |
v-model(value + input) |
modelValue + update:modelValue(defineModel) |
COMPONENT_V_MODEL |
✓ |
:title.sync="x" |
v-model:title="x" |
COMPILER_V_BIND_SYNC |
✓ |
$listeners |
$attrs に統合 |
INSTANCE_LISTENERS |
✓ |
$children |
テンプレート参照(ref)や provide/inject |
INSTANCE_CHILDREN |
|
同じ要素の v-if と v-for(v-forが優先) |
v-ifが優先。computedで絞り込む | COMPILER_V_IF_V_FOR_PRECEDENCE |
✓ |
<template v-for> の子に key |
<template v-for> 自体に key |
COMPILER_V_FOR_TEMPLATE_KEY_PLACEMENT |
|
<template functional> |
普通のコンポーネント | (ビルドエラー) | ✓ |
非同期コンポーネント () => import() |
defineAsyncComponent(() => import()) |
COMPONENT_ASYNC |
✓ |
beforeDestroy / destroyed |
beforeUnmount / unmounted |
OPTIONS_BEFORE_DESTROY |
✓ |
@keyup.13 |
@keyup.enter |
V_ON_KEYCODE_MODIFIER |
✓ |
h('p', { attrs: { id } }) |
import { h } from 'vue'、h('p', { id }) |
RENDER_FUNCTION |
✓ |
.v-enter / .v-leave |
.v-enter-from / .v-leave-from |
(今回は警告なし) | ✓ |
<transition><router-view/></transition> |
<router-view v-slot> の中に <transition> |
Vue Routerの R0060 |
✓ |
Vue.set / this.$set |
通常の代入でOK | GLOBAL_SET |
|
:checked="false" で属性が消える |
false は "false" として出力(消すなら null) |
ATTR_FALSE_VALUE |
✓ |
new VueRouter({ mode: 'history' }) |
createRouter({ history: createWebHistory() }) |
(Vue Router側) | ✓ |
path: '*' |
path: '/:pathMatch(.*)*' |
(Vue Router側) | ✓ |
new Vuex.Store() |
Vuex 4 の createStore()、または Pinia |
(Vuex側) | ✓ |
全項目は公式「Vue 3 移行ガイド」の破壊的変更一覧で確認できます。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Vue2からVue3への移行手順|2.7→移行ビルド→3.5を実測」
Vue 2のサポート終了と、その後の選択肢
Vue 2は2023年12月31日にサポートが終了しました(EOL:End of Life)。 Vue公式の「Vue 2 LTS, EOL & Extended Support」ページには、次のように書かれています。
- npmやCDNでの配布は続く(今あるアプリがすぐ動かなくなるわけではない)
- セキュリティやブラウザ互換の修正を含む更新は行われない
- 延長サポートが必要な場合の選択肢として、HeroDevs社の「Never-Ending Support(NES)for Vue 2」 が案内されている
npmの最後の版は vue 2.7.16(2023年12月24日公開) です。今回の移行前サンプルで npm audit を実行すると、次の表示になりました。
$ npm audit --omit=dev
vue 2.0.0-alpha.1 - 2.7.16
ReDoS vulnerability in vue package that is exploitable through inefficient regex evaluation in the parseHTML function - https://github.com/advisories/GHSA-5j4c-8p2g-v4jx
fix available via `npm audit fix --force`
Will install vue@3.5.43, which is a breaking change
...
2 low severity vulnerabilities
深刻度は「low」で、テンプレートを文字列から実行時にコンパイルする処理(parseHTML)に関するものです。 修正版は「Vue 3への更新」と表示されます。影響の有無は、アプリが実行時にテンプレートをコンパイルしているかで変わるため、自分の構成で確認します。
| 選択肢 | 向いている状況 | 考えておくこと |
|---|---|---|
| 現状のまま運用 | 利用者が限られる社内システム、更新予定が少ない | 脆弱性情報の定期確認、周辺ライブラリの更新停止 |
| 延長サポート(NES)を利用 | 規程で「サポートのあるソフトウェア」が必要、移行に時間がかかる | 費用と契約。公式ページから案内されている |
| Vue 3へ段階的に移行 | 今後も機能追加を続ける | この記事の手順。UIライブラリの対応状況 |
| 作り直し | 画面の大部分を作り直す予定がある | 範囲とスケジュール |
どれが正解ということはなく、システムの寿命と更新頻度で選びます。 移行する場合は、次の手順で少しずつ進めると、動く状態を保ちながら作業できます。
移行の全体像
flowchart LR
A["Vue 2.6以前"] --> B["Vue 2.7<br>script setup を先に導入"]
B --> C["Vite + @vue/compat<br>Vuex 4 / Router 4以降"]
C --> D["警告を1つずつ解消"]
D --> E["Vue 3.5<br>Pinia / Router 5"]
| 段階 | やること | 状態 |
|---|---|---|
| 1. Vue 2.7 | <script setup> や Composition APIを先に使い始める |
Vue 2のまま動く |
| 2. 移行ビルド | vue を @vue/compat に差し替え、Vuex・Routerを上げる |
Vue 3上でVue 2の書き方がほぼ動き、警告が出る |
| 3. 警告の解消 | 警告コードごとに直す | 警告が減っていく |
| 4. Vue 3.5 | @vue/compat を外す。VuexをPiniaへ |
通常のVue 3 |
検証したサンプル(移行前)
Vue 2の代表的な書き方をあえて含めた、タスク一覧アプリです。
| ファイル | 含めたVue 2の書き方 |
|---|---|
main.js |
new Vue().$mount()、Vue.filter、Vue.prototype.$bus = new Vue() |
router.js |
new VueRouter({ mode: 'history' })、path: '*'、h('p', { attrs }) |
store.js |
new Vuex.Store()、Vue.set |
App.vue |
<transition> の中に <router-view>、.fade-enter |
TaskInput.vue |
value / input の v-model、$listeners |
DoneBadge.vue |
<template functional> |
TaskListPage.vue |
フィルター、v-if と v-for の併用、.sync、@keyup.13、$on、beforeDestroy、非同期コンポーネント |
{
"dependencies": { "vue": "^2.7.16", "vue-router": "^3.6.5", "vuex": "^3.6.2" },
"devDependencies": { "@vue/cli-service": "^5.0.9", "vue-template-compiler": "^2.7.16" }
}
動作はPlaywright(Chromium)で、「2件追加(ボタンとEnterキー)→ 1件完了 → 未完了だけ表示 → 別ページ → 存在しないURL」を操作して確認しました。移行前の結果です。
一覧: [ '備品を発注する(2026年10月01日)', '会議室を予約する(2026年10月20日)', '名刺を補充する(2026年10月20日)' ]
ログ: 「名刺を補充する」を追加しました
残り: 残り 2 件 / バッジ: 1件完了
未完了だけ: 2 件
非同期コンポーネント: Enterキーでも追加できます
about: このアプリはタスクを管理します
404: ページが見つかりません
--- console warnings/errors: 0
移行後も、この出力がまったく同じになることをゴールにします。
手順1:Vue 2.7で Composition API を先に使う
Vue 2.7では <script setup>・ref・computed などがVue 2のまま使えます。 新しく書く部品や、触る予定の部品から切り替えておくと、後の移行で直す量が減ります。
<!-- src/components/FilterTabs.vue(Vue 2.7 で動作、Vue 3 でも無修正で動いた) -->
<script setup>
defineProps({ filter: String })
const emit = defineEmits(['update:filter'])
const filters = ['all', 'open']
</script>
<template>
<div>
<button v-for="f in filters" :key="f" :id="'tab-' + f" @click="emit('update:filter', f)">{{ f }}</button>
</div>
</template>
Vue CLI 5でビルドは成功し、画面の動作も変わりませんでした。このファイルは最後まで一度も直さずにVue 3.5で動きました。
手順2:移行ビルド(@vue/compat)に切り替える
ビルドツールをViteにし、vue を @vue/compat に差し替える
公式の「移行ビルド」の手順どおり、vue の読み込み先を @vue/compat に向け、互換モード(MODE: 2)を指定します。今回はあわせてビルドツールをVue CLIからViteへ変えました(index.html をルートに置き、<script type="module" src="/src/main.js"> を書く)。
// vite.config.js(移行ビルドの間だけ)
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
resolve: { alias: { vue: '@vue/compat' } },
plugins: [
vue({ template: { compilerOptions: { compatConfig: { MODE: 2 } } } }),
],
})
つまずき1:Vuex 3のままではインストールできない
最初はVuex 3とVue Router 3のまま入れようとして、npmが止まりました。
$ npm i vue@3.5.43 @vue/compat@3.5.43 vuex@3.6.2 vue-router@3.6.5 (出力は抜粋)
npm error code ERESOLVE
npm error ERESOLVE unable to resolve dependency tree
npm error Found: vue@3.5.43
npm error Could not resolve dependency:
npm error peer vue@"^2.0.0" from vuex@3.6.2
Vuex 3・Vue Router 3はVue 2専用なので、移行ビルドの段階で一緒に上げます。 Vuex 4(createStore)とVue Router 5を入れ、作り方を変えました。Vue Router 5は、unplugin-vue-routerを使っていない4からならコード変更なしで上げられると公式の移行ガイドに書かれています(主な変化はファイルベースルーティングの統合)。
// router.js(Vue Router 3 → 5)
import { createRouter, createWebHistory } from 'vue-router'
import TaskListPage from './views/TaskListPage.vue'
export default createRouter({
history: createWebHistory(), // mode: 'history' の代わり
routes: [
{ path: '/', component: TaskListPage },
{ path: '/about', component: () => import('./views/AboutPage.vue') },
{ path: '/:pathMatch(.*)*', component: NotFound }, // path: '*' の代わり(NotFound は後述)
],
})
// main.js(移行ビルドの段階)
import Vue, { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store' // Vuex 4 の createStore() で作り直したもの
// そのまま残したもの(移行ビルドが警告を出すか確認する)
Vue.filter('dateJa', (v) => v.replace(/^(\d{4})-(\d{2})-(\d{2})$/, '$1年$2月$3日'))
Vue.prototype.$bus = new Vue()
createApp(App).use(router).use(store).mount('#app')
つまずき2:functional テンプレートはビルドエラー
[plugin vite:vue] src/components/DoneBadge.vue
SyntaxError: <template functional> is no longer supported in Vue 3, since functional components no longer have significant performance difference from stateful ones. Just use a normal <template> instead.
移行ビルドでも <template functional> だけはビルドが止まりました。 普通のコンポーネントにします。
<!-- 直した後 -->
<template>
<span class="badge">{{ count }}件完了</span>
</template>
<script setup>
defineProps({ count: Number })
</script>
移行ビルドで出た警告(実際の一覧)
開発サーバー(vite)で画面を操作すると、ブラウザのコンソールと端末に次の警告が出ました(一部を抜粋)。
[Vue warn] (deprecation COMPILER_V_BIND_SYNC) .sync modifier for v-bind has been removed. Use v-model with argument instead. `v-bind:filter.sync` should be changed to `v-model:filter`.
[Vue warn] (deprecation COMPILER_V_IF_V_FOR_PRECEDENCE) v-if / v-for precedence when used on the same element has changed in Vue 3: v-if now takes higher precedence and will no longer have access to v-for scope variables.
[vite] (client) warning: (deprecation COMPILER_FILTERS) filters have been removed in Vue 3. The "|" symbol will be treated as native JavaScript bitwise OR operator.
[Vue warn]: (deprecation GLOBAL_MOUNT) The global app bootstrapping API has changed: vm.$mount() and the "el" option have been removed.
[Vue warn]: (deprecation GLOBAL_PROTOTYPE) Vue.prototype is no longer available in Vue 3. Use app.config.globalProperties instead.
[VUE_ROUTER_R0060] <router-view> can no longer be used directly inside <transition>.
[Vue warn]: (deprecation INSTANCE_EVENT_EMITTER) vm.$on/$once/$off() have been removed. Use an external event emitter library instead.
[Vue warn]: (deprecation COMPONENT_V_MODEL) v-model usage on component has changed in Vue 3.
[Vue warn]: (deprecation COMPONENT_ASYNC) Async component <HelpBox> should be explicitly created via `defineAsyncComponent()` in Vue 3.
[Vue warn]: (deprecation INSTANCE_LISTENERS) vm.$listeners has been removed.
[Vue warn]: (deprecation V_ON_KEYCODE_MODIFIER) Using keyCode as v-on modifier is no longer supported.
deprecation の警告コードは14種類でした(抜粋外の ATTR_FALSE_VALUE・OPTIONS_BEFORE_DESTROY・RENDER_FUNCTION を含む。アプリ本体は createApp に変えていたので、GLOBAL_MOUNT はイベントバスの new Vue() から出たもの)。各警告には公式移行ガイドの該当ページのURLが付くので、それを見ながら1つずつ直せます。
気をつけたい点が2つありました。
v-ifとv-forの併用は、移行ビルドでもVue 3の動き(v-if優先)になりました。 「未完了だけ」を押すとProperty "t" was accessed during render but is not defined on instance.とCannot read properties of undefined (reading 'done')が出て、未完了だけの件数が「3 件」(本来は2件)になりました。互換モードでも動きが変わる箇所があるので、警告が出たら動作確認までセットで行います。- トランジションのクラス名(
.fade-enter→.fade-enter-from)には警告が出ませんでした。 CSSは実行時の警告の対象外なので、grep -rn "enter|leave" srcのように文字列で探して直します。
手順3:警告を1つずつ直す(直す前と後)
イベントバスとフィルター
// 前:main.js
Vue.filter('dateJa', (v) => ...)
Vue.prototype.$bus = new Vue()
// コンポーネント内:this.$bus.$on('added', ...) / {{ t.due | dateJa }}
// 後:src/bus.js(mitt は gzip で200バイト未満のイベントライブラリ)
import mitt from 'mitt'
export const bus = mitt()
// 後:src/format.js(フィルターは普通の関数に)
export const dateJa = (v) => v.replace(/^(\d{4})-(\d{2})-(\d{2})$/, '$1年$2月$3日')
イベントバスは、画面をまたいで状態を共有しているなら Pinia に移す方が見通しがよくなります。 通知だけなら mitt で十分です。
v-model・$listeners(TaskInput.vue)
<!-- 前(Vue 2) -->
<template>
<input :value="value" v-on="listeners" />
</template>
<script>
export default {
props: ['value'],
computed: {
listeners() {
return { ...this.$listeners, input: (e) => this.$emit('input', e.target.value) }
},
},
}
</script>
<!-- 後(Vue 3.5) -->
<script setup>
const model = defineModel()
</script>
<template>
<!-- 親の @keyup などは $attrs として自動でこの要素に渡る -->
<input v-model="model" />
</template>
Vue 3では親が付けたイベント(@keyup.enter など)は $attrs に含まれ、ルート要素へ自動で渡ります。 $listeners を合成する処理はまるごと不要になりました。
テンプレート(TaskListPage.vue)
<!-- 前 -->
<TaskInput id="title" v-model="title" @keyup.13="add" />
<FilterTabs :filter.sync="filter" />
<li v-for="t in tasks" v-if="filter === 'all' || !t.done" :key="t.id" class="task">
{{ t.title }}({{ t.due | dateJa }})
</li>
<!-- 後 -->
<TaskInput id="title" v-model="title" @keyup.enter="add" />
<FilterTabs v-model:filter="filter" />
<li v-for="t in visibleTasks" :key="t.id" class="task">
{{ t.title }}({{ dateJa(t.due) }})
</li>
// 後:script(Options APIのまま。TaskInput等のimportは省略)
import { defineAsyncComponent } from 'vue'
import { mapState, mapActions } from 'pinia'
import { useTaskStore } from '../stores/tasks'
import { bus } from '../bus'
import { dateJa } from '../format'
export default {
components: {
TaskInput, DoneBadge, FilterTabs,
HelpBox: defineAsyncComponent(() => import('../components/HelpBox.vue')),
},
data() { return { title: '', due: '2026-10-15', filter: 'all', log: '' } },
computed: {
...mapState(useTaskStore, ['tasks', 'remaining']),
visibleTasks() { return this.tasks.filter((t) => this.filter === 'all' || !t.done) },
},
created() { bus.on('added', this.onAdded) },
beforeUnmount() { bus.off('added', this.onAdded) }, // beforeDestroy から改名
methods: {
...mapActions(useTaskStore, { addTask: 'add', toggle: 'toggle' }),
dateJa,
onAdded(title) { this.log = `「${title}」を追加しました` },
add() {
if (!this.title) return
this.addTask({ title: this.title, due: this.due })
bus.emit('added', this.title)
this.title = ''
},
},
}
Options API(data / methods で書く方式)はVue 3でも公式にサポートされています。 移行と同時にComposition APIへ書き換える必要はなく、今回も TaskListPage.vue はOptions APIのまま動かしました。
トランジションと404ページ
<!-- App.vue 後 -->
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
<style>
.fade-enter-from, .fade-leave-to { opacity: 0; } /* .fade-enter から改名 */
.fade-enter-active, .fade-leave-active { transition: opacity .2s; }
</style>
// router.js 後:render 関数は h を import し、属性は直接書く
import { h } from 'vue'
const NotFound = { render: () => h('p', { id: 'notfound' }, 'ページが見つかりません') }
手順4:@vue/compat を外し、Vuex を Pinia へ
警告がなくなったら、vite.config.js の alias と compatConfig を消し、@vue/compat をアンインストールします。
// vite.config.js(最終)
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({ plugins: [vue()] })
Vue公式は新規の状態管理にPiniaを推奨しています。 Vuex 4はVue 3で動くので、移行ビルドの段階ではVuex 4のまま進め、最後にPiniaへ移すと作業を分けられます。
// 前:Vuex 3(store.js)
export default new Vuex.Store({
state: { tasks: [...] },
getters: { remaining: (s) => s.tasks.filter((t) => !t.done).length },
mutations: {
add(state, task) { state.tasks.push(task) },
toggle(state, id) { const t = state.tasks.find((x) => x.id === id); Vue.set(t, 'done', !t.done) },
},
actions: { add({ commit }, { title, due }) { commit('add', { id: Date.now(), title, due, done: false }) } },
})
// 後:Pinia 4(src/stores/tasks.js)
import { defineStore } from 'pinia'
export const useTaskStore = defineStore('tasks', {
state: () => ({ tasks: [{ id: 1, title: '備品を発注する', due: '2026-10-01', done: false }] }),
getters: { remaining: (s) => s.tasks.filter((t) => !t.done).length },
actions: {
add({ title, due }) { this.tasks.push({ id: Date.now(), title, due, done: false }) },
toggle(id) { const t = this.tasks.find((x) => x.id === id); t.done = !t.done },
},
})
// 後:main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
createApp(App).use(createPinia()).use(router).mount('#app')
| Vuex | Pinia |
|---|---|
state / getters |
そのまま(state は関数で返す) |
mutations と actions |
actions に一本化(this で直接変更) |
this.$store.commit('toggle', id) |
store.toggle(id) |
mapState / mapActions(vuex) |
mapState(useStore, [...]) / mapActions(pinia) |
Vue.set(obj, key, v) |
普通の代入 |
移行後の結果(動作・サイズ・ビルド時間)
移行後のアプリで同じPlaywrightの操作をした結果、出力は移行前と完全に一致し、Vueの警告は0件になりました(出たのは favicon.ico の404のみ)。
一覧: [ '備品を発注する(2026年10月01日)', '会議室を予約する(2026年10月20日)', '名刺を補充する(2026年10月20日)' ]
ログ: 「名刺を補充する」を追加しました
残り: 残り 2 件 / バッジ: 1件完了
未完了だけ: 2 件
...
404: ページが見つかりません
バンドルサイズ(JSの合計、gzip -9 で測定)
| 段階 | 構成 | JSファイル数 | 圧縮前 | gzip |
|---|---|---|---|---|
| 移行前 | Vue 2.7.16 + Vuex 3 + Router 3 + Vue CLI 5 | 4 | 118.7 KiB | 41.5 KiB |
| 移行ビルド | @vue/compat 3.5.43 + Vuex 4 + Router 5 + Vite 8 | 3 | 229.9 KiB | 83.6 KiB |
| 移行後 | Vue 3.5.43 + Pinia 4 + Router 5 + mitt + Vite 8 | 3 | 105.9 KiB | 41.1 KiB |
移行ビルドの段階では互換用のコードが入るため約2倍になりましたが、@vue/compat を外すと移行前とほぼ同じ大きさ(41.1 KiB)に戻りました。 移行ビルドはあくまで途中の足場で、本番へ出す前に外すことを前提にします。
ビルド時間(本番ビルド、キャッシュを消して各3回)
| 段階 | 1回目 | 2回目 | 3回目 |
|---|---|---|---|
| 移行前(Vue CLI 5 / webpack 5) | 4.21秒 | 4.24秒 | 4.06秒 |
| 移行ビルド(Vite 8) | 0.94秒 | 0.89秒 | 0.93秒 |
| 移行後(Vite 8) | 0.92秒 | 0.89秒 | 0.84秒 |
コマンドの起動から終了までで、約4.2秒 → 約0.9秒になりました(2コアの共有環境での参考値)。
UIライブラリを使っている場合
UIライブラリの移行は、Vue本体より作業量が大きくなりやすい部分です。 先に対応版と変更点を確認し、画面単位で進めます。
| Vue 2で使っていたもの | Vue 3の対応版 | ひとこと |
|---|---|---|
| Vuetify 2 | Vuetify 3 → 4(最新は4.2.2) | 2→3でコンポーネントのAPIが大きく変わった。4ではCSSレイヤー、テーマ既定値(system)、ブレークポイントの値なども変更 |
| Element UI | Element Plus(最新は2.14.6) | Vue 3版として別パッケージ。コンポーネント名は似ているが、props名の変更などを公式ドキュメントで確認 |
よくある失敗と直し方
| 症状 | 原因 | 直し方 |
|---|---|---|
ERESOLVE ... peer vue@"^2.0.0" from vuex@3.6.2 |
Vuex 3・Router 3はVue 2専用 | Vuex 4(またはPinia)とVue Router 4以降に上げる |
<template functional> is no longer supported |
functionalテンプレートの廃止 | 普通のコンポーネントにする |
| 一覧の絞り込み結果がおかしい | v-if が v-for より先に評価される |
computedで絞り込んでから v-for |
| フェードが効かない | .v-enter の改名(警告なし) |
.v-enter-from に直す。文字列で検索 |
<router-view> can no longer be used directly inside <transition> |
Router 4以降の書き方 | <router-view v-slot="{ Component }"> |
入力値が [object InputEvent] になる |
$listeners をそのまま渡し、input イベントが素通り |
Vue 3では defineModel と $attrs に任せる |
最後の行は、実はVue 2のサンプルを作る段階で起きたものです。 v-on="$listeners" と @input を併用すると、親の v-model にイベントオブジェクトが渡ってしまいます。Vue 3の defineModel ではこの組み立て自体が不要になります。
よくある質問
Q. Vue 2のサポートが終わったら、すぐに移行しないといけませんか? A. 配布は続いており、アプリがすぐに動かなくなるわけではありません。更新が止まった点を踏まえ、脆弱性情報の確認、延長サポートの利用、段階的な移行のどれを選ぶかを、システムの寿命と更新頻度で判断します。
Q. 移行と同時にComposition APIへ書き換えるべきですか?
A. 必須ではありません。Options APIはVue 3でも公式にサポートされており、今回もOptions APIのまま移行しました。触る機会のある部品から少しずつ <script setup> にしていく進め方が現実的です。
Q. Vue CLIのままVue 3へ移行できますか? A. 公式の移行ビルドの手順にはvue-loader 16以上へ上げる方法も書かれています。今回はビルド時間の短縮も狙ってViteへ同時に移しましたが、作業を分けたい場合はビルドツールの変更を後回しにする選択もあります。
まとめ
- Vue 2は2023年12月31日にサポート終了。配布は続くが更新はなし。選択肢は 現状運用・延長サポート(NES)・段階移行・作り直し
- 進め方は Vue 2.7で
<script setup>を先に導入 →@vue/compatで警告を見ながら修正 → Vue 3.5 - 移行ビルドの段階で Vuex 3・Router 3は上げる必要がある(ERESOLVEで止まる)。
<template functional>はビルドエラー - 今回の警告は14種類。
v-ifとv-forの併用は互換モードでも動きが変わり、トランジションのクラス名は警告が出ない - Options APIのままでもVue 3で動く。Vuexは最後にPiniaへ移すと作業を分けられる
- 実測で gzipは41.5 KiB → 41.1 KiB(移行ビルド中は83.6 KiB)、ビルドは約4.2秒 → 約0.9秒
- 移行後も Playwrightの操作結果が移行前と完全一致することをゴールにすると、安心して切り替えられる
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Vue2からVue3への移行手順|2.7→移行ビルド→3.5を実測」
株式会社RJCでは、長く動いてきたシステムを大切に保守しながら、新しいバージョンへ安全に移す方法もプロジェクトの中で共有しています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。