App Router 基本概念

不需要 20 小时的 App Router 教程,只要 8 个概念就能解释你代码为什么这样跑。本文给你这 8 个。

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——文件约定是真的概念。

实操步骤

  1. 默认 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>
  );
}
  1. Client Component。 'use client' 让文件(和它的 import)进客户端 bundle。useStateuseEffect、事件处理函数都要:
'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>;
}
  1. 文件系统 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
  1. Layout。 包子树,且子树内导航不会重新渲染 layout——这才是设计意图:
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh">
      <body>
        <nav>...</nav>           {/* 切换 route 时保留 */}
        {children}
      </body>
    </html>
  );
}
  1. 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>;
}
  1. 扩展过的 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'] } });
  1. Metadata API。 typed metadata export 设 <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}/` },
  };
}
  1. 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 变动态)混进想保持静态的页——检查 dynamic export。
  • 把 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 用 async server component 加 fetch 选项替代。
  • getServerSideProps 怎么对应?: 对应 fetchcache: 'no-store',或者调 cookies() / headers() 强制动态。
  • Server Action 能上生产吗?: 能,Next.js 14 起稳定。表单和 mutation 没问题,但要审一下安全模型——它们是公开 endpoint。

相关阅读

标签: #独立开发 #Next.js #入门 #工作流