12
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

un-T factory! XA Advent Calendar 2023

Day 14

Nuxt 2 から 3 へのマイグレーション 思い出 3 選

Last updated at Posted at 2023-12-14

はじめに

直近で Nuxt で構築されたサイトを更新する機会があったのですが、制作時は 2020 年で、Nuxt v2.14 で構築されていました。
v2.13 で Full Static Site Generate に対応され、Jamstack とともに盛り上がりを見せていたのを覚えています。

3 年も経てば、良いと思う方法や慣れた書き方も変わってくるものです。
Vue.js に関しては Composition API と TypeScript の組み合わせが自分は気に入っており、古い環境を刷新する良い機会だと考えたため Nuxt のバージョンアップを試みました。

Nuxt の変更点も多かったため不安感はありましたが、なんとか移行できたのでその際のメモです。
今後マイグレーションを行う方の一助となれば幸いです。

概要

  • 数十ページ程度の静的 Web サイトを Nuxt 2 から 3 にアップデート
  • 移行はそれなりに大変
    • mixin 周りで一気に触る必要があるファイルが多かったのが主な原因
    • 他にも Nuxt のバージョンに追従させるために、ディレクトリ構成や使用パッケージのバージョンも見直した
  • やってよかったと思う
    • 個人的には Options API よりも Composition API と TypeScript が扱いやすい
    • Nuxt 3 の面倒見がよく、コードもかなりスッキリした
      • 反面、剥がすとなったら大変そうな印象もある

前提

移行を行ったサイトは下記のように構成されていました。

  • 自社製作、数十ページ程度の静的 Web サイト
    • ロジックはさほど多くなく、Options API の Vue SFC で記述されているファイルが大半
    • 複雑な状態管理などはなかったため、Vuex や pinia などの状態管理ライブラリは使用せず
    • アニメーション部分やサイト内で使用している機能の部分を Vue.js / Nuxt で取り回していた

目的は Composition API と TypeScript を使用したいというところなので、どういう風にマイグレーションを進めるか検討するところから始めました。
具体的には下記の 3 パターンが考えられます。

  • Nuxt 2 → Nuxt 3 に一気にバージョンをあげてしまう
    • ほぼ作り直しになる。ちょっとやってみたものの、移行時にどこが原因で動かなくなったのか特定が難しく、時間をかけすぎないうちに断念。
  • Nuxt 2 → Nuxt/bridge までのアップデートで止めておく
    • 最小限のアップデート。一番工数がかからず、Composition API + TypeScript を使用したいという目的も達成できそう。
  • Nuxt 2 → Nuxt/bridge → Nuxt 3 の順で段階的にバージョンを上げる
    • 今後の運用を考えるとここまでできるとベスト。ただ作業的には結構重くなりそう

おおむね上記の理由で Nuxt/bridge までにしておくか Nuxt 3 まで上げるか迷いましたが、
Nuxt/bridge でメタ周りが事前に埋め込まれている形の Full Static Site Generate が実現できなかったため、段階的に Nuxt 3 まで上げることとしました。

その1. sass のアップデート

Node.js のバージョンが古かった

Nuxt/bridge を使用したマイグレーションの方法が公式ドキュメントに記載されています。
Migrate to Nuxt Bridge

@nuxt/bridge の動作環境は "node": "^14.16.0 || ^16.11.0 || >=17.0.0" ですが、
移行前環境のバージョンが "nuxt": 2.14.0"node": 10.14.0 とかなり古かったため、そのままでは @nuxt/bridge を動作させることができませんでした。

node.js のアップデートから行う必要がありましたが、その上でまずネックとなったのが node-sass と sass-loader でした。

node-sass のアップデート

node-sass は既に deprecated となっているため、自分は新規開発で使用での利用は避けています。
今回のマイグレーションでも、スタイリング部分は最終的に Dart Sass に移行しました。

@nuxt/bridge の動作確認を優先して行いたかったため、最初に Node.js のバージョンを上げることで node-sass のアップデートを行いました。

node-sass と Node.js のバージョンの対応表は以下にあります:
https://www.npmjs.com/package/node-sass

同時に sass-loader のバージョンも調整しました。
sass-loader は 11 系以降は webpack 5 での動作となるため、webpack 4 を使用している Nuxt 2 では 10 系を使用します。

  "devDependencies": {
    "node-sass": "^6.0.1",
    "sass-loader": "^10.0.2"
  }

