Skip to content

feat: Nuxt 2 から Nuxt 4 へ移行する - #560

Merged
NEKOYASAN merged 10 commits into
masterfrom
yuiseki-migrate-nuxt-4
Jul 31, 2026
Merged

feat: Nuxt 2 から Nuxt 4 へ移行する#560
NEKOYASAN merged 10 commits into
masterfrom
yuiseki-migrate-nuxt-4

Conversation

@yuiseki

@yuiseki yuiseki commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

概要 | About

Nuxt 2.17.3 から Nuxt 4.5.1 に移行します。
pug と Options API はそのまま残しています。

base は #559(vue-mapbox 撤去)です。
そちらがマージされると base が #558、さらに master に切り替わります。

#559 がマージされるまでは Draft にしておきます。

Nuxt 2 は 2024-06 に EOL、Vue 2 は 2023-12 に EOL です。
Dependabot Alert の 59% はこの移行で解消します。
構造的に残るのは maplibre-gl 1.x 経由の protocol-buffers-schema だけで、これは maplibre-gl の更新を別 PR にします。

既存の配置を動かさない方針にしています。
Nuxt 4 の既定 srcDir は app/ ですが、srcDir: "."dir: { app: "app" } を指定して pages/ components/ lib/ を今の場所に残しました。
ファイル移動を伴う差分にすると、このPRのレビューが実質不可能になるためです。

主な入れ替え

  • nuxt-i18n v6 → @nuxtjs/i18n v9。isolanguagelocalePath / switchLocalePath は composable になったため setup で公開
  • jest → vitest(@nuxt/test-utils/config)、Vue Test Utils v1 → v2
  • generate.routes()nitro.prerender.routes。全13ロケール × 4地図で 145 ルートを事前生成
  • require() による設定読み込み → import.meta.glob(Vite では動的 require が使えない)
  • static/public/

Vue 3 で黙って挙動が変わる箇所を5つ踏みました。
同一要素での v-for + v-if の禁止、配列の watcher に deep: true が必要になること、$i18n.localelocales が ref になること、defineNuxtComponenthead() が setup の戻り値も computed も見られないこと、data() から this.$route が引けないことです。
いずれも例外が出ないため、テストと実機確認で見つけています。

動作確認方法 | How to check

Node は 24 系です(Nuxt 4 の要件)。

yarn install
yarn test:unit   # 12 suites / 71 tests passed
yarn run lint    # 0 errors
yarn run generate

