はじめに
直近で 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 に置き換えていきます。
公式ドキュメントに記載があったため、それに合わせて調整を行います。
まずは 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 を剥がす際にちょっと面倒になるかも、と感じた部分ですね。
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 経由で参照する方法があります。
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」とかはうまく反映してくれずでした。
まとめ
概要の繰り返しにはなりますが、おおむね
- 数十ページ程度の静的 Web サイトを Nuxt 2 → 3 にバージョンアップした
- 移行はそれなりに大変だった
- 主に mixin 周りで一気に触る必要があるファイルが多かった
- 他にも Nuxt のバージョンに追従させるためにディレクトリ構成や使用パッケージのバージョンも見直す必要があった
- とはいえ、やってよかった。
- 個人的には Options API よりも Composition API と TypeScript が扱いやすい
- Nuxt 3 の面倒見がよく、かなりスッキリした
- 反面、剥がすとなったら大変そうな印象もある
という感じでした。
移植できたことは何よりですが、それ以上にいろいろ経験することができて面白かったです。
大変だったものの Nuxt 3、v2 よりかなり触りやすくなっていていいなと思えたマイグレーションでした。