App Router 新词一大堆——server component、client component、layout、route handler、parallel route、intercepting route、server action。独立内容站 8 个概念覆盖你 95% 会碰的东西。先学这些,剩下的等用到再说。
问题背景
App Router 在 Next.js 13 落地,14、15 成熟,2026 是官方推荐默认。Pages Router 还能用,但新功能(PPR、server action、缓存改进)都先在 App Router 上。心智模型和 Pages Router 差别足够大,“我会 React”不够用。
判断标准
- 代码报错 “You’re importing a component that needs
useState. It only works in a Client Component”——你需要懂 server / client 边界。 - 页面切换时 layout 不重新渲染——这正是设计意图,去学共享 layout。
- server component 里的
fetch()不打招呼就被缓存了——缓存默认值很重要。 loading.tsx文件莫名其妙控制了 suspense——文件约定是真的概念。
实操步骤
- 默认 Server Component。
app/下每个文件默认 server component,顶部加'use client'才变客户端。能直接await,浏览器零 JS:
// 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>
);
}
- 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 里能随便 import client;反过来要通过 children 传:
// app/page.tsx(server)—— 把服务端渲染的子节点传给客户端壳
import { ClientShell } from './ClientShell';
import { ServerData } from './ServerData';
export default function Page() {
return <ClientShell><ServerData /></ClientShell>;
}
- 文件系统 route。 文件夹结构就是 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
- Layout。 包子树,且子树内导航不会重新渲染 layout——这才是设计意图:
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh">
<body>
<nav>...</nav> {/* 切换 route 时保留 */}
{children}
</body>
</html>
);
}
- Static / Dynamic / ISR。 默认静态。碰
cookies()、headers()、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 }));
}
export default async function ArticlePage({ params }: { params: { slug: string } }) {
const article = await getArticle(params.slug);
return <article><h1>{article.title}</h1></article>;
}
- 扩展过的
fetch。 缓存是 opt-in;Next 15 后默认值改了:
// 不缓存,每次都拉新
const data = await fetch(url, { cache: 'no-store' });
// 每 60 秒重新校验
const data = await fetch(url, { next: { revalidate: 60 } });
// tag 失效——以后调 revalidateTag('posts') 一刀清
const data = await fetch(url, { next: { tags: ['posts'] } });
- Metadata API。 typed
metadataexport 设<title>、OG、canonical;动态用generateMetadata:
// app/articles/[slug]/page.tsx
import type { Metadata } from 'next';
export async function generateMetadata(
{ params }: { params: { slug: string } }
): Promise<Metadata> {
const article = await getArticle(params.slug);
return {
title: article.title,
description: article.description,
alternates: { canonical: `/articles/${article.slug}/` },
openGraph: { title: article.title, url: `/articles/${article.slug}/` },
};
}
- Server Action。 标
'use server'的函数,能从 client component 调、跑在服务端,省掉大量/api/*样板:
// app/actions.ts
'use server';
import { revalidateTag } 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 } });
revalidateTag('posts');
}
// app/new/page.tsx —— 在 form 里直接用
import { createPost } from '../actions';
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" />
<button type="submit">保存</button>
</form>
);
}
容易踩的坑
- “保险起见”每个文件顶部都加
'use client'——等于把 App Router 退化成 SPA,框架优势全丢。 - client component 里 import 仅限服务端的库——build 报错,或者更糟,泄漏密钥。
- 忘了
layout.tsx在其作用域内导航不重新渲染——把按页状态放里面会很意外。 - 把
searchParams(让 route 变动态)混进想保持静态的页——检查dynamicexport。 - 把 server action 当 RPC 跑重活——底层是 HTTP,同样的冷启动代价。
这篇适合谁
2026 年新开 Next.js 项目,或者从 Pages Router 迁过来想搞清变了什么。
这篇不适合谁
已有 Pages Router 项目、没迁移动力——App Router 能共存,但完整转换是真活。
FAQ
- 要不要单独学 React Server Components?: RSC 就是 App Router 的地基——学 App Router 就是学 RSC,不用单独找教程。
- 还能用
getStaticProps吗?: 只在 Pages Router 能。App Router 用asyncserver component 加fetch选项替代。 getServerSideProps怎么对应?: 对应fetch的cache: 'no-store',或者调cookies()/headers()强制动态。- Server Action 能上生产吗?: 能,Next.js 14 起稳定。表单和 mutation 没问题,但要审一下安全模型——它们是公开 endpoint。