前端开发精选文章6.71 分钟阅读

Next.js App Router 完全指南

全面介绍 Next.js 14 的 App Router,包括路由系统、布局、加载状态、错误处理等核心概念和最佳实践。

作者:li-lingfeng发布于 2024年2月1日

Next.js App Router 完全指南

Next.js 13 引入了全新的 App Router,这是基于 React Server Components 构建的下一代路由系统。本文将全面介绍 App Router 的核心概念和最佳实践。

App Router vs Pages Router

Pages Router (传统方式)

pages/
├── index.js          // /
├── about.js          // /about
└── blog/
    ├── index.js      // /blog
    └── [slug].js     // /blog/[slug]

App Router (新方式)

app/
├── page.tsx          // /
├── about/
│   └── page.tsx      // /about
└── blog/
    ├── page.tsx      // /blog
    └── [slug]/
        └── page.tsx  // /blog/[slug]

核心概念

1. 文件约定

App Router 使用特殊的文件名来定义路由行为:

  • page.tsx: 定义路由页面
  • layout.tsx: 定义布局
  • loading.tsx: 定义加载状态
  • error.tsx: 定义错误页面
  • not-found.tsx: 定义 404 页面
  • route.tsx: 定义 API 路由

2. 布局系统

根布局 (必需)

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="zh">
      <body>
        <header>
          <nav>全局导航</nav>
        </header>
        <main>{children}</main>
        <footer>全局页脚</footer>
      </body>
    </html>
  );
}

嵌套布局

// app/blog/layout.tsx
export default function BlogLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="blog-container">
      <aside>
        <h2>博客侧边栏</h2>
        <nav>博客导航</nav>
      </aside>
      <div className="blog-content">
        {children}
      </div>
    </div>
  );
}

3. 页面组件

// app/blog/page.tsx
export default function BlogPage() {
  return (
    <div>
      <h1>博客首页</h1>
      <p>欢迎来到我的博客</p>
    </div>
  );
}

4. 动态路由

单个动态段

// app/blog/[slug]/page.tsx
export default function BlogPost({
  params,
}: {
  params: { slug: string };
}) {
  return (
    <div>
      <h1>文章: {params.slug}</h1>
    </div>
  );
}

多个动态段

// app/blog/[category]/[slug]/page.tsx
export default function CategoryPost({
  params,
}: {
  params: { category: string; slug: string };
}) {
  return (
    <div>
      <h1>分类: {params.category}</h1>
      <h2>文章: {params.slug}</h2>
    </div>
  );
}

捕获所有路由

// app/docs/[...slug]/page.tsx
export default function DocsPage({
  params,
}: {
  params: { slug: string[] };
}) {
  return (
    <div>
      <h1>文档路径: {params.slug.join('/')}</h1>
    </div>
  );
}

服务器组件 vs 客户端组件

服务器组件 (默认)

// 这是一个服务器组件
async function BlogPost({ params }: { params: { slug: string } }) {
  // 可以直接在服务器端获取数据
  const post = await getPost(params.slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

客户端组件

'use client'; // 标记为客户端组件

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>计数: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        增加
      </button>
    </div>
  );
}

数据获取

服务器端数据获取

// app/posts/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    // 可以配置缓存策略
    next: { revalidate: 3600 } // 1小时后重新验证
  });

  if (!res.ok) {
    throw new Error('Failed to fetch posts');
  }

  return res.json();
}

export default async function PostsPage() {
  const posts = await getPosts();

  return (
    <div>
      <h1>文章列表</h1>
      {posts.map((post: any) => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </div>
      ))}
    </div>
  );
}

并行数据获取

