STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

Nuxt Bridge を活用して Nuxt 3 へ移行しました

Nuxt Bridge を活用して Nuxt 3 へ移行しました

業務委託で STORES の開発をしている @inouetakuya です。

以前 STORES が Nuxt Bridge を活用して Nuxt 3 への移行を進めている旨の記事を wattanx が書いてくれました。

そして先月(2024年7月)ようやく Nuxt 3 へ移行することができましたので、本記事は前回の記事の続編にあたります。

Nuxt Bridge とは

おさらいになりますが、Nuxt Bridge とは Nuxt 3 の機能の一部を Nuxt 2 でも利用できるようにしたライブラリです。これを活用すると Nuxt 2 のプロジェクトのまま Nuxt 3 の機能を利用するようコードを変更でき、Nuxt 3 へのバージョンアップの際の差分を極力小さくできます。

Nuxt 3 への移行以外にも並行して機能開発が行われており、コンフリクトができるだけ発生しないようにするため、また Nuxt 3 への移行時に不具合が発生しないようにするため、ビッグバンリリースを避けたいと考え、STORES では Nuxt Bridge を採用しました。

本記事の内容

では Nuxt 3 移行を行ってみて実際のところどうだったのか?

  • Nuxt Bridge を活用することでビッグバンリリースを避けることができたのか?
  • 移行にあたって他に工夫したところは?
  • 苦労した点
  • 移行してよかった点

これらについて得られた知見を共有させていただきます。

本記事で触れないこと

一方で Nuxt Bridge を活用して Nuxt 3 へ移行するための手順についての説明は本記事では割愛させていただきます。

具体的な手順については公式ドキュメントや Nuxt Bridge の主要なコントリビューターの一人である wattanx の記事や資料などを参考にしてください。

プロダクトの規模

今回 Nuxt 3 へ移行させたのは STORES ネットショップの一部であり、ショップオーナーさんが使う管理画面にあたるプロダクトでした。参考のために規模を書いておきますと、

  • pages/ 配下のコンポーネント数: 236
  • components/ 配下のコンポーネント: 1019

といったところです(いずれも Nuxt 3 移行直後のもの)。およそ伝わるでしょうか。

ビッグバンリリースを避けられたか

Nuxt 3 の機能のうち Nuxt Bridge が対応していないものや、Vue 3 の機能のうち Vue 2.7 が対応していないものはいくつか存在します。それらについては Nuxt 3 移行用のフィーチャーブランチを作成し、そちらに対して変更を加えていくしかありません。

つまり Nuxt 3 移行用のフィーチャーブランチと main ブランチとの差分が大きくなればなるほどビッグバンリリースになってしまいます。

コードの差分についてプルリクエスト数を見てみると、下記のような結果でした。

  • Nuxt 3 移行に関連して作成したプルリクエスト数の合計: 640
  • main ブランチに対するプルリクエスト数: 534
  • Nuxt 3 移行ブランチに対するプルリクエスト数: 106

こうしてみてみると Nuxt 3 移行ブランチに対して作成したプルリクエストもやや数がありますが、例えば Dynamic Routes に関する変更(_id.vue -> [id].vue)を細かくプルリクエストを分けて進めていったことなども影響しており、大半は main ブランチに対してプルリクエストを作成しました。ビッグバンリリースを避けられたと評価しています。

工夫した点

ラッパーの作成

Nuxt Bridge を活用する方法を採用した点もそうですが、他にもビッグバンリリースを避けるためにいくつか工夫しました。

例えば Vue.js 向けのテストライブラリである Vue Test Utils は Vue 2 向けと Vue 3 向けではコンポーネントをマウントするメソッドの I/F が異なります。

Vue 2 用:

import { mount } from '@vue/test-utils'

const wrapper = mount(MessageComponent, {
  propsData: {
    msg: 'Hello world'
  }
})

Vue 3 用:

import { mount } from '@vue/test-utils'

const wrapper = mount(MessageComponent, {
  props: {
    msg: 'Hello world'
  }
})

この I/F の差分を吸収するラッパーを作成し、各テストコードではラッパーのほうを使うようにしました。

// 以下はコードのイメージです。実際には動きません
import { mount } from '@vue/test-utils'

export const createWrapper = (component, mountOptions) => {
  // Vue 3 へ切り替える前
  const wrapper = mount(component, {
    propsData: mountOptions.props
    // ...
  })

  // Vue 3 へ切り替えた後
  const wrapper = mount(component, {
    props: mountOptions.props
    // ...
  })

  return wrapper
})
// 各テストコードではラッパーのほうを使う
import { createWrapper } from '~/test/utils/createWrapper'

const wrapper = createWrapper(MessageComponent, {
  props: {
    msg: 'Hello world'
  }
})