Vercel preview(fork 側の PR: yuiseki#24

https://mapprint-git-yuiseki-migrate-nuxt-4-yuiseki-s-team.vercel.app/

kamimap.com と全ページ突き合わせて確認しました。
印刷日時を正規化して比較すると、事前生成した 65 ページすべてで差分ゼロです。

地図ページは地図領域が 96.71% 一致で、差は下部のマーカー番号だけです。
番号は inBoundsMarkers.indexOf(marker) + 1 で決まり、その並びは mounted() が各ソースを await せず読み込み完了順に layers.push するため run ごとに変わります。
kamimap.com を 3 回開くと順序が変わることを確認しました。
このPRとは無関係な既存の挙動なので、直していません。
印刷される POI 一覧の内容(節・件数・地点名)は 4 つの地図ページすべてで一致しています。

スクリーンショット | Screenshot

kamimap.com(Nuxt 2)と このPR(Nuxt 4)の比較です。
1280x1024 の chromium、Accept-Language は ja-JP です。
読み込みタイミングが毎回変わる第三者ウィジェット(FB / X / LINE / GA)は両方で遮断しています。

Before(kamimap.com / Nuxt 2)

Image from Gyazo

After(このPR / Nuxt 4)

Image from Gyazo

Diff

赤が差分、灰色が一致です。
赤が 1px もありません。

Image from Gyazo

平均絶対差 0.000/2551 チャンネルでも差のあるピクセルが 0% です。
ja / en / zh / ko / th の 5 ロケールすべてで同じです。

はじめは差分の判定に「1 チャンネル 12 以下は無視」という閾値を使っていました。
文字のアンチエイリアスを除くためでしたが、それが ~assets の解決に失敗して消えていた背景画像を飲み込んでいました(閾値 0 では 10.35% のピクセルが違っていました)。
NEKOYASAN さんのご指摘で気づき、パスを直したうえで閾値 0 で測り直した数字が上記です。

挙動が変わる点 | Behavior change

ここだけ意図して kamimap.com と挙動を変えています。

detectBrowserLanguage をルートだけに限定しました(redirectOn: 'root')。

nuxt-i18n v6 の既定は全ルートで振り分けるため、現在は日本語端末で /en/ を開くと / に飛ばされて日本語になります。
これは i18n を導入した #299 の受け入れ条件「/en で英語が表示されること」に反しています。
redirectOn: 'root' にすると / の自動振り分けは今と同じまま、明示された接頭辞は尊重されます。

現在 このPR
/ + ネパール語端末 /ne/ /ne/
/ + 英語端末 /en/ /en/
/en/ + 日本語端末 / 日本語 /en/ 英語

外国人向けに URL を渡す使い方を想定すると後者が望ましいと考えましたが、製品判断だと思います。
今のままにすべきであれば戻します。

振り分けが起きる場合だけ Hydration completed but contains mismatches が出ます。
事前生成された / の日本語 HTML をクライアントが差し替えるためです。
現在も同じ差し替えが起きていますが、Nuxt 2 は警告を出しません。

移行で必要になった修正 | Fixes needed for the migration

Nuxt 2 では症状が出ず、移行して初めて出るものです。
3 点とも先にテストを書き、壊れた状態で落ちることを確認してから直しました。

トップページのタイトルpages/index.vue
defineNuxtComponenthead() は setup 経由で評価されるため、その時点で this.localeundefined になり常に default 分岐に落ち、日本語でも KamiMap になっていました。
useHead に移しました。

「このサイトについて」の本文components/Modal.vue
vue-i18n 9 で <i18n path><i18n-t keypath> に変わりました。
旧名は未知のコンポーネント扱いで本文が丸ごと消え、例外も出ません。

カードの列数assets/sass/vendor/gridlex/_gridlex-vars.scss
$gl-mq-list が降順で、min-width のメディアクエリはソース順に出力されます。
そのため 1280px では後に来る xs-6(50%)が lg-4(33.3%)に勝ち、PC で 3 列ではなく 2 列になっていました。
Nuxt 2 のビルドはメディアクエリを昇順にまとめ直していたため症状が出ていませんでした。
cssMinify: false でも再現するので最小化のせいではなく、ソースの並び順そのものが以前から逆でした。

別PRにしたいもの | Out of scope

  • maplibre-gl 1.15.3 → 6.x(protocol-buffers-schema の Alert が残る)
  • mounted() が読み込み完了順に layers.push するため、印刷される一覧とマーカー番号の並びが毎回変わる件
  • pages/index.vuecase "kr" はロケールコードの誤り(ko)。挙動を変えないためこのPRでは直していません

yuiseki added 7 commits July 31, 2026 21:31
pug と Options API は維持し、動かなくなったものだけを置き換えた。
srcDir: "." で既存のディレクトリ構造を保っているのでファイル移動は無い。
jest→vitest、nuxt-i18n→@nuxtjs/i18n、require→import、Node 20→24。
static/→public/、layouts/error.vue→error.vue、app.vue追加、pages/map/_map.vue→[map].vue。
Vue 3 で挙動が変わる箇所を修正: 同一要素のv-for+v-if、配列watcherにdeepが必要、
$i18n.locale/localesがref、head()はuseHeadへ。SSR不可のimportはクライアント動的import。
lib/mapConfigs.ts と KML の回帰テストを追加。
vue-i18n 9 でコンポーネント名と prop 名が変わり、旧名のままだと
未知のコンポーネント扱いで本文が丸ごと消えていた。例外は出ない。
defineNuxtComponent の head() は setup 時点で評価されるため this.locale が
undefined になり、日本語でも常に "KamiMap" になっていた。
min-width のメディアクエリはソース順に出るため降順だと xs が lg より後勝ちし、
col-12_xs-6_lg-4 が PC で2列になっていた。Nuxt 2 のビルドはメディアクエリを
昇順にまとめ直していたため症状が出ていなかった。
翻訳のあるロケールと固定文へのフォールバックを両方テストする。
useHead が locale を見られているかは index.head.spec で見る。
nuxt-i18n v6 の既定は全ルートで振り分けるため、日本語端末で /en/ を開くと
日本語になっていた。i18n を導入した #299 の受け入れ条件に反する。
redirectOn: 'root' にすると / の自動振り分けは残り、明示の接頭辞は尊重される。
@yuiseki
yuiseki force-pushed the yuiseki-migrate-nuxt-4 branch from 8fe5bf4 to 9877a81 Compare July 31, 2026 12:31
@yuiseki
yuiseki marked this pull request as ready for review July 31, 2026 13:45
@yuiseki
yuiseki changed the base branch from yuiseki-rm-vue-mapbox to master July 31, 2026 13:47

@NEKOYASAN NEKOYASAN left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

遅くなりすみません!全体的には素晴らしいです。

コメントを入れているものとは別に、以下の2点ご確認いただければと思います!

  1. yarn run generate 時の出力先が .output/public に変わっているようなので、gh-pagesのpublish_dirを変更いただきたいです!
    publish_dir: ./dist
  2. VRTの差分としては出ていないようなのですが、背景画像が正しく解決されていなくて表示されていないようで... assets/sass/_layout.scss の background-image を相対パスに変更する必要がありそうです
    background-image: url("~assets/images/index_body_bg.png");

Comment thread components/PrintableMap.vue
Comment thread .node-version
@@ -1 +1 @@
20.10.0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

この変更に合わせてworkflowのバージョン指定も変更いただくか、node-version-fileを使う形にしていただけるとよいかもです!

node-version: '20.10.0'

yuiseki added 3 commits August 1, 2026 02:31
Vue 3 は v-for の template ref の順序を保証しないため、表示範囲が変わって
DOM が再利用されると別の地点の吹き出しが付いていた。
併せて、残ったマーカーの番号が古いままだったのを書き換えるようにした。
~assets は webpack のエイリアスで、Vite では未解決のまま残り画像が出力されない。
@yuiseki

yuiseki commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

ありがとうございます。4点すべて直しました。

マーカーと吹き出しのずれ(must) ご指摘のとおり私の撤去時の欠陥です。原因は Vue 3 が v-for の template ref の配列順序を保証しないことで、$refs.markerEls[index] が別の地点を指していました。DOM 側に鍵を持たせて鍵で引くようにしました。
併せて、地点が範囲外に出たときに残ったマーカーの番号が古いままになる問題も見つかりました。地図の番号と印刷される一覧の番号が食い違うので、番号を書き換えるようにしています。PrintableMap.markerPairing.spec.js を追加し、修正を外すと2件落ちることを確認しました。

背景画像 ~assets は webpack のエイリアスで、Vite では未解決のまま CSS に残り、画像自体が出力されていませんでした。相対パスにしました。

gh-pages publish_dir./.output/public にし、node-version: '20.10.0' を他の4つの workflow と同じ node-version-file: '.node-version' に揃えました。

背景の件はこちらの検証不備です。PR 本文に「一致 100.00%、差分 0px」と書きましたが、比較の閾値(1チャンネル12以下を無視)が消えた背景を飲み込んでいました。 閾値0で測り直すと修正前は 10.35% のピクセルが違っていました。現在は平均差 0.000/255、閾値0でも差分 0% です。本文の数字も直します。

手元の結果です。yarn test:unit 13 files / 73 tests、yarn lint 0 errors、yarn run generate の出力が参照する局所アセット13件すべての実在も確認しました。

@NEKOYASAN NEKOYASAN left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

修正ありがとうございます!バッチリでした!

@NEKOYASAN
NEKOYASAN merged commit 9ce37bd into master Jul 31, 2026
4 checks passed
@NEKOYASAN
NEKOYASAN deleted the yuiseki-migrate-nuxt-4 branch July 31, 2026 17:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Vue 2/Nuxt 2からVue 3/Nuxt 3にアップデートする

2 participants