App Router 一上来就是一堆新词:server component、client component、layout、route handler、parallel route、intercepting route、server action。对一个独立内容站或小型 SaaS 来说,8 个概念基本覆盖了你日常会碰到的 95%。把这些学扎实,剩下的等真要用到再说。
本文对应 Next.js 16(截至 2026 年 6 月最新补丁是 16.2.7,16.2 于 2026 年 3 月发布)。这个版本有两点会让很多老教程失效:params 和 searchParams 现在是异步的;过去”fetch 默认缓存一切”的行为,正被一套显式、opt-in 的缓存模型取代。下面都会讲到,而且代码是真能在 16 上编译通过的。
一句话总结
app/下的文件默认是 server component;只有需要 state、effect 或浏览器事件时才加'use client'。- 文件夹结构就是 URL。
layout.tsx、page.tsx、loading.tsx、not-found.tsx是文件约定,不是配置项。 - 在 Next.js 16 里,
params、searchParams、cookies()、headers()都是异步的,必须await,否则 build 直接报错。 - 缓存现在是显式的:默认动态,靠
'use cache'(Cache Components)或fetch选项 opt-in。旧的隐式fetch缓存没了。 - Server Action(
'use server')替你省掉了大部分手写的/api/*路由——表单提交和 mutation 都能用。
App Router 在 2026 年 6 月的现状
App Router 在 Next.js 13(2023)落地,13.4 转稳定,如今 create-next-app 新建的每个项目都默认用它。Next.js 16(2025 年 10 月)把 Turbopack 设成默认打包器——零配置就能拿到最高 10 倍的 Fast Refresh 和 2–5 倍的生产构建提速。Pages Router 还能用,也能和 App Router 共存,但所有新能力(Partial Prerendering、Cache Components、server action、路由系统重写)都先落在 App Router 上。
它的心智模型和 Pages Router 差别足够大,“我会 React”并不够用。最容易绊倒人的四点转变:渲染默认在服务端、文件系统就是路由、请求期数据现在是异步的、缓存从自动变成了 opt-in。
你撞上其中某个概念的信号
- build 报错
You're importing a component that needs 'useState'. It only works in a Client Component——你越过了 server / client 边界。 - 你看到
Route used params.slug. params should be awaited before using its properties——你在 Next.js 15+,却还把params当普通对象用。 - 在
layout.tsx的子树内导航,它从不重新渲染——这是设计意图,就是共享 layout 的行为。 - 一个
loading.tsx文件莫名其妙控制了 Suspense 兜底——文件约定在这里是真实、承重的概念。
8 个概念
1. 默认 Server Component
app/ 下每个文件默认都是 server component,除非首行写了 'use client'。Server component 能直接 await 数据,且给浏览器发送零 JavaScript:
// app/page.tsx —— server component
import { db } from '@/lib/db';
export default async function HomePage() {
const posts = await db.post.findMany({ take: 10 });
return (
<ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
);
}
2. Client Component
'use client' 让这个文件(以及它 import 的一切)进入客户端 bundle。useState、useEffect、事件处理函数都需要它:
'use client';
import { useState } from 'react';
export function Counter() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>计数:{n}</button>;
}
在 server component 里随便 import client component;反过来不行——要把服务端渲染好的结果通过 children 往下传:
// app/page.tsx(server)—— 把服务端渲染的子节点传进客户端壳
import { ClientShell } from './ClientShell';
import { ServerData } from './ServerData';
export default function Page() {
return <ClientShell><ServerData /></ClientShell>;
}
3. 文件系统路由
文件夹结构就是 URL,没有路由配置文件:
app/
├── layout.tsx → 包住所有页
├── page.tsx → /
├── blog/
│ ├── layout.tsx → 包住 /blog/*
│ ├── page.tsx → /blog
│ └── [slug]/
│ ├── page.tsx → /blog/:slug
│ ├── loading.tsx → Suspense 兜底
│ └── not-found.tsx → 这段路由内的 404
└── (marketing)/ → route group,不进 URL
├── about/page.tsx → /about
└── pricing/page.tsx → /pricing
4. Layout
Layout 包住它的子树,且在子树内导航时不重新渲染。这种”保留”正是要点——它让共享的导航、侧栏、客户端 state 在切页时一直活着:
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh">
<body>
<nav>...</nav> {/* 切换 route 时保留 */}
{children}
</body>
</html>
);
}
5. 异步的动态 API(Next.js 15/16 的关键改动)
这是抄老教程时最大的坑。从 Next.js 15 起、并在 16 中强制:params、searchParams、cookies()、headers()、draftMode() 全是 Promise,必须 await。同步解构 params 的老代码现在会 build 失败。
// app/articles/[slug]/page.tsx —— Next.js 16 的签名
export default async function ArticlePage(
{ params }: { params: Promise<{ slug: string }> }
) {
const { slug } = await params; // ✅ 要 await
const article = await getArticle(slug);
return <article><h1>{article.title}</h1></article>;
}
在 client component 里没法 await,就用 React 的 use hook 把 Promise 拆开:
'use client';
import { use } from 'react';
export function Filters({ searchParams }: { searchParams: Promise<{ q?: string }> }) {
const { q } = use(searchParams);
return <span>搜索:{q}</span>;
}
迁移时,官方 codemod 能自动改掉大部分调用点:
npx @next/codemod@canary next-async-request-api .
6. 静态 / 动态 / ISR
路由默认静态。一旦读取请求期数据——await cookies()、await headers() 或 await searchParams——这个路由就变动态。增量静态再生成(ISR)就一句 export const revalidate:
// app/articles/[slug]/page.tsx
export const revalidate = 3600; // ISR:最多每小时再生成一次
export async function generateStaticParams() {
const articles = await getAllArticles();
return articles.map(a => ({ slug: a.slug })); // 预构建这些路径
}
generateStaticParams 告诉 Next 哪些动态路径在构建时渲染;没列进去的,会在首次请求时生成(之后按你的 revalidate 缓存)。
7. 缓存:现在是显式的(Cache Components)
老版本 App Router 里,fetch() 结果被隐式缓存,这让不少人措手不及。Next.js 16 把默认值反过来了:一切默认动态(请求期渲染),除非你主动 opt-in。你有三种 opt-in 工具。
HTTP 请求继续用 fetch 选项:
// 每次请求都拉新(16 的默认行为)
const data = await fetch(url, { cache: 'no-store' });
// 按时间重新校验:最多每 60 秒一次
const data = await fetch(url, { next: { revalidate: 60 } });
// 按 tag:之后用 revalidateTag('posts', 'max') 失效
const data = await fetch(url, { next: { tags: ['posts'] } });
要缓存整个函数或组件(不只是 fetch),Next.js 16 加了 'use cache' 指令,靠 next.config.ts 里的 cacheComponents: true 开启。编译器会根据函数输入自动推导缓存 key:
// lib/posts.ts
export async function getPopularPosts() {
'use cache';
return db.post.findMany({ orderBy: { views: 'desc' }, take: 5 });
}
失效方式也变了。从 16 起,revalidateTag 第二个参数要传 cacheLife profile 来启用 stale-while-revalidate;Server Action 里还多了 updateTag 用于 read-your-writes:
import { revalidateTag } from 'next/cache';
revalidateTag('posts', 'max'); // 先发旧数据,后台再刷新
8. Server Action
标了 'use server' 的函数能从 client component 调用,但只在服务端运行。它替你省掉大部分手写的 /api/* 路由:
// app/actions.ts
'use server';
import { updateTag } from 'next/cache';
import { db } from '@/lib/db';
export async function createPost(formData: FormData) {
const title = formData.get('title')?.toString() ?? '';
await db.post.create({ data: { title } });
updateTag('posts'); // read-your-writes:调用方立刻看到新帖
}
// app/new/page.tsx —— 直接接到 form 的 action 上
import { createPost } from '../actions';
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" />
<button type="submit">保存</button>
</form>
);
}
两分钟搞定 Metadata
它不在这 8 个里,但你建内容站第一天就会用到。静态页 export 一个 typed metadata 对象,动态的按路由用 generateMetadata。注意这里 params 同样是异步签名:
// app/articles/[slug]/page.tsx
import type { Metadata } from 'next';
export async function generateMetadata(
{ params }: { params: Promise<{ slug: string }> }
): Promise<Metadata> {
const { slug } = await params;
const article = await getArticle(slug);
return {
title: article.title,
description: article.description,
alternates: { canonical: `/articles/${slug}/` },
openGraph: { title: article.title, url: `/articles/${slug}/` },
};
}
容易踩的坑
- “保险起见”每个文件顶部都加
'use client'。 你等于把站点退化成 SPA,丢掉了服务端渲染、更小的 bundle 和直接取数。 - 在 client component 里 import 仅限服务端的库。 轻则 build 报错,重则把密钥泄漏进浏览器 bundle。给绝不能进客户端的模块加
import 'server-only'。 - 把
params/searchParams当普通对象。 在 16 上这是硬性 build 失败(should be awaited)。永远await。 - 指望
layout.tsx在导航时重新渲染。 它不会,这是设计如此。按页变化的 state 放在 page 里,不要放 layout。 - 以为
fetch还会自动缓存。 在 16 上不会了——用cache、next.revalidate或'use cache'明确表态。 - 还在用
middleware.ts。 16 里它被废弃,换成proxy.ts(逻辑不变、跑 Node.js runtime、名字更清楚)。把文件名和导出的函数都改掉。 - 把 Server Action 当免费 RPC 跑重活。 它底层就是 HTTP POST,和任何 serverless 函数一样有冷启动代价。
这篇适合谁
2026 年新开 Next.js 项目,或者从 Pages Router 迁过来、想搞清到底变了什么的人。如果你已有 Pages Router 项目又没迁移动力,App Router 能共存,但完整转换是真活——先跑 async-API codemod,再一条路由一条路由地迁。
FAQ
- 要不要单独学 React Server Components?: 不用。RSC 是 App Router 的地基,学 App Router 就是在学 RSC,不需要再找一份单独的 RSC 教程。
- 我的老
params代码怎么突然坏了?: Next.js 15 把params和searchParams变成了异步 Promise,16 强制执行。把组件改成async并await params,或者跑npx @next/codemod@canary next-async-request-api .。 - 还能用
getStaticProps/getServerSideProps吗?: 只在 Pages Router 能。App Router 把两者都替代了:asyncserver component 加fetch选项搞定静态和 ISR;cache: 'no-store'或 awaitcookies()/headers()强制按请求动态渲染。 - 隐式
fetch缓存真的没了吗?: 在开了 Cache Components 的 Next.js 16 里,是的——请求期渲染成了默认,缓存靠'use cache'或fetch选项 opt-in。这其实才是大多数人一直以为的行为。 - Server Action 能上生产吗?: 能,Next.js 14 起就稳定了。表单和 mutation 都没问题。但记住它们是公开 endpoint——像对待任何 API 路由一样校验输入、检查鉴权。
- 必须开 React Compiler 吗?: 不必。它在 16 里已稳定,但要靠
reactCompiler: trueopt-in。它会自动 memo 组件,不过因为走 Babel 会拖慢构建——等应用稳定后再开。
相关阅读
外部参考:官方 Next.js 16 升级指南 与 Cache Components 文档。