そのため Nuxt 3 移行ブランチをリリースするタイミングではラッパーの中身を書き換えるだけで済み、main ブランチとの差分を小さくすることができます。

また Vue Test Uitls の移行はラッパー作成の一例であり、他にも $loading(Nuxt 2 では window.$loading を使うが、Nuxt 3 では useLoadingIndicator を使う)などのラッパーを作成しました。

書き換えツールのドックフーディング

Nuxt 3 への移行に際しコンポーネントの書き換えについては、これまでに何度も登場している wattanx 製の wattanx-converter を大いに活用しました。

このツールをドックフーディングすることで、ツールの不具合修正や新しい機能の追加などに繋がり、より使いやすいツールにすることができました。

definePageMeta への書き換え機能を追加した例:

苦労した点

コンポーネントの記述方法の違い

コンポーネントの記述方法には(ページコンポーネントを含めると)

  • defineNuxtComponent を使った記述(Composition API)
  • defineComponent を使った記述(Composition API)
  • script setup を使った記述(Composition API)
  • Options API による記述

などがあります。

複数の記述方法を使っていると「この記述方法のとき〇〇はうまく動くか」をそれぞれ確認するコストが発生します。ですので、例えばですが、もし移行対象のプロジェクト内のコンポーネントの大半が script setup を使った記述をしているのであれば、残りのコンポーネントもすべて script setup を使うように書き換えた後に Nuxt 3 移行を行ったほうがラクだったかもしれません。

余談ですが、例えば definePageMeta について

  • Nuxt Bridge の defineNuxtComponent の setup 関数内では使えない(Nuxt Bridge のバグ)
  • Nuxt 3 の deineNuxtComponent の setup 関数内では使える
  • defineNuxtComponent ではなく defineComponent の setup 関数内だと Nuxt Bridge でも使える

みたいなことがありました。

ただこの Nuxt Bridge のバグは現在は wattanx によって修正されています。

Nuxt 3 に対応していないモジュール

Sentry の Nuxt モジュール などは Nuxt 3 に対応していません。また Nuxt 3 はデフォルトでは Vuex をサポートしていません。

Sentry のモジュールについては

などを参考にしつつ、プロジェクト内にモジュールを自前で作成しました。

また Vuex についても自前でモジュールを作成し npm パッケージとして公開しました(wattanx)

カルーセルライブラリである Hooper についても Vue 3 対応版を公開しています(wattanx)

ページコンポーネントの name 重複

pages/ 配下のコンポーネントの name(defineNuxtComponent / defineComponent / definePageMeta の name プロパティで指定)が他のページコンポーネントのものと重複していると、重複しているうちの片方のページが表示されないという現象に出くわしました。

これは name が重複している場合には Vue Router がルーティング設定(Nuxt を使っていると Nuxt が生成してくれる)内の最後にマッチした route しか保持しないためです。

Each name must be unique across all routes. If you add the same name to multiple routes, the router will only keep the last one. You can read more about this in the Dynamic Routing section.

これについては hooks を設定し、name の重複があった場合にはビルドエラーになるようにして対応しました。

// nuxt.config.ts
export default defineNuxtConfig({
  // ...
  hooks: {
    // ...
    'pages:extend': (pages: NuxtPage[]): void => {
      const names: string[] = pages.map((page) => page.name || '')

      const duplications = names.filter(
        (name, index) => names.indexOf(name) !== index
      )

      if (duplications.length > 0) {
        throw new Error(
          `Duplicated page names found: ${duplications.join(', ')}`
        )
      }
    },
  },
  // ...
})

移行してよかった点

ビルドの高速化

Nuxt によるビルドが高速になり、その結果、例えばステージング環境へのデプロイがこれまでおよそ 5分かかっていたのが 3分に短縮されるようになりました。

バンドルサイズの減少

全体のバンドルサイズが Nuxt 2 のときと比較して 44% 減少しました。

Vite による開発体験の向上

Nuxt 2 でも Vite を使うことができますが、今回 Nuxt 3 移行プロジェクトのなかで webpack から Vite へ移行して、やはり Vite の HMR(Hot Module Replacement)めちゃ速いなと実感しました。

まとめ

これまで見てきたとおり STOERS では Nuxt Bridge を活用することでビッグバンリリースを避けて Nuxt 3 移行を行うことができました。

Nuxt 3 移行に関連するプルリクエスト全体の数と、Nuxt 3 移行用のブランチに対するプルリクエスト数の実際の比較をみても、リリース時の差分を大きく減らせていることは明らかだと思います。

Nuxt 3 移行を行おうとされている方の参考になれば何よりです。

また、Nuxt 3 に直接移行するか Nuxt Bridge を使うかという点について、タイトルがそのままの記事を過去に wattanx が書いていますので、よろしければそちらも合わせて参考にしてください。