CI 是綠的。

建置通過了。沒有 TypeScript 錯誤。沒有警告。看起來一切都很乾淨。我按下部署,然後去泡茶。

回來後打開 staging,結果有些地方壞掉了,而且壞得毫無道理。某個重新導向沒作用。lint 在建置流程裡悄悄消失了。某條 API 路由在第一次真正的請求時就拋錯。我兩週前寫的一個 revalidation 呼叫有在執行,卻什麼都沒做。

這些問題沒有一個在建置時出現。所有東西在出事前,看起來都完全正常。

這就是我在升級到 Next.js 16 時實際發生的事,以及你在上線前應該檢查什麼。

1. middleware.ts 停止運作,而且什麼都沒告訴我

我的 middleware 檔案本身沒問題。它能編譯。匯出也合法。TypeScript 也沒意見。

升級到 Next.js 16 之後,它就不再對請求生效了。沒有錯誤。沒有棄用警告。終端機也沒有任何異常訊息。這個檔案就這樣被直接忽略了。

原因是:Next.js 16 把 middleware.ts 改成了 proxy.ts。專案位置一樣,但檔名不同。匯出的函式名稱也不同。

// 之前:middleware.ts
export function middleware(request: NextRequest) {
  return NextResponse.redirect(new URL('/home', request.url))
}
// 之後:proxy.ts
export function proxy(request: NextRequest) {
  return NextResponse.redirect(new URL('/home', request.url))
}

改動就只有這樣:改檔名、改函式名。但因為舊檔案沒有丟出任何錯誤,我還以為它仍然有在跑。直到我預期中的重新導向沒有發生,才花了超久時間去查錯方向。

有一點要注意:如果你特別需要 edge runtime 的行為,middleware.ts 仍然保留給那個用途。在我的情況裡,升級後那段邏輯就不再執行了。把檔名和匯出名稱改掉後立刻就修好了。codemod 會自動處理這件事。但如果你是手動升級套件卻沒跑它,或它漏掉了某個檔案,這個問題會完全隱形。

上線前請先做:改檔名、改匯出名稱,並測試一個依賴它的路由。

2. revalidateTag('products') 能編譯、能部署,卻默默做了錯的事

在遷移過程中我寫了這樣的程式:

revalidateTag('products')

只有一個參數。在 Next.js 15 裡這很正常。我兩週前寫過一次,之後就沒再想它。

在 Next.js 16 裡,單一參數的形式已被棄用,而且會產生 TypeScript 錯誤。但前提是你的 tsconfig 有開 strict mode。我的沒有。那是很多年前從舊專案搬來的設定,之後就沒動過。

所以它編譯過了。部署成功了。也真的執行了。只是它回退到舊的 invalidation 行為,而不是新的 SWR 型系統。頁面沒有反映 mutation。沒有任何錯誤,只有一直是舊資料,而且我比應該的時間更久才把原因歸到別的地方。

修正方式只是加上第二個參數:

revalidateTag('products', 'max')          // SWR,建議預設
revalidateTag('products', { expire: 0 })  // 立即過期,適合 webhook

codemod(npx @next/codemod@canary upgrade latest)會處理這件事。但如果你升級後又寫了新的 revalidation 呼叫,或 codemod 漏掉了某個檔案,請手動檢查。

真正的修正是把 tsconfig 的 strict mode 打開。這個改動會讓它變成編譯錯誤,而不是悄悄在 runtime 出問題:

{
  "compilerOptions": {
    "strict": true
  }
}

先做這件事。

3. next lint 不見了,而我的 CI 還一直顯示通過

這件事聽起來不大,但其實不是。

next lint 在 Next.js 16 裡被完整移除了。不是棄用,不是改名,就是沒了。next.config.ts 裡的 eslint 選項也被移除了。next build 也不再自動執行 lint。

我的 CI 設定成把 next lint 當作其中一個步驟。升級之後,那個指令已經不存在了。依照你的 CI 如何處理缺失的指令,它可能會明確失敗,也可能會安靜地成功然後繼續往下跑。我的就是直接往下跑。

所以我等於是在沒有 lint 的情況下送出程式碼,而 CI 還回報綠燈。直到有個明顯問題漏過去,而我原本以為 lint 會抓到它,我才發現這件事。

遷移方式是直接執行 ESLint:

"scripts": {
  "lint": "eslint .",
  "lint:fix": "eslint . --fix"
}

codemod 會建立 eslint.config.mjs,並更新你 package.json 的 scripts。但你的 CI 設定是另一個檔案,codemod 不會碰它。兩邊都要檢查。

4. 有一個元件還在同步讀取 params

codemod 幫我正確更新了大多數頁面。但我有一個 layout 檔案沒被處理到。那個元件直接讀取 params,沒有先 await,在 Next.js 15 這樣還可以,但在 16 就不對了,因為現在的 params 是 Promise。

// 之前 — Next.js 15
export default function Layout({ params }: { params: { id: string } }) {
  const id = params.id
}

// 之後 — Next.js 16
export default async function Layout({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
}

這個問題確實有拋錯,但只有在 staging 中該路由第一次真正收到請求時才會發生,不會在建置時出現。建置是完全乾淨地通過。

如果你有 layouts、pages 或 route handlers,請整個 codebase 搜尋直接使用 params. 的地方,確認每一個都已更新。同樣也要檢查 searchParamscookies()headers()draftMode()。現在都變成非同步了,都需要 await

連結這四個問題的共同模式

這些都不是快取 bug。它們是升級 bug。這類問題的特徵就是:建置會通過、程式碼在技術上是合法的,但錯誤行為只會在特定條件下出現:真正觸發重新導向、需要反映 mutation、lint 問題進到 review、某條特定路由被打到。

codemod 可以處理大部分內容。在你改其他東西之前,先跑 npx @next/codemod@canary upgrade latest。然後手動檢查三件事:搜尋所有只有一個參數的 revalidateTag(、檢查 CI 設定裡是否還有 next lint、以及打開 strict TypeScript。這三項可以涵蓋 codemod 最容易漏掉的大部分內容。

如果你已經完成升級,現在正在處理的是快取行為本身,那麼這系列前面的文章有涵蓋這部分。我做的除錯器,讓開發期間的快取行為變得可視化 以及 那七個在 production 中編譯正常、卻會悄悄壞掉的 bug

如果你想要完整參考,我也有一份包含前後對照的完整逐步遷移指南:shubhra.dev/tutorials/nextjs-16-cache-components

你遇到的是哪一個?還是還有我這裡沒提到的問題?


原文出處:https://dev.to/shubhradev/nextjs-16-broke-my-app-in-4-places-and-none-of-them-threw-an-error-51mn


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝12   💬4   ❤️1
476
🥈
alicec
📝1   ❤️2
88
#4
我愛JS
💬1  
3
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登