こちらで一旦 Node.js 16 系までアップデートを行い、@nuxt/bridge が動作する環境を整えました。
その後、マイグレーションガイドに沿って Nuxt を v2.17 までアップデートし、@nuxt/bridge を追加して... という手順で進めました。
nuxt.config.js は @nuxt/bridge を導入する際に defineNuxtConfig でラップした程度で、この段階では移行前のものをほぼそのまま使用しています。

Dart Sass へのアップデート

node-sass を Dart Sass に置き換えていきます。
公式ドキュメントに記載があったため、それに合わせて調整を行います。

Using Preprocessors

まずは package.json と nuxt.config.js の移行から。
具体的には下記の作業を行っています。

  • sass のインストール
  • "@nuxtjs/style-resources" の削除
    • コンポーネント側で必要な変数を import するように変更しました。
  • scss ファイルの読み込み位置変更(style-resources から nuxt.config.js の css 配列内へ)

その後、Dart Sass の記法に合わせて scss ファイルを調整していきます。
おおむね以下のようなディレクトリ構成としています。(/assets/scss 配下)

├── _partials
│   ├── base.scss
│   ├── function.scss
│   ├── globals.scss
│   ├── keyframe.scss
│   ├── mixin.scss
│   ├── setting.scss(scss 変数管理用)
│   └── utilities.scss
└── main.scss

全体的に @import での記述を @use に変更したくらいで、各ファイルの中身は大きく変わっていませんが、下記のような globals.scss を新しく追加しています。

@forward "setting";
@forward "function";
@forward "mixin";
@forward "keyframe";

各 Vue.js の SFC から * で読み込むことで、node-sass と大きく使用感を変えずに移行することができました。

<template>
  <footer class="footer">footer sample</footer>
</template>

<style lang="scss" scoped>
// この行を追加している
@use "@/assets/scss/_partials/globals" as *;

.footer {
  position: relative;
  // breakpoint-up は setting.scss に記述
  z-index: $z-index-footer;

  // breakpoint-up は mixin.scss に記述
  @include breakpoint-up(md) {
    padding-top: 4rem;
  }
}
</style>

その2. ディレクトリ周りの Auto Import

Nuxt 3 では components, plugins, composables など、特定のディレクトリへのパスが自動で設定されるため、import を書かずにファイルを読み込むことが可能です。
plugins などはディレクトリ配下にファイルを置くだけで、自動的に読み込まれるようになっています。
今回 Nuxt の面倒見の良さと、逆に Nuxt を剥がす際にちょっと面倒になるかも、と感じた部分ですね。

Auto-imports

v2 でも components ディレクトリの Auto Import 自体はあり、v2.13 以降で使用できるようになっていましたが、実装当初に使用しないまま進めてしまっていたため、これを機に調整することにしました。

シンプルなコンポーネントでも他のコンポーネントを参照している場合、components を記述していたため、今回の調整で Vue.js の SFC に記載している scripts 自体が不要になるものも多かったです。

<template>
  <section>
    <Heading />
    <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis repellat laboriosam perferendis sunt nisi magni, nam, quidem impedit voluptatum voluptate mollitia ad commodi id consequuntur optio quas beatae illum nobis.</p>
  </section>
</template>

<script>
// このパターンだと script の記述がまるまる不要になります
import Heading from '@/components/Heading.vue';

export default {
  components: {
    Heading
  }
};
</script>

<style scoped>
p {
  font-size: 18px;
  font-weight: bold;
}
</style>

使用するコンポーネントの名称はディレクトリ構造から自動で指定されます。
ページを跨いで使用される共通コンポーネントとページ内のみで使用されるコンポーネントを分けて管理したかったため、components ディレクトリ内に shared, pages を切っています。
その際、共通コンポーネントには shared のプレフィックス、pages にはプレフィックスなしで命名されるように nuxt.config.ts の設定を少し調整しています。

export default defineNuxtConfig({
  components: [
    {
      path: "~/components/pages/",
    },
    {
      path: "~/components/",
    },
  ],
});

components のリストは自動で生成されます。
ビルトインのコンポーネント(Link など)と一緒に components.d.ts に追加されるようです。

declare module 'vue' {
  export interface GlobalComponents {
    'WorksList': typeof import("../components/pages/works/List.vue")['default']
    'WorksMainvisual': typeof import("../components/pages/works/Mainvisual.vue")['default']
    'SharedFooter': typeof import("../components/shared/Footer.vue")['default']
    'SharedHeading': typeof import("../components/shared/Heading.vue")['default']
    ...
  }
}

