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

## はじめに

直近で 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](https://nuxt.com/docs/bridge/overview)

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

https://github.com/nuxt/bridge/blob/main/package.json

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 系を使用します。

```js
  "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](https://nuxt.com/docs/getting-started/styling#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 と大きく使用感を変えずに移行することができました。

```vue
<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](https://nuxt.com/docs/guide/concepts/auto-imports)

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

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

```vue
<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 の設定を少し調整しています。

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

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

```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 を調整しようとして進めていると、芋づる式に変更箇所が増えていく感じですね。

```vue
<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](https://github.com/nuxt/nuxt/discussions/15950)

```ts
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 拡張機能とは](https://learn.microsoft.com/ja-jp/visualstudio/ide/visual-studio-github-copilot-chat?view=vs-2022)

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

![スクリーンショット 2023-12-14 9.02.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/390806/cc585a49-6a2c-281f-0655-4fb8fabd974e.png)

## まとめ

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

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