async function getUser(id: string) {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

async function getUserPosts(id: string) {
  const res = await fetch(`/api/users/${id}/posts`);
  return res.json();
}

export default async function UserProfile({
  params,
}: {
  params: { id: string };
}) {
  // 并行获取数据
  const [user, posts] = await Promise.all([
    getUser(params.id),
    getUserPosts(params.id),
  ]);

  return (
    <div>
      <h1>{user.name}</h1>
      <div>
        <h2>用户文章</h2>
        {posts.map((post: any) => (
          <div key={post.id}>{post.title}</div>
        ))}
      </div>
    </div>
  );
}

加载状态

// app/blog/loading.tsx
export default function Loading() {
  return (
    <div className="loading-container">
      <div className="spinner"></div>
      <p>加载中...</p>
    </div>
  );
}

错误处理

// app/blog/error.tsx
'use client';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div className="error-container">
      <h2>出错了!</h2>
      <p>{error.message}</p>
      <button onClick={reset}>重试</button>
    </div>
  );
}

路由组

使用括号创建路由组,不影响 URL 结构:

app/
├── (marketing)/
│   ├── about/
│   │   └── page.tsx     // /about
│   └── contact/
│       └── page.tsx     // /contact
└── (shop)/
    ├── products/
    │   └── page.tsx     // /products
    └── cart/
        └── page.tsx     // /cart

每个路由组可以有自己的布局:

// app/(marketing)/layout.tsx
export default function MarketingLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="marketing-layout">
      <nav>营销页面导航</nav>
      {children}
    </div>
  );
}

拦截路由

使用 (..) 语法拦截路由:

app/
├── feed/
│   └── page.tsx
├── photo/
│   └── [id]/
│       └── page.tsx
└── @modal/
    └── (..)photo/
        └── [id]/
            └── page.tsx

并行路由

使用 @ 语法创建并行路由:

// app/layout.tsx
export default function Layout({
  children,
  analytics,
  team,
}: {
  children: React.ReactNode;
  analytics: React.ReactNode;
  team: React.ReactNode;
}) {
  return (
    <>
      {children}
      {analytics}
      {team}
    </>
  );
}

最佳实践

1. 合理使用服务器组件和客户端组件

// 服务器组件负责数据获取
async function PostList() {
  const posts = await getPosts();

  return (
    <div>
      {posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  );
}

// 客户端组件负责交互
'use client';
function PostCard({ post }) {
  const [liked, setLiked] = useState(false);

  return (
    <div>
      <h3>{post.title}</h3>
      <button onClick={() => setLiked(!liked)}>
        {liked ? '❤️' : '🤍'}
      </button>
    </div>
  );
}

2. 优化数据获取

// 使用适当的缓存策略
async function getData() {
  const res = await fetch('https://api.example.com/data', {
    next: {
      revalidate: 3600, // 静态重新生成
      tags: ['posts'] // 标签重新验证
    }
  });

  return res.json();
}

3. 错误边界和加载状态

为每个路由段提供适当的错误处理和加载状态:

app/
├── dashboard/
│   ├── loading.tsx      // 仪表板加载状态
│   ├── error.tsx        // 仪表板错误处理
│   ├── page.tsx
│   └── analytics/
│       ├── loading.tsx  // 分析页面加载状态
│       ├── error.tsx    // 分析页面错误处理
│       └── page.tsx

总结

Next.js App Router 带来了许多强大的特性:

  • 基于文件系统的路由: 直观的路由结构
  • 布局系统: 灵活的嵌套布局
  • 服务器组件: 更好的性能和 SEO
  • 流式渲染: 改善用户体验
  • 并行路由: 复杂 UI 的解决方案

App Router 代表了 React 和 Next.js 的未来方向,值得我们深入学习和实践。

相关文章

前端开发5.1 分钟

React 18 并发特性深度解析

深入探讨 React 18 的并发渲染机制,包括 useTransition、useDeferredValue 等新 Hook 的使用场景和最佳实践。

前端开发8.775 分钟

TypeScript 高级类型实战指南

深入探索 TypeScript 的高级类型系统,包括条件类型、映射类型、模板字面量类型等,通过实际案例学习如何构建类型安全的应用。

觉得这篇文章有用?

分享给更多朋友,让知识传播得更远 ✨

评论讨论

参与讨论

登录后即可发表评论,与其他读者交流想法

加载评论中...