管理画面をNuxt2からNuxt3へ移行してみた感想
この記事は、Luup Advent Calendar 2023 の23日目の記事です。
はじめに
こんにちは、Luupのサーバサイドチームに業務委託として参加しているsmithshiroです。
Luupの管理画面ではフロントサイドのフレームワークとしてNuxt.jsを採用してるのですが、Nuxt2のサポートが2024年6月30日(https://v2.nuxt.com/lts/) に切れてしまうので、この度Nuxt3へのバージョンアップを行いました。
この記事は、実際にどうやって移行を進めたか、Nuxt3にして便利になった部分、移行で苦労した部分についてのざっくりした内容となります。
プロジェクトの規模感や移行にかけた時間など
- 画面数は60ページほど
- 移行期間は約2か月
- 移行に携わったメンバーは大体4名
移行の進め方
Nuxt Bridgeを使わずNuxt3用のディレクトリーを作成して、一から作り直しました。
具体的な試みとしてはこんな感じになりました。
- レイアウトやページ、コンポーネントをVue3の文法で動くように書き換える
-
Storybook
(https://storybook.js.org/) を導入し、コンポーネント単位で独立して開発できる環境を整備する -
Vuex
の使用をやめてバックエンドとのデータ通信のロジックをcomposables
に書き換える - Vue2のみで動作する依存パッケージを自作または代わりのライブラリーに置き換える
Nuxt Bridgeを使わなかったことについて
最初はNuxt2の既存ディレクトリーに対して「Nuxt Bridge」(https://nuxt.com/docs/bridge/overview) を適用し、文法や依存するパッケージなどをNuxt3で動くものに置き換えていくように進めていたのですが、結果的にNuxt3用のディレクトリーを作成して既存の機能を一から作り直す方針で進めました。
管理画面の移行なので利用者側との合意が社内で完結できたのが幸いだったのだと思います。
本流の開発を止めることになるので、その間はビジネス的な価値を生まず機会損失に繋がりかねません。
ただ、一から作り直して良かったなと感じる部分をあげると
- 移行後のソースコードが確実にNuxt3で動作することが保証される
- Nuxt Bridgeではいくつかの制限があってNuxt3ならではの
composables
で使えないものがある(useAsyncData
やuseFetch
など) - Nuxt Bridgeでは使用している他のパッケージがNuxt3に切り替えても動くことを保証しているわけではない
- Nuxt Bridgeではいくつかの制限があってNuxt3ならではの
- スムーズに移行に専念できる
- ビジネス的な都合で既存機能にNuxt2のコードを加えざるを得ないといったことが起きない
といったところがございます。
Nuxt2からNuxt3への移行時の変更点
Nuxt3へ移行する際に行った変更をまとめます。
nuxt.configの記述
Nuxtのアプリケーションを動かす設定ファイルとなるnuxt.config
の定義の仕方が変わりました。
Nuxt3ではdefineNuxtConfig
関数を使用して全体をラップして定義します。
Nuxt2の場合
export default { ... }
Nuxt3の場合
export default defineNuxtConfig({ ... })
静的ファイルの起き場所
Nuxt2ではstatic
ディレクトリーに静的ファイル配置していましたが、Nuxt3ではpublic
ディレクトリーに変更する必要があります。
Vue2からVue3にすることによる破壊的な変更点
Nuxt3はVue3で動作するため、Vue2からVue3にバージョンアップする必要があり、それによる破壊的な変更点を対応する必要があります。
変更点の詳細についてはこちらにまとめられております。(https://v3-migration.vuejs.org/ja/breaking-changes/)
v-model
に対応する子コンポーネントのプロパティ名とイベント名
Nuxt2ではv-model
はvalue
と@input
の組み合わせでしたが、Nuxt3からはそれぞれmodelValue
とupdate:modelValue
となります。
Nuxt2の場合
<InputText v-model="name" />
// または
<InputText :value="name" @input="(value) => name = value" />
import { Component, Vue, Prop, Ref } from 'nuxt-property-decorator'
@Component
export default class InputText extends Vue {
@Prop({ type: [String], default: '' }) readonly value!: string
get input() {
return this.value
}
set input(value) {
this.$emit('input', value)
}
}
Nuxt3の場合
<InputText v-model="name" />
// または
<InputText :model-value="name" @update:model-value="(value) => name = value" />
type Props = {
modelValue: string
}
const props = defineProps<Props>()
type Emits = {
"update:modelValue": [text: string]
}
const emit = defineEmits<Emits>()
const input = computed({
get: () => {
return isNull(props.modelValue) ? "" : props.modelValue
},
set: (value) => {
emit("update:modelValue", value)
},
})
<template v-for>
のkey
の記述場所
Nuxt2では繰り返し要素を描画するためのv-for
ディレクティブを<template>
タグに使用した場合、
子要素にkey
ディレクティブを指定していましたが、Nuxt3では<template>
タグに配置する必要があります。
Nuxt2の場合
<template v-for="(row, ri) in rows">
<tr :key="ri">...</tr>
</template>
Nuxt3の場合
<template v-for="(row, ri) in rows" :key="ri">
<tr>...</tr>
</template>
グローバルAPIの定義の仕方
Nuxtではplugins
ディレクトリにアプリケーション全体で使用するメソッドやコンポーネントを定義が可能ですが、その方法がNuxt3で変更されます。
Nuxt2の場合
Vue.prototype.$hello = () => { console.log("hello") }
Nuxt3の場合
export default defineNuxtPlugin(() => ({
provide: {
hello: () => { console.log("hello") }
},
}))
Nuxt3に移行して便利になったところ
Nuxt3に移行することでパフォーマンスや開発の生産性が上がりました。
個人的にはコードの記述量を減らせたり、責務を分離することでメンテナンスをしやすくなったのが大きな恩恵だと感じました。
自動インポート
デフォルトではcomponents
、utils
、composables
の配下にあるファイルが自動でインポートされるようになりました。
ディレクトリーを自動インポートを可能にしたい場合などは、nuxt.config
を編集します。
export default defineNuxtConfig({
...
imports: {
dirs: ["composables/**", "domains/**"],
},
})
composables
Nuxt3からはコンポーネントファイル(.vue)からロジックに相当するソースコードをcomposables
に分離することが可能になりました。
<template>
<!-- ビュー -->
<div v-for="city in cities" :key="city.id">
...
</div>
</template>
<script lang="ts" setup>
const { cities, findMany } = useCities()
onBeforeMount(async () => {
await findMany()
});
export const useCities = () => {
const cities = useState<City[]>(keyByFile(import.meta, "cities"), () => [])
const findMany = async () => {
cities.value = findCities()
}
return {
cities,
findMany,
}
}
苦労した話
移行に関して苦労したところをまとめます。
外部パッケージ関連
Nuxt2で動いているパッケージがNuxt3では動かなくなるというのはフロントサイド界隈ではなかなか衝撃的だったと思います。
このプロジェクトでもいくつかのコンポーネントやバリデーションなどで外部パッケージを使っているのですが、それを正しく動かす部分で苦労しました。
Vue2の文法で記述されているパッケージはもちろんのこと、Nuxt3ではビルドツールがVite
(https://vitejs.dev/) に変わったため、CommonJsの書き方であるdynamic require
を使用しているパッケージも使用ができなくなりました。
Vue3に対応したパッケージに置き換えた
以下のパッケージを置き換えました。
Nuxt2 | Nuxt3 |
---|---|
vue-datetime | @vuepic/vue-datepicker |
vue-infinite-loading | v3-infinite-loading |
vuedraggable | vue3-moveable |
特にvuedraggableとvue3-moveableとでは仕様が大きく異なるので、パッケージを使用しているソースコードにも手直しが必要でした。
バリデーションのパッケージが動かない
フォームデータのバリデーションではvalidatorjs(https://www.npmjs.com/package/validatorjs) というパッケージを使用しており、
移行当初は「コンポーネントではないからVue2の文法に依存した書き方ではないはずだからそのまま使えるでしょw」と踏んでいたのですが、
内部ではdynamic require
でエラーメッセージの言語ファイルを読み込んでいる箇所が原因でそのままでは使用できません(https://github.com/mikeerickson/validatorjs/issues/467) でした。
このパッケージに依存している箇所が多く、仮に置き換えた場合はそれなりに工数が増えてしまうのを避けたいのでエラーメッセージの言語データを事前に読み込むことで他のパッケージに置き換えるのを回避しました。
import Validator from "validatorjs"; // eslint-disable-line import/no-named-as-default
const ja = {
accepted: ":attributeを確認してください。",
...
};
Validator.setMessages("ja", ja);
Validator.useLang("ja");
品質保証
複数人で分担した移行作業でしたので、お互いの担当範囲の間で「どっちかが先に実装しないと、こっちが終わらない」みたいな箇所もありました。
なので 、その部分は後で手直しをして先に画面を表示できて操作が一通りできる状態にすることを優先して進めていました。
そのため、移行前のプロジェクト通りに動いているかまでは検証しておらず、QAチームに品質の確認を依頼する前にエンジニア達で動作チェックとソースコードの再確認をしました。
いざやってみると自分が実装していない機能の仕様について把握していないことがあり、正しい結果に対して確信を持ってテストするために仕様書があって良かったと思ってます。
おわり
以上、Nuxt3移行に関するお話でした。
Nuxt Bridgeを使わなかったのですが、一から作り直す方法も移行作業に専念できるので良かったと思っています。
移行では
- Vue2からVue3への破壊的変更
- Nuxt2からNuxt3への変更点
- ビルドツールがwebpackからViteになることによる影響
などを考慮する必要があるという学びを得られました。
これからNuxt3への移行に挑戦する方へ向けて、本記事の内容が少しでも参考になれば幸いです。
Discussion