
Next.js App Router 约定式文件实战:loading、error、not-found 怎么兜住加载态与异常用 Next.js App Router 写页面,你迟早会遇到这三个问题:页面里await fetch(...)拉数据时,用户盯着一片空白,不知道是在加载还是卡死了。接口挂了或抛异常,整个页面直接白屏崩掉,还可能把错误堆栈暴露给用户。用户访问了一个不存在的资源(比如/posts/99999),你想给个像样的 404 页,而不是默认那个丑页面。很多人第一反应是在组件里手写isLoading、try/catch、if (!data) return NotFound/。但 App Router 提供了约定式文件——在路由目录里放特定文件名,框架自动帮你接管加载态、错误态和 404。这篇把loading.tsx、error.tsx、not-found.tsx三个一次讲透。朴素写法:所有状态挤在一个组件里先看不用约定式文件时,一个详情页大概长这样:// app/posts/[id]/page.tsx —— 反面教材 use client; import { useEffect, useState } from react; export default function PostPage({ params }: { params: { id: string } }) { const [post, setPost] useState(null); const [loading, setLoading] useState(true); const [error, setError] useState(null); useEffect(() { fetch(/api/posts/${params.id}) .then(r { if (!r.ok) throw new Error(加载失败); return r.json(); }) .then(setPost) .catch(setError) .finally(() setLoading(false)); }, [params.id]); if (loading) return p加载中.../p; if (error) return p出错了/p; if (!post) return p找不到文章/p; return articleh1{post.title}/h1p{post.body}/p/article; }问题很明显:业务渲染只有最后一行,前面全是状态判断的噪音;而且被迫写成客户端组件(use client),丢掉了服务端组件直接await数据的能力。App Router 的约定式文件就是来拆掉这些样板的。loading.tsx:自动加载态,基于 Suspense在路由目录放一个loading.tsx,Next.js 会自动用它包一层 Suspense。当同级page.tsx(服务端组件)在await数据时,先渲染loading.tsx的内容,数据好了再换成真实页面:// app/posts/[id]/loading.tsx export default function Loading() { // 骨架屏,比加载中...体验好得多 return ( div classNameanimate-pulse space-y-4 div classNameh-8 w-2/3 bg-gray-200 rounded / div classNameh-4 w-full bg-gray-200 rounded / div classNameh-4 w-5/6 bg-gray-200 rounded / /div ); }有了它,page.tsx就能回归纯粹的服务端组件,直接await,不用管 loading:// app/posts/[id]/page.tsx —— 服务端组件,直接 await export default async function PostPage({ params, }: { params: Promise{ id: string }; }) { const { id } await params; // Next.js 15 起 params 是 Promise const res await fetch(https://api.example.com/posts/${id}); const post await res.json(); return ( article h1{post.title}/h1 p{post.body}/p /article ); }关键理解:loading.tsx的本质就是给page.tsx套了Suspense fallback{Loading/}。所以它对「路由跳转时的加载」和「组件 await 期间」都生效,你什么都不用手写。error.tsx:兜住渲染异常,必须是客户端组件如果page.tsx里的 fetch 抛了异常,或者渲染过程报错,同级的error.tsx会接住它,展示降级 UI,而不是整页白屏:// app/posts/[id]/error.tsx use client; // ← error.tsx 必须是客户端组件,这是硬性要求 import { useEffect } from react; export default function Error({ error, reset, }: { error: Error { digest?: string }; reset: () void; // 调它会重新渲染这段路由,相当于重试 }) { useEffect(() { // 上报到监控系统,别把错误吞了 console.error(页面渲染出错:, error); }, [error]); return ( div classNamep-6 text-center h2 出了点问题/h2 p classNametext-gray-500{error.message}/p button onClick{() reset()} classNamemt-4 px-4 py-2 bg-blue-600 text-white rounded 重试 /button /div ); }有两个坑要记牢:坑一:error.tsx必须加use client。它内部用 React 错误边界实现,错误边界只能是客户端组件。忘了加,构建直接报错。坑二:error.tsx抓不到同级layout.tsx的错误。因为 error 边界包在 layout 内部,layout 本身抛错它管不着。要兜住 layout 的错误,得在上一级目录放error.tsx,或者用global-error.tsx兜住根 layout。reset()函数是 App Router 特有的:调用它会尝试重新渲染出错的这段路由子树,给用户一个「重试」而不用刷新整页。not-found.tsx notFound():优雅的 404资源不存在时,别手动return div404/div。App Router 提供了notFound()函数,调用它会立即中断渲染,并渲染最近的not-found.tsx:// app/posts/[id]/page.tsx import { notFound } from next/navigation; export default async function PostPage({ params, }: { params: Promise{ id: string }; }) { const { id } await params; const res await fetch(https://api.example.com/posts/${id}); if (res.status 404) { notFound(); // ← 中断渲染,跳到 not-found.tsx,后面代码不会执行 } const post await res.json(); return articleh1{post.title}/h1p{post.body}/p/article; }// app/posts/[id]/not-found.tsx import Link from next/link; export default function NotFound() { return ( div classNamep-6 text-center h2文章不存在/h2 p classNametext-gray-500你要找的文章可能已被删除。/p Link href/posts classNametext-blue-600 underline 返回文章列表 /Link /div ); }notFound()的原理是抛出一个特殊错误,被框架捕获后渲染not-found.tsx,同时响应状态码正确返回 404——这点对 SEO 很重要,搜索引擎知道这是不存在的页面,不会收录。手写div404/div的话状态码还是 200,搜索引擎会以为是正常页。它们的嵌套关系:一张图理清这几个约定式文件不是平级的,它们的包裹顺序是固定的。同一个路由段里,Next.js 大致这样嵌套:layout error ← 兜住下面的渲染错误 suspense fallback{loading} ← loading 兜住加载态 not-found 边界 page ← 你的实际页面 /not-found /suspense /error /layout从这张图能推出几条实用结论:loading和error都定义在路由段上,子路由可以有自己的一套,就近生效。error在layout内层,所以管不到同级 layout 的错误(要放上一级)。not-found既能被notFound()主动触发,也会兜住未匹配的路由。小结App Router 的三个约定式文件,把过去挤在组件里的状态处理拆成了框架职责:loading.tsx:自动 Suspense fallback,让page.tsx回归纯服务端组件直接await,写骨架屏体验最佳。error.tsx:错误边界,必须use client;提供reset()做重试;抓不到同级 layout 的错误,要放上一级。not-found.tsxnotFound():优雅 404,且返回正确的 404 状态码,对 SEO 友好;notFound()会中断后续渲染。记忆点:别再手写isLoading / try-catch / 404 判断——在路由目录里放对文件名,框架自动帮你套好 Suspense 和错误边界。