Next.js App Router 全栈开发指南:服务器组件与路由
Next.js 是目前使用最广的 React 全栈框架。13 版引入的 App Router 把"目录即路由、服务器组件优先"确立为默认心智,到了 15、16 版,它已经是整个框架路由与数据层的基石,不再只是 Pages Router 的替代品。这篇指南从最小项目结构讲起,落到能直接运行的代码,最后给出从 Pages Router 迁移时的取舍,适合已经会写 React 组件、想全面切到 App Router 的开发者。
Next.js 属于前端搭建与建站技术两个分类的核心内容。想先补 React 新特性,可以读React 19 实战指南;首屏与 SSR 性能的深挖见Next.js SSR 性能优化。
从一个最小项目看目录约定
执行下面的命令会得到一个带 TypeScript、Tailwind、ESLint 的 App Router 项目,Turbopack 已经是默认打包器:
npx create-next-app@latest my-app --ts --tailwind --eslint --app
cd my-app && npm run dev
核心结构只有几个文件:
my-app/
├── app/
│ ├── layout.tsx # 根布局:必须包含 <html> 与 <body>
│ ├── page.tsx # 首页,对应 /
│ └── globals.css
├── public/
└── next.config.ts
App Router 的约定很直观:目录嵌套决定 URL 嵌套,每个路由目录里放特殊文件来声明它的行为。
| 文件 | 职责 |
|---|---|
layout.tsx |
共享布局,子页面切换时保留状态 |
page.tsx |
页面内容,对应可访问的路由 |
loading.tsx |
加载态,配合 Suspense 做流式渲染 |
error.tsx |
错误边界,能按段捕获异常 |
not-found.tsx |
404 页面 |
route.ts |
定义 API 路由(GET/POST 等) |
动态路由用方括号目录表达:app/blog/[slug]/page.tsx 匹配 /blog/hello-world,参数通过 params 拿到;想匹配任意多段可以用 [...slug]。布局支持无限嵌套,所以"站点导航 / 博客栏目 / 单篇文章"这类层级可以自然地映射成一棵组件树,每一层都有自己的 loading 与 error 边界。
服务器组件与客户端组件
app/ 下的组件默认都是服务器组件(Server Component)。写在里面的代码只在服务器执行,不会打包进浏览器:可以直接 await 数据库查询、读取文件、调用内部服务。只有需要交互、状态或浏览器 API 时,才在文件顶部加 "use client"。
一个典型的博客首页,在服务器组件里直连数据库渲染:
// app/blog/page.tsx —— 默认是服务器组件
export default async function BlogIndex() {
const posts = await db.post.findMany({ take: 20 });
return (
<ul>
{posts.map((p) => (
<li key={p.id}>
<a href={`/blog/${p.slug}`}>{p.title}</a>
</li>
))}
</ul>
);
}
需要点击的"点赞"按钮则放进客户端组件:
// app/blog/like-button.tsx
"use client";
export function LikeButton({ postId }: { postId: string }) {
const [count, setCount] = useState(0);
return <button onClick={() => setCount((c) => c + 1)}>赞 {count}</button>;
}
两个常见误区值得记下来:服务器组件里不能用 useState、onClick 或 useEffect,报错先检查是不是漏了 "use client";反过来,客户端组件里别放数据库连接和密钥。把"需要交互的叶子节点"标成客户端、其余保持服务器组件,是这套模型的核心纪律,也是它能显著减小打包体积的原因。
数据获取、缓存与流式渲染
服务器组件里直接 await 数据即可,配合 next/cache 控制缓存。Next.js 15 起 fetch 默认不再缓存,需要显式声明:
export default async function Page() {
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 3600 }, // 每小时后台重建一次
});
const posts = await res.json();
// ...
}
revalidate 相当于把 ISR 带进了组件级:页面按需重建,不必整站重新部署。更细粒度的做法是标签缓存——给"文章列表"打一个 tag,发布新文章后调用 revalidateTag('posts'),只让相关页面失效,其余缓存照常命中。
流式渲染(Streaming)解决的是"整页等一个慢接口"的问题。把页面拆成多个 Suspense 边界,每个边界先吐骨架屏、数据到位后再填内容,首字节(TTFB)明显变快,弱网下的感知速度也更好。loading.tsx 本质就是给整条路由提供一个默认的 Suspense 边界,所以它通常和异步数据获取配合使用。
从 Pages Router 迁移
迁移最怕推翻重来。App Router 可以和 Pages Router 共存,按路由渐进切换,不必一次搬完。对应关系大致如下:
| Pages Router | App Router |
|---|---|
pages/index.tsx |
app/page.tsx |
pages/blog/[id].tsx |
app/blog/[id]/page.tsx |
getServerSideProps |
服务器组件内直接取数 |
getStaticProps + revalidate |
fetch + next.revalidate |
_app.tsx |
根 layout.tsx |
pages/api/* |
app/api/*/route.ts |
迁移时最常踩的三个坑:一是 _app.tsx 里用组件包裹注入的全局布局,在 App Router 要改成嵌套 layout.tsx,否则子页面切换会丢状态;二是大量 useEffect 拉数据的组件,先想想能不能直接搬进服务器组件同步 await,通常能删掉一半样板代码;三是 next/router 的 API 要换成 next/navigation 的 useRouter,两者并不完全一致。
一个真实的落地案例
假设要重做一个"企业官网 + 内容博客 + 后台面板"。一个典型的 App Router 切法:
app/(site)/**:官网与博客。全部用服务器组件渲染,公开内容配合revalidate静态化,SEO 和首屏都有保障;app/(dashboard)/**:后台。登录后访问,在layout.tsx里统一做鉴权,表单与图表等交互组件集中在叶子节点;- 数据层:公开内容走服务器组件直连或缓存接口,写操作通过
route.ts暴露的 API 处理。
这种"路由分组 + 服务器组件优先"的结构比旧的"纯客户端渲染 + 全局状态"更容易维护,还能和PWA 实现指南搭配,把移动端体验再往前推一步。
常见问题
useState一用就报错? 组件默认是服务器组件,要交互就得加"use client"。loading.tsx不生效? 只有路由里有真正挂起的异步操作时才会显示,纯静态页面不会触发。- 数据一直不更新? Next.js 15+ 默认不缓存
fetch,需要显式revalidate或标签缓存。 - 和 Astro 怎么选? 需要大量动态交互的 SaaS 和后台,用 Next.js 更顺手;纯内容站追求极致静态可以看Astro 建站指南。
常规页面结构也可以参考本站的HTML 页面模板与响应式导航模板,把布局和样式先搭起来。
参考:Next.js 官方文档 https://nextjs.org/docs/app/getting-started/installation;App Router 数据获取与缓存 https://nextjs.org/docs/app/building-your-application/data-fetching