😇

管理画面をNuxt2からNuxt3へ移行してみた感想

2023/12/23に公開

この記事は、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で使えないものがある(useAsyncDatauseFetchなど)
    • Nuxt Bridgeでは使用している他のパッケージがNuxt3に切り替えても動くことを保証しているわけではない
  • スムーズに移行に専念できる
    • ビジネス的な都合で既存機能にNuxt2のコードを加えざるを得ないといったことが起きない

といったところがございます。

Nuxt2からNuxt3への移行時の変更点

Nuxt3へ移行する際に行った変更をまとめます。

nuxt.configの記述

Nuxtのアプリケーションを動かす設定ファイルとなるnuxt.configの定義の仕方が変わりました。

Nuxt3ではdefineNuxtConfig関数を使用して全体をラップして定義します。

Nuxt2の場合

nuxt.config.ts
export default { ... }

Nuxt3の場合

nuxt.config.ts
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-modelvalue@inputの組み合わせでしたが、Nuxt3からはそれぞれmodelValueupdate:modelValueとなります。

Nuxt2の場合

parent.vue
<InputText v-model="name" />
// または
<InputText :value="name" @input="(value) => name = value" />
InputText.vue
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の場合

parent.vue
<InputText v-model="name" />
// または
<InputText :model-value="name" @update:model-value="(value) => name = value" />
InputText.vue
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の場合

hello.ts
Vue.prototype.$hello = () => { console.log("hello") }

Nuxt3の場合

hello.ts
export default defineNuxtPlugin(() => ({
  provide: {
    hello: () => { console.log("hello") }
  },
}))

Nuxt3に移行して便利になったところ

Nuxt3に移行することでパフォーマンスや開発の生産性が上がりました。

個人的にはコードの記述量を減らせたり、責務を分離することでメンテナンスをしやすくなったのが大きな恩恵だと感じました。

自動インポート

デフォルトではcomponentsutilscomposablesの配下にあるファイルが自動でインポートされるようになりました。

ディレクトリーを自動インポートを可能にしたい場合などは、nuxt.configを編集します。

nuxt.config.ts
export default defineNuxtConfig({
  ...
  imports: {
    dirs: ["composables/**", "domains/**"],
  },
})

composables

Nuxt3からはコンポーネントファイル(.vue)からロジックに相当するソースコードをcomposablesに分離することが可能になりました。

pages/cities/index.vue
<template>
  <!-- ビュー -->
  <div v-for="city in cities" :key="city.id">
    ...
  </div>
</template>
<script lang="ts" setup>
const { cities, findMany } = useCities()

onBeforeMount(async () => {
  await findMany()
});
composables/useCities.ts
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) でした。

このパッケージに依存している箇所が多く、仮に置き換えた場合はそれなりに工数が増えてしまうのを避けたいのでエラーメッセージの言語データを事前に読み込むことで他のパッケージに置き換えるのを回避しました。

validator.ts
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への移行に挑戦する方へ向けて、本記事の内容が少しでも参考になれば幸いです。

Luup Developers Blog

Discussion