誤頁(yè)實(shí)戰(zhàn):404/500 與錯(cuò)誤邊界全解析)
Next.js 項(xiàng)目里的默認(rèn)錯(cuò)誤頁(yè)面說(shuō)實(shí)話就是那種最標(biāo)準(zhǔn)的“404 Not Found”白底黑字或者說(shuō)“Application error: a client-side exception has occurred”的提示。如果你只見(jiàn)過(guò)本地開(kāi)發(fā)環(huán)境沒(méi)上過(guò)線可能永遠(yuǎn)注意不到這事。但一旦用戶訪問(wèn)到某個(gè)失效鏈接或者服務(wù)端拋了一個(gè)未捕獲異常那個(gè)粗糙的默認(rèn)頁(yè)面會(huì)瞬間拉低產(chǎn)品質(zhì)感甚至讓用戶懷疑網(wǎng)站是不是掛了。這個(gè)項(xiàng)目就是圍繞 Next.js 中自定義錯(cuò)誤頁(yè)面展開(kāi)的核心覆蓋 App Router 和 Pages Router 兩套體系下的 404 頁(yè)面、500 頁(yè)面、錯(cuò)誤邊界組件error.js、global-error.js、not-found.js的實(shí)現(xiàn)方式。適合正在維護(hù) Next.js 項(xiàng)目的前端開(kāi)發(fā)者尤其是產(chǎn)品已經(jīng)上線、開(kāi)始關(guān)心用戶體驗(yàn)和可觀測(cè)性的團(tuán)隊(duì)。讀完你會(huì)知道為什么錯(cuò)誤頁(yè)面不能隨便寫(xiě)寫(xiě)以及如何做出一個(gè)既能挽留用戶、又能幫你定位問(wèn)題的錯(cuò)誤頁(yè)體系。1. 項(xiàng)目背景與需求解析為什么錯(cuò)誤頁(yè)面值得單獨(dú)做1.1 默認(rèn)錯(cuò)誤頁(yè)面的“原罪”我見(jiàn)過(guò)很多團(tuán)隊(duì)功能頁(yè)面精雕細(xì)琢但錯(cuò)誤頁(yè)面用的是框架默認(rèn)模板。不是說(shuō)默認(rèn)的不能用而是它有幾個(gè)實(shí)際痛點(diǎn)第一信息量完全不可控。默認(rèn) 404 頁(yè)面不會(huì)告訴用戶“你從哪里來(lái)、可以去哪里”用戶除了點(diǎn)擊瀏覽器返回按鈕沒(méi)有任何引導(dǎo)。如果這個(gè)用戶是第一次進(jìn)入你的網(wǎng)站大概率直接流失。第二無(wú)法傳遞品牌調(diào)性。一個(gè)服務(wù)中斷、突然報(bào)錯(cuò)這時(shí)候用戶是最焦慮的。如果錯(cuò)誤頁(yè)能延續(xù)網(wǎng)站的語(yǔ)言風(fēng)格和視覺(jué)體系焦慮感會(huì)明顯降低如果突然換成瀏覽器默認(rèn)樣式用戶會(huì)懷疑“我是不是被帶到了一個(gè)陌生網(wǎng)站”。第三排查問(wèn)題困難。默認(rèn) 500 頁(yè)面只有一個(gè)通用文案服務(wù)端異常的具體信息被吞掉了。開(kāi)發(fā)人員在用戶報(bào)障之后只能靠日志系統(tǒng)去反查而一套好的自定義錯(cuò)誤頁(yè)面本身就能配合日志、錯(cuò)誤上報(bào)把發(fā)生異常的上下文信息結(jié)構(gòu)化地記錄下來(lái)。這個(gè)項(xiàng)目的本質(zhì)需求其實(shí)就是三件事把 404 做成有引導(dǎo)的落地頁(yè)、把 500 做成可反饋可重試的中轉(zhuǎn)頁(yè)、把錯(cuò)誤信息變成可觀測(cè)的數(shù)據(jù)源。1.2 自定義錯(cuò)誤頁(yè)面的四個(gè)實(shí)際收益從業(yè)務(wù)價(jià)值角度看錯(cuò)誤頁(yè)面不是一個(gè)“做了就行”的邊緣功能它直接關(guān)聯(lián)四個(gè)指標(biāo)用戶留存給出相似導(dǎo)航、推薦內(nèi)容、搜索入口的 404 頁(yè)用戶跳出率會(huì)明顯低于只有一個(gè)“返回首頁(yè)”鏈接的頁(yè)面。用戶信任友好的錯(cuò)誤提示加上明確的“我們已知悉”類(lèi)文案比冷冰冰的異常堆棧更能維護(hù)產(chǎn)品口碑。開(kāi)發(fā)效率錯(cuò)誤頁(yè)把 digest、statusCode、cause 等字段渲染出來(lái)配合詳情內(nèi)容報(bào)障溝通成本大幅降低。合規(guī)與安全默認(rèn)錯(cuò)誤頁(yè)在某些框架下會(huì)透露框架版本號(hào)、內(nèi)部路徑等敏感信息自定義頁(yè)面能完全過(guò)濾掉這些細(xì)節(jié)。1.3 動(dòng)手前必須分清的路由模式Next.js 現(xiàn)在有兩套路由體系舊的 Pages Router 和新的 App Router。這也是新手最容易踩的第一個(gè)坑。Pages Router 的目錄是pages/自定義錯(cuò)誤的入口是404.js、500.js、_error.jsApp Router 的目錄是app/自定義錯(cuò)誤的入口是not-found.js、error.js、global-error.js。兩個(gè)體系的文件命名、組件形態(tài)、數(shù)據(jù)獲取方式差異很大如果你混著用很容易出現(xiàn)“我明明寫(xiě)了 404 頁(yè)面為什么不管用”的情況。我的建議是新項(xiàng)目直接用 App Router存量項(xiàng)目如果已經(jīng)在 Pages Router 上穩(wěn)定運(yùn)行也不必急著遷移先把對(duì)應(yīng)體系的錯(cuò)誤頁(yè)補(bǔ)齊才是當(dāng)務(wù)之急。下面兩個(gè)部分我會(huì)分別拆解這兩套方案。2. 核心原理拆解Next.js 到底是怎么處理錯(cuò)誤的2.1 App Router 的“三件套”error.js、not-found.js、global-error.jsApp Router 下錯(cuò)誤處理遵循“就近捕獲”原則由三個(gè)文件配合完成not-found.js處理兩類(lèi)情況。一類(lèi)是訪問(wèn)了不存在的 URL 路徑框架自動(dòng)渲染另一類(lèi)是你在服務(wù)端組件或路由處理程序里主動(dòng)調(diào)用notFound()函數(shù)手動(dòng)觸發(fā) 404 頁(yè)面。這個(gè)文件可以是服務(wù)端組件也可以加use client變成客戶端組件比較靈活。error.js處理路由段segment內(nèi)部拋出的運(yùn)行時(shí)錯(cuò)誤包括服務(wù)端組件里同步拋出的異常、客戶端組件事件處理中的渲染異常。它必須是一個(gè)客戶端組件文件頂部必須有use client。它接收兩個(gè) properror和reset。error對(duì)象里包含錯(cuò)誤信息和digest字段一個(gè)哈希值用于日志對(duì)賬reset是一個(gè)函數(shù)調(diào)用后會(huì)在不刷新整個(gè)頁(yè)面的情況下重渲染當(dāng)前路由段。global-error.js處理根布局root layout里拋出的錯(cuò)誤以及error.js沒(méi)有捕獲到的、向上冒泡到根節(jié)點(diǎn)的錯(cuò)誤。這個(gè)文件比較特殊因?yàn)樗鼤?huì)替換掉整個(gè)根布局所以組件里必須自己包含html和body標(biāo)簽。三者的捕獲邊界可以這樣理解not-found.js 處理“找不到”error.js 處理“當(dāng)前段出錯(cuò)了”global-error.js 處理“連布局都炸了”的終極情況。2.2 錯(cuò)誤邊界的嵌套邏輯React 本身有 Error Boundary 機(jī)制Next.js 在 App Router 里把它做了文件約定層面的封裝。每個(gè)路由段都可以有自己的error.js這個(gè)組件會(huì)包裹住該路由段下面的所有子頁(yè)面和子布局。嵌套規(guī)律是最內(nèi)層的 error.js 優(yōu)先捕獲。比如你有app/dashboard/和app/dashboard/analytics/兩個(gè)層級(jí)在 analytics 頁(yè)面的數(shù)據(jù)獲取里拋錯(cuò)只有 analytics 目錄下的 error.js 會(huì)觸發(fā)如果app/dashboard/layout.js本身拋錯(cuò)那么 dashboard 目錄下的 error.js 才能捕獲。如果一直向上冒泡都沒(méi)有對(duì)應(yīng)的 error.js最后會(huì)到 global-error.js。這個(gè)機(jī)制很重要它意味著你可以針對(duì)不同業(yè)務(wù)模塊做更精細(xì)的錯(cuò)誤提示而不是所有錯(cuò)誤都走到同一個(gè)頁(yè)面。2.3 notFound() 與狀態(tài)碼的關(guān)系notFound()是 Next.js 提供的一個(gè)服務(wù)端函數(shù)調(diào)用后不僅會(huì)渲染not-found.js還會(huì)向客戶端發(fā)送真實(shí)的 404 狀態(tài)碼。這一點(diǎn)對(duì) SEO 很關(guān)鍵因?yàn)樗阉饕媾老x(chóng)會(huì)依賴 HTTP 狀態(tài)碼判斷頁(yè)面是否存在如果你返回 200 但內(nèi)容顯示“沒(méi)找到”搜索引擎會(huì)認(rèn)為這是個(gè)軟 404長(zhǎng)期影響收錄質(zhì)量。另外注意一個(gè)細(xì)節(jié)如果你在客戶端組件里想觸發(fā) 404不能直接用notFound()它是服務(wù)端函數(shù)需要在服務(wù)端組件、服務(wù)端操作Server Action或路由處理器Route Handler里調(diào)用??蛻舳俗霾坏骄蛼佉粋€(gè)非 404 的異?;蛘哂胷edirect()路由到一個(gè)專門(mén)展示 404 的頁(yè)面。2.4 Pages Router 時(shí)代的傳統(tǒng)機(jī)制Pages Router 自定義錯(cuò)誤的入口要簡(jiǎn)單很多pages/404.js靜態(tài)渲染專門(mén)對(duì)應(yīng) 404 狀態(tài)碼。pages/500.js靜態(tài)渲染專門(mén)對(duì)應(yīng) 500 狀態(tài)碼。pages/_error.js動(dòng)態(tài)錯(cuò)誤頁(yè)可以處理任意狀態(tài)碼的錯(cuò)誤通過(guò)getInitialProps拿到statusCode后自行判斷渲染內(nèi)容。Pages Router 這套方案的問(wèn)題在于_error.js里的錯(cuò)誤處理比較粗糙——它能拿到狀態(tài)碼但拿不到詳細(xì)的錯(cuò)誤對(duì)象。而且它也不支持錯(cuò)誤邊界這種細(xì)粒度的捕獲方式一個(gè)錯(cuò)誤頁(yè)管所有錯(cuò)誤。這也是為什么 App Router 的“三件套”出來(lái)之后我傾向于推薦新項(xiàng)目用它。2.5 兩套體系的完整對(duì)照維度App RouterPages Router404 直接頁(yè)面app/not-found.jspages/404.js500 直接頁(yè)面app/error.js客戶端pages/500.js動(dòng)態(tài)兜底頁(yè)面app/global-error.jspages/_error.js局部錯(cuò)誤邊界每個(gè)路由段一個(gè) error.js不支持主動(dòng)觸發(fā) 404服務(wù)端調(diào)用notFound()pages/_error.js內(nèi)判斷狀態(tài)碼錯(cuò)誤對(duì)象error、digest、resetstatusCode、err布局修復(fù)支持global-error 可重建 html/body不支持3. 實(shí)操過(guò)程App Router 下實(shí)現(xiàn)一套完整的錯(cuò)誤頁(yè)面體系3.1 第一步設(shè)計(jì)信息結(jié)構(gòu)與視覺(jué)框架在寫(xiě)代碼之前我的習(xí)慣是先決定錯(cuò)誤頁(yè)要展示什么信息?;A(chǔ)元素包括狀態(tài)碼、錯(cuò)誤標(biāo)題、用戶可讀的說(shuō)明文字、行動(dòng)按鈕回首頁(yè)、重試、可選的錯(cuò)誤追蹤 ID。這里有一個(gè)取舍問(wèn)題是否把error.message直接渲染給用戶我建議是永遠(yuǎn)不要。錯(cuò)誤消息本身是給開(kāi)發(fā)者看的里面可能有數(shù)據(jù)庫(kù)連接串、文件路徑等敏感信息直接展示在頁(yè)面上不僅暴露內(nèi)部結(jié)構(gòu)用戶也完全看不懂。正確做法是把error.message和digest寫(xiě)入日志上報(bào)系統(tǒng)頁(yè)面上只展示一個(gè)脫敏的“追蹤編號(hào)”比如ERROR-20250512-8f3a91b2用戶反饋時(shí)把這個(gè)編號(hào)發(fā)給你你就能在日志系統(tǒng)里快速定位。3.2 第二步實(shí)現(xiàn) not-found.js以app/not-found.tsx為例一個(gè)實(shí)用的 404 頁(yè)面長(zhǎng)這樣import Link from next/link; export default function NotFound() { return ( main classNameflex min-h-screen flex-col items-center justify-center gap-6 px-4 text-center p classNametext-6xl font-bold text-slate-300404/p h1 classNametext-2xl font-medium你訪問(wèn)的頁(yè)面已經(jīng)搬遷或不存在/h1 p classNamemax-w-md text-slate-500 你可以回到首頁(yè)繼續(xù)瀏覽或者檢查一下網(wǎng)址是否輸入有誤。如果這個(gè)問(wèn)題一直出現(xiàn) 建議把頁(yè)面地址反饋給我們的值班同學(xué)。 /p div classNameflex gap-4 Link href/ classNamerounded-md bg-slate-900 px-5 py-2.5 text-white 回到首頁(yè) /Link Link href/search classNamerounded-md border px-5 py-2.5 去搜索 /Link /div /main ); }這個(gè)組件會(huì)在兩種場(chǎng)景下渲染路由根本不存在或者你在某個(gè)頁(yè)面里主動(dòng)調(diào)用了notFound()。如果業(yè)務(wù)上有需要你可以給這個(gè)文件加上use client在 404 頁(yè)面里做一些客戶端交互。但我個(gè)人建議保持它是服務(wù)端組件服務(wù)端渲染更快也能享受流式傳輸?shù)膬?yōu)化。3.3 第三步實(shí)現(xiàn)帶重置能力的 error.jsapp/error.tsx必須是客戶端組件代碼結(jié)構(gòu)上要注意三點(diǎn)文件頂端必須有use client、組件簽名接收{(diào) error, reset }、不能包含html或body標(biāo)簽?zāi)鞘?global-error.js 的職責(zé)。use client; import Link from next/link; import { useEffect } from react; export default function ErrorPage({ error, reset, }: { error: Error { digest?: string }; reset: () void; }) { useEffect(() { // 這里把錯(cuò)誤上下文發(fā)送到監(jiān)控平臺(tái) console.error(路由段渲染異常, { message: error.message, digest: error.digest, path: window.location.pathname, time: new Date().toISOString(), }); }, [error]); return ( main classNameflex min-h-screen flex-col items-center justify-center gap-6 px-4 text-center p classNametext-6xl font-bold text-slate-300500/p h1 classNametext-2xl font-medium頁(yè)面開(kāi)小差了/h1 p classNamemax-w-md text-slate-500 我們的服務(wù)短暫遇到了問(wèn)題稍微等一下再試試。 /p p classNametext-sm text-slate-400追蹤編號(hào){error.digest ?? 未知}/p div classNameflex gap-4 button onClick{reset} classNamerounded-md bg-slate-900 px-5 py-2.5 text-white 重新加載 /button Link href/ classNamerounded-md border px-5 py-2.5 回到首頁(yè) /Link /div /main ); }reset函數(shù)的語(yǔ)義值得展開(kāi)說(shuō)它不會(huì)重新刷新整個(gè)瀏覽器頁(yè)面而是讓 React 重新渲染發(fā)生錯(cuò)誤的那個(gè)路由段。這聽(tīng)起來(lái)很美好但有一個(gè)坑——如果錯(cuò)誤來(lái)源于某個(gè)外部服務(wù)的臨時(shí)故障比如接口超時(shí)重渲染大概率還是會(huì)失敗。所以更好的體驗(yàn)是onClick里先調(diào)用reset()同時(shí)設(shè)置一個(gè)標(biāo)志位如果再次觸發(fā)同一個(gè)錯(cuò)誤就引導(dǎo)用戶刷新整個(gè)頁(yè)面。3.4 第四步實(shí)現(xiàn)兜底的 global-error.jsapp/global-error.tsx是最后一道防線。它捕獲的是根布局app/layout.tsx里的錯(cuò)誤也就是渲染骨架本身出錯(cuò)時(shí)的情況。因?yàn)楦季忠呀?jīng)崩了這個(gè)組件只能自己輸出完整的 HTML 結(jié)構(gòu)use client; export default function GlobalError({ error, reset, }: { error: Error; reset: () void; }) { return ( html langzh-CN body main style{{ minHeight: 100vh, display: flex, flexDirection: column, alignItems: center, justifyContent: center, gap: 16px, fontFamily: system-ui, sans-serif, }} h1服務(wù)暫時(shí)不可用/h1 button onClick{reset} style{{ padding: 10px 20px, borderRadius: 8px, border: 1px solid #ccc, background: #fff, cursor: pointer, }} 重試 /button /main /body /html ); }注意兩點(diǎn)根布局里引用的全局樣式比如 Tailwind 的globals.css在 global-error.js 里加載不到因?yàn)?root layout 已經(jīng)失效了。所以這個(gè)文件里盡量用內(nèi)聯(lián)樣式或者簡(jiǎn)單的style標(biāo)簽別依賴外部樣式表。另外這個(gè)文件也接收error和reset但它的reset語(yǔ)義更弱一些我實(shí)際測(cè)試下來(lái)觸發(fā)全局錯(cuò)誤后reset能成功的情況非常少通常還是走整頁(yè)刷新。3.5 第五步處理嵌套路由的局部錯(cuò)誤在一個(gè)中大型項(xiàng)目里你大概率不會(huì)只寫(xiě)一個(gè)頂層 error.js而是按模塊拆分。比如訂單中心和用戶中心是兩個(gè)完全不同的業(yè)務(wù)域用戶在訂單列表頁(yè)遇到網(wǎng)絡(luò)錯(cuò)誤時(shí)希望看到的文案是“訂單數(shù)據(jù)加載失敗”用戶中心則是“賬號(hào)信息同步失敗”。做法很簡(jiǎn)單在對(duì)應(yīng)路由目錄下各自新建一個(gè)error.js即可。還要注意一個(gè)細(xì)節(jié)loading.js和error.js是配套存在的。錯(cuò)誤如果出現(xiàn)在服務(wù)端數(shù)據(jù)獲取階段Next.js 會(huì)先展示loading.js如果在相同路由段定義了然后異步渲染完成后再?zèng)Q定是展示頁(yè)面還是觸發(fā)error.js。這個(gè)機(jī)制能避免用戶面對(duì)一個(gè)長(zhǎng)時(shí)間空白屏幕體驗(yàn)上比直接白屏要好得多。3.6 樣式適配讓錯(cuò)誤頁(yè)融入整體設(shè)計(jì)錯(cuò)誤頁(yè)最容易犯的設(shè)計(jì)錯(cuò)誤就是“用力過(guò)猛”。我見(jiàn)過(guò)不少團(tuán)隊(duì)把動(dòng)漫插畫(huà)、大型動(dòng)畫(huà)塞進(jìn) 404 頁(yè)面結(jié)果因?yàn)閯?dòng)畫(huà)庫(kù)加載失敗反而觸發(fā)二次錯(cuò)誤。穩(wěn)健的樣式方案包含這幾個(gè)原則純靜態(tài)視覺(jué)不要依賴 JS 動(dòng)畫(huà)庫(kù)、使用全局已有的 CSS 變量和設(shè)計(jì) token、保證高對(duì)比度文案、移動(dòng)端單列布局。如果你在用 Tailwind直接沿用項(xiàng)目里的工具類(lèi)就行錯(cuò)誤頁(yè)也是常規(guī)模板的一部分不屬于需要特殊隔離的場(chǎng)景。暗色模式適配要特別注意。如果你的站點(diǎn)支持明暗主題錯(cuò)誤頁(yè)里的自定義色值務(wù)必和主題系統(tǒng)打通否則用戶切到暗色模式后再訪問(wèn) 404 頁(yè)面會(huì)出現(xiàn)一個(gè)刺眼的白色錯(cuò)誤塊。4. 兼容方案Pages Router 下的自定義錯(cuò)誤頁(yè)實(shí)現(xiàn)4.1 簡(jiǎn)單的 404 靜態(tài)頁(yè)面如果你的項(xiàng)目還在用 Pages Router方案文件結(jié)構(gòu)如下。首先是pages/404.jsimport Link from next/link; export default function Custom404() { return ( div style{{ textAlign: center, padding: 80px 20px }} h1404/h1 p頁(yè)面不存在或已被移除。/p Link href/回到首頁(yè)/Link /div ); }這個(gè)文件在構(gòu)建時(shí)就被靜態(tài)化了性能很好。但它的局限在于所有 404 都走同一個(gè)頁(yè)面無(wú)法按模塊區(qū)分。4.2 動(dòng)態(tài)兜底頁(yè)面 _error.jspages/_error.js的作用更像一個(gè)動(dòng)態(tài)分發(fā)器。它會(huì)在項(xiàng)目拋出任意未捕獲錯(cuò)誤時(shí)被調(diào)用通過(guò)getInitialProps拿到statusCode和err對(duì)象function ErrorPage({ statusCode }) { return ( div style{{ textAlign: center, padding: 80px 20px }} h1{statusCode || 客戶端錯(cuò)誤}/h1 p {statusCode ? 服務(wù)端返回了錯(cuò)誤狀態(tài)碼 ${statusCode} : 頁(yè)面在客戶端渲染時(shí)出現(xiàn)了未知問(wèn)題} /p a href/回到首頁(yè)/a /div ); } ErrorPage.getInitialProps async ({ res, err }) { const statusCode res ? res.statusCode : err ? err.statusCode : 404; return { statusCode }; }; export default ErrorPage;這里有個(gè)坑Pages Router 會(huì)把_error.js用于所有錯(cuò)誤包括服務(wù)端渲染錯(cuò)誤和客戶端渲染錯(cuò)誤??蛻舳藗?cè)的err對(duì)象往往信息不全所以getInitialProps里的兜底邏輯必須健壯。另外_error.js本身也會(huì)被用于 404 和 500如果你同時(shí)定義了404.js和500.js它們會(huì)優(yōu)先于_error.js被使用。4.3 Pages Router 與 App Router 的取舍建議從工程實(shí)踐角度我對(duì)兩套方案的使用推薦是新項(xiàng)目無(wú)腦 App Router老項(xiàng)目如果只是簡(jiǎn)單靜態(tài)站Pages Router 的404.js500.js完全夠用如果老項(xiàng)目已經(jīng)深度使用getServerSideProps、有復(fù)雜的自定義服務(wù)器邏輯遷移 App Router 的成本需要單獨(dú)評(píng)估不必為了錯(cuò)誤頁(yè)而強(qiáng)行遷移。但無(wú)論哪套方案錯(cuò)誤頁(yè)的“狀態(tài)碼正確性”都是底線。我用一個(gè)簡(jiǎn)單命令就能檢查線上部署是否正常curl -o /dev/null -w %{http_code} https://你的域名/一個(gè)不存在的路徑期望輸出是404如果輸出200說(shuō)明錯(cuò)誤頁(yè)沒(méi)有走框架的 404 邏輯而是被某個(gè)中間件、重寫(xiě)規(guī)則或者自定義服務(wù)器吞掉了這種情況必須排查。5. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄5.1 error.js 沒(méi)生效頁(yè)面直接白屏這種狀況我遇到好幾次每次都是同一個(gè)原因文件頂部忘記寫(xiě)use client。error.js的本質(zhì)是一個(gè) React Error BoundaryError Boundary 只能存在于客戶端組件體系中。如果你把它寫(xiě)成服務(wù)端組件Next.js 不會(huì)報(bào)編譯錯(cuò)誤但運(yùn)行時(shí)它無(wú)法捕獲錯(cuò)誤最終就會(huì)走到 global-error.js。排查方法先看瀏覽器控制臺(tái)有沒(méi)有報(bào)錯(cuò)再看代理網(wǎng)絡(luò)面板里返回的狀態(tài)碼。如果狀態(tài)碼是 500 但頁(yè)面顯示的是 global-error 的內(nèi)容那就是 error.js 所在的層級(jí)不對(duì)或者根本缺失。5.2 global-error.js 樣式全亂global-error.js會(huì)替換根布局所以它自己必須包含html和body標(biāo)簽。但全局樣式文件globals.css是在根布局里被 import 的根布局崩了樣式自然也就不在了。我的經(jīng)驗(yàn)是在global-error.js里不要依賴任何外部樣式全部用內(nèi)聯(lián)樣式或者內(nèi)置一個(gè)極簡(jiǎn)style標(biāo)簽。雖然代碼看起來(lái)沒(méi)那么優(yōu)雅但它是最后兜底環(huán)——兜底的東西要求的就是簡(jiǎn)單、可靠、不依賴外部資源。5.3 點(diǎn)擊“重新加載”后還是同一個(gè)錯(cuò)誤reset()重新渲染路由段時(shí)并不會(huì)重置 React 組件的本地狀態(tài)。如果錯(cuò)誤發(fā)生在某個(gè)客戶端組件的useEffect里而這個(gè)組件的依賴數(shù)組沒(méi)變化那么重渲染后可能立刻再次觸發(fā)相同錯(cuò)誤。我處理這種問(wèn)題時(shí)會(huì)在error.tsx里做一版“失敗計(jì)數(shù)”const [retryCount, setRetryCount] useState(0); async function handleReset() { if (retryCount 1) { window.location.reload(); return; } setRetryCount((c) c 1); reset(); }這個(gè)思路就是“給重試限次”第一次失敗后按鈕文案改成“刷新頁(yè)面”引導(dǎo)用戶整頁(yè)重新加載避免在兩個(gè)錯(cuò)誤頁(yè)之間反復(fù)橫跳。5.4 404 頁(yè)面的 HTTP 狀態(tài)碼不是 404出現(xiàn)這種情況常見(jiàn)于你在app/not-found.js里寫(xiě)了一個(gè)會(huì)執(zhí)行跳轉(zhuǎn)的客戶端邏輯或者你的next.config.js里配置了rewrites規(guī)則把不存在的路徑重寫(xiě)到了某個(gè)頁(yè)面。另一個(gè)常見(jiàn)原因是中間件middleware里對(duì)路徑做了兜底處理直接返回頁(yè)面內(nèi)容。驗(yàn)證方法是先臨時(shí)注釋掉所有 rewrite 和 middleware 邏輯再測(cè)試一個(gè)不存在的深層路徑例如/a/b/c/nonexist。如果這時(shí)狀態(tài)碼正確變成 404那問(wèn)題就在重寫(xiě)規(guī)則或中間件邏輯上。5.5 局部 error.js 一直不觸發(fā)報(bào)錯(cuò)總是跑到頂層如果你的子路由目錄下已經(jīng)有error.js但錯(cuò)誤還是冒泡到了app/error.js排查順序如下子目錄的error.js是否出現(xiàn)在正確的分段目錄下不是子頁(yè)面里的page.jspage 文件自身不能命名為 error。子目錄的page.tsx是否沒(méi)有自己執(zhí)行數(shù)據(jù)獲取反而在布局layout.tsx里做異步請(qǐng)求——布局拋錯(cuò)會(huì)冒泡到布局所在段的 error.js而不是頁(yè)面所在段的。子目錄的error.js里是否也拋出了異常錯(cuò)誤邊界自己出錯(cuò)了會(huì)繼續(xù)向上傳播。注意error.js 組件本身如果拋錯(cuò)會(huì)直接跳過(guò)同一層次的捕獲向上層冒泡。所以寫(xiě) error.js 的時(shí)候組件內(nèi)部邏輯要格外簡(jiǎn)單不要在里面做網(wǎng)絡(luò)請(qǐng)求、狀態(tài)管理等復(fù)雜操作。5.6 需要后臺(tái)管理的“錯(cuò)誤上報(bào)”怎么做錯(cuò)誤頁(yè)只是前端展示層真正的價(jià)值在于和日志系統(tǒng)聯(lián)動(dòng)。在app/error.tsx的useEffect里向監(jiān)控平臺(tái)發(fā)送事件時(shí)至少包含以下字段error.message原始錯(cuò)誤、error.digest框架生成的追蹤 ID、pathname當(dāng)前路徑、renderTime渲染時(shí)間戳、source標(biāo)識(shí)來(lái)自 error.js 還是 global-error.js。error.digest這個(gè)字段非常實(shí)用它是 Next.js 服務(wù)端錯(cuò)誤和客戶端錯(cuò)誤共用的哈希標(biāo)識(shí)。服務(wù)端日志和前端上報(bào)通過(guò) digest 關(guān)聯(lián)用戶報(bào)障時(shí)只需要把頁(yè)面上的“追蹤編號(hào)”發(fā)給你你就能在日志平臺(tái)里精確搜索到當(dāng)時(shí)完整的錯(cuò)誤堆棧。6. 后續(xù)可以怎么擴(kuò)展這套方案我做這一套錯(cuò)誤頁(yè)體系時(shí)最后臨時(shí)加的幾樣?xùn)|西反而成了最常用的部分。第一個(gè)是“健康檢查專用路徑”在平臺(tái)做可用性監(jiān)控時(shí)走獨(dú)立路由判斷整個(gè)應(yīng)用的基礎(chǔ)依賴是否可用這個(gè)和用戶側(cè)的 404/500 頁(yè)面互不干擾但對(duì)線上穩(wěn)定性判斷很有幫助。第二個(gè)是“錯(cuò)誤頁(yè) A/B 文案池”不同業(yè)務(wù)狀態(tài)下用戶需要的引導(dǎo)不同。比如大促期間 404 頁(yè)面引導(dǎo)到活動(dòng)主會(huì)場(chǎng)平時(shí)引導(dǎo)到搜索頁(yè)。這個(gè)用not-found.js里加一個(gè)簡(jiǎn)單的服務(wù)端配置讀取就能實(shí)現(xiàn)不需要為了文案變化頻繁發(fā)版。第三個(gè)是“CSP 兼容性檢查”在做內(nèi)容安全策略時(shí)錯(cuò)誤頁(yè)展示的內(nèi)聯(lián)樣式、上報(bào)腳本的域名白名單都容易成為漏網(wǎng)之魚(yú)。我踩過(guò)一次坑錯(cuò)誤頁(yè)上報(bào)接口被 CSP 攔截結(jié)果就是頁(yè)面正常顯示了但日志里完全沒(méi)有錯(cuò)誤記錄。上線前用瀏覽器控制臺(tái)看一眼有沒(méi)有 CSP 違規(guī)提示這個(gè)動(dòng)作應(yīng)該寫(xiě)進(jìn)發(fā)布清單里。根據(jù)我在實(shí)際項(xiàng)目里的經(jīng)驗(yàn)自定義錯(cuò)誤頁(yè)面這件事最容易被低估但它的性價(jià)比其實(shí)極高——寫(xiě)一個(gè)頁(yè)面用的時(shí)間不過(guò)半天換來(lái)的是用戶流失降低、線上問(wèn)題定位提速、以及品牌調(diào)性在所有場(chǎng)景下的統(tǒng)一。你如果正在規(guī)劃這個(gè)功能我的建議是先做通 App Router 的“三件套”再補(bǔ)上全局錯(cuò)誤上報(bào)日志字段這兩個(gè)完成之后這套體系基本就立住了。