その3. mixin の移植

これが一番大変でした。mixin を composable に置き換えていきます。
composable は Vue コンポーネントからロジック部分を切り出したもので、React でいうところの hooks に相当するかと思います。
mixin は Options API での共通部分の切り出しであり、Composition API では使用することができないため composable に書き換えを行います。
その際、関連するコンポーネントとその内部で使用されている mixin を一気に composable に移行する必要が出てきます。

つまり

  • 一つの mixin を composable に置き換える
  • 置き換えた composable を使用するために、Vue コンポーネント側を Composition API に書き換える
  • Composition API 内で他にも mixin が使用されている場合、それらも同時に composable に置き換える必要がある
    という流れになります。
    一つだけ mixin を調整しようとして進めていると、芋づる式に変更箇所が増えていく感じですね。
<template>
  <p>sample components</p>
</template>

<script>
// mixins
import meta from "@/mixins/meta.ts";
import emitPageData from "@/mixins/emitPageData.ts";

export default {
  mixins: [meta, emitPageData],
};
</script>

上記のパターンだと、meta の mixin を変更しようとしたところ、コンポーネント自体を Composition API に書き換える都合で emitPageData も composable に調整する必要がある、ということですね。
ロジックがあまり複雑でなく、コンポーネント内から呼び出されている mixin も多くて 3, 4 個程度だったためなんとか移行できたものの、これが mixin の機能をフルに活用した複雑性のあるアプリケーションなら間違いなく心が折れていたと思います。

this.$nuxt の参照を解決

mixin 内で Nuxt のグローバルオブジェクトを参照して $on, $emit を使用していた箇所がありましたが、composable や Composition API では this.$nuxt を参照できないため、こちらも調整を行いました。
v2 での this.$nuxt 相当は useNuxtApp という composable を経由して使用することができます。

on, emit のハンドリングについては mitt というライブラリを使用してイベントバスを設定し、useNuxtApp 経由で参照する方法があります。

Event listener in Nuxt v3

import mitt from "mitt";

export default defineNuxtPlugin(() => {
  const emitter = mitt();

  return {
    provide: {
      eventBus: {
        $on: emitter.on,
        $emit: emitter.emit,
      },
    },
  };
});

上記のような plugins/eventBus.ts を追加することで、useNuxtApp().$eventBus から $on, $emit を使用することができました。

composable に関係ないコンポーネントも composition API に書き換え

Vue.js 3 では Options API、composition API をどちらも並行して使用することができます。
Nuxt についても例外ではなく、「こちらのコンポーネントは Options API、こちらは Composition API」というような運用が可能です。
この変更はマストで必要なわけではなかったのですが、改めて全体的な確認と記法の統一の意味で移植を行いました。

余談ですが、Composition API への書き換え作業は一部 GitHub Copilot Chat を使用しました。

Visual Studio の GitHub Copilot Chat 拡張機能とは

羂索もびっくりの「キッショ、なんで分かるんだよ」という精度のものを出してくれることもあります。
setup script での書き方にできなかったり、アロー関数を使ってくれなかったりといった調整は必要でしたが、叩き台をぱっと作ってくれるのはかなりありがたいですね。
インラインチャットでのメッセージの投げ方を調整することでなんとかできるかもしれませんが、「with arrow function」とかはうまく反映してくれずでした。

スクリーンショット 2023-12-14 9.02.34.png

まとめ

概要の繰り返しにはなりますが、おおむね

  • 数十ページ程度の静的 Web サイトを Nuxt 2 → 3 にバージョンアップした
  • 移行はそれなりに大変だった
    • 主に mixin 周りで一気に触る必要があるファイルが多かった
    • 他にも Nuxt のバージョンに追従させるためにディレクトリ構成や使用パッケージのバージョンも見直す必要があった
  • とはいえ、やってよかった。
    • 個人的には Options API よりも Composition API と TypeScript が扱いやすい
    • Nuxt 3 の面倒見がよく、かなりスッキリした
      - 反面、剥がすとなったら大変そうな印象もある
      という感じでした。

移植できたことは何よりですが、それ以上にいろいろ経験することができて面白かったです。
大変だったものの Nuxt 3、v2 よりかなり触りやすくなっていていいなと思えたマイグレーションでした。

12
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?