关于 nextjs
面向 Next.js 开发者的参考技能,涵盖应用构建与维护,重点介绍 App Router、渲染、数据获取和优化。
主要功能
- 01App Router 路由与布局
- 02Server 和 Client Components
- 03数据获取与缓存
- 04路由处理器与导航
- 05元数据与 SEO 配置
- 06图像和字体优化
适用场景
- 01启动 Next.js 应用
- 02迁移 Pages Router 项目
- 03实现服务器渲染页面
- 04配置 SEO 与元数据
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ timelessco/ recollect --skill nextjs安装后在 Claude Code 中输入/nextjs确认已加载。
详细介绍
Next.js 技能
Next.js 是一个 React 框架,用于构建全栈 Web 应用,内置服务端渲染、静态生成和强大的优化功能。
参考
https://nextjs.org/docs/llms.txt
何时使用此技能
在以下情况下使用此技能:
- 构建新的 Next.js 应用(v15+)
- 实现 App Router 架构
- 使用 Server Components 和 Client Components
- 设置路由、布局和导航
- 实现数据获取模式
- 优化图片、字体和性能
- 配置元数据和 SEO
- 设置 API 路由和路由处理程序
- 从 Pages Router 迁移到 App Router
- 部署 Next.js 应用
核心概念
App Router 与 Pages Router
App Router(推荐用于 v13+):
- 采用 React Server Components 的现代架构
- 基于文件系统的路由,位于
app/目录中 - 布局、加载状态和错误边界
- 支持流式传输和 Suspense
- 使用布局实现嵌套路由
Pages Router(旧版):
- 位于
pages/目录中的传统页面路由 - 使用
getStaticProps、getServerSideProps、getInitialProps - 仍支持现有项目
关键架构原则
- 默认使用 Server Components:除非标记
'use client',否则app/中的组件都是 Server Components - 基于文件的路由:文件系统定义应用路由
- 嵌套布局:使用布局在多个路由间共享 UI
- 渐进增强:尽可能在没有 JavaScript 的情况下运行
- 自动优化:自动优化图片、字体和脚本
安装与设置
创建新项目
npx create-next-app@latest my-app
# or
yarn create next-app my-app
# or
pnpm create next-app my-app
# or
bun create next-app my-app
交互式设置提示:
- TypeScript?(推荐选是)
- ESLint?(推荐选是)
- Tailwind CSS?(可选)
src/目录?(可选)- App Router?(新项目推荐选是)
- 导入别名?(默认值:@/*)
手动设置
npm install next@latest react@latest react-dom@latest
package.json 脚本:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
}
}
项目结构
my-app/
├── app/ # App Router (v13+)
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home page
│ ├── loading.tsx # Loading UI
│ ├── error.tsx # Error UI
│ ├── not-found.tsx # 404 page
│ ├── global.css # Global styles
│ └── [folder]/ # Route segments
├── public/ # Static assets
├── components/ # React components
├── lib/ # Utility functions
├── next.config.js # Next.js configuration
├── package.json
└── tsconfig.json
路由
文件约定
page.tsx- 路由的页面 UIlayout.tsx- 片段及其子项共享的 UIloading.tsx- 加载 UI(使用 Suspense 包裹页面)error.tsx- 错误 UI(使用 Error Boundary 包裹页面)not-found.tsx- 404 UIroute.ts- API 端点(路由处理程序)template.tsx- 重新渲染的布局 UIdefault.tsx- 并行路由回退页面
基本路由
静态路由:
app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
└── blog/
└── page.tsx → /blog
动态路由:
// app/blog/[slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
return <h1>Post: {params.slug}</h1>;
}
捕获所有路由:
// app/shop/[...slug]/page.tsx
export default function Shop({ params }: { params: { slug: string[] } }) {
return <h1>Category: {params.slug.join("/")}</h1>;
}
可选捕获所有路由:
// app/docs/[[...slug]]/page.tsx
// Matches /docs, /docs/a, /docs/a/b, etc.
路由组
在不影响 URL 的情况下组织路由:
app/
├── (marketing)/ # Group without URL segment
│ ├── about/page.tsx → /about
│ └── blog/page.tsx → /blog
└── (shop)/
├── products/page.tsx → /products
└── cart/page.tsx → /cart
并行路由
在同一布局中渲染多个页面:
app/
├── @team/ # Slot
│ └── page.tsx
├── @analytics/ # Slot
│ └── page.tsx
└── layout.tsx # Uses both slots
// app/layout.tsx
export default function Layout({
children,
team,
analytics,
}: {
children: React.ReactNode;
team: React.ReactNode;
analytics: React.ReactNode;
}) {
return (
<>
{children}
{team}
{analytics}
</>
);
}
拦截路由
拦截路由并在模态框中显示:
app/
├── feed/
│ └── page.tsx
├── photo/
│ └── [id]/
│ └── page.tsx
└── (..)photo/ # Intercepts /photo/[id]
└── [id]/
└── page.tsx
布局
根布局(必需)
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
嵌套布局
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<section>
<nav>Dashboard Nav</nav>
{children}
</section>
);
}
布局具有以下特点:
- 在多个页面间共享
- 在导航时保留状态
- 在导航时不会重新渲染
- 可以获取数据
服务端和客户端组件
Server Components(默认)
默认情况下,app/ 中的组件都是 Server Components:
// app/page.tsx (Server Component)
async function getData() {
const res = await fetch("https://api.example.com/data");
return res.json();
}
export default async function Page() {
const data = await getData();
return <div>{data.title}</div>;
}
优点:
- 在服务端获取数据
- 直接访问后端资源
- 将敏感数据保留在服务端
- 减少客户端 JavaScript
- 加快初始页面加载
限制:
- 不能使用钩子(useState、useEffect)
- 不能使用浏览器 API
- 不能添加事件监听器
Client Components
使用 'use client' 指令标记组件:
// components/counter.tsx
"use client";
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
以下情况使用 Client Components:
- 交互式 UI(onClick、onChange)
- 状态管理(useState、useReducer)
- 副作用(useEffect、useLayoutEffect)
- 浏览器 API(localStorage、navigator)
- 自定义钩子
- React 类组件
组合模式
// app/page.tsx (Server Component)
import { ClientComponent } from "./client-component";
export default function Page() {
return (
<div>
<h1>Server-rendered content</h1>
<ClientComponent />
</div>
);
}
数据获取
在 Server Component 中获取数据
// app/posts/page.tsx
async function getPosts() {
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 3600 }, // Revalidate every hour
});
if (!res.ok) {
throw new Error("Failed to fetch");
}
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
缓存策略
强制缓存(默认):
fetch("https://api.example.com/data", { cache: "force-cache" });
不缓存(动态):
fetch("https://api.example.com/data", { cache: "no-store" });
重新验证:
fetch("https://api.example.com/data", {
next: { revalidate: 3600 }, // Seconds
});
基于标签的重新验证:
fetch("https://api.example.com/data", {
next: { tags: ["posts"] },
});
// Revalidate elsewhere:
import { revalidateTag } from "next/cache";
revalidateTag("posts");
并行获取数据
async function getData() {
const [posts, users] = await Promise.all([
fetch("https://api.example.com/posts").then((r) => r.json()),
fetch("https://api.example.com/users").then((r) => r.json()),
]);
return { posts, users };
}
顺序数据获取
async function getData() {
const post = await fetch(`https://api.example.com/posts/${id}`).then((r) =>
r.json(),
);
const author = await fetch(
`https://api.example.com/users/${post.authorId}`,
).then((r) => r.json());
return { post, author };
}
路由处理器(API 路由)
基本路由处理器
// app/api/hello/route.ts
export async function GET(request: Request) {
return Response.json({ message: "Hello" });
}
export async function POST(request: Request) {
const body = await request.json();
return Response.json({ received: body });
}
动态路由处理器
// app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } },
) {
const post = await getPost(params.id);
return Response.json(post);
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } },
) {
await deletePost(params.id);
return new Response(null, { status: 204 });
}
请求辅助工具
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
const cookies = request.headers.get("cookie");
return Response.json({ id });
}
响应类型
// JSON
return Response.json({ data: "value" });
// Text
return new Response("Hello", { headers: { "Content-Type": "text/plain" } });
// Redirect
return Response.redirect("https://example.com");
// Status codes
return new Response("Not Found", { status: 404 });
导航
Link 组件
import Link from "next/link";
export default function Page() {
return (
<>
<Link href="/about">About</Link>
<Link href="/blog/post-1">Post 1</Link>
<Link href={{ pathname: "/blog/[slug]", query: { slug: "post-1" } }}>
Post 1 (alternative)
</Link>
</>
);
}
useRouter Hook(客户端)
"use client";
import { useRouter } from "next/navigation";
export function NavigateButton() {
const router = useRouter();
return <button onClick={() => router.push("/dashboard")}>Dashboard</button>;
}
路由器方法:
router.push(href)- 导航到路由router.replace(href)- 替换当前历史记录router.refresh()- 刷新当前路由router.back()- 后退router.forward()- 前进router.prefetch(href)- 预取路由
程序化导航(服务端)
import { redirect } from "next/navigation";
export default async function Page() {
const session = await getSession();
if (!session) {
redirect("/login");
}
return <div>Protected content</div>;
}
元数据与 SEO
静态元数据
// app/page.tsx
import { Metadata } from "next";
export const metadata: Metadata = {
title: "My Page",
description: "Page description",
keywords: ["nextjs", "react"],
openGraph: {
title: "My Page",
description: "Page description",
images: ["/og-image.jpg"],
},
twitter: {
card: "summary_large_image",
title: "My Page",
description: "Page description",
images: ["/twitter-image.jpg"],
},
};
export default function Page() {
return <div>Content</div>;
}
动态元数据
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
};
}
元数据文件
favicon.ico、icon.png、apple-icon.png- 网站图标opengraph-image.png、twitter-image.png- 社交媒体图片robots.txt- Robots 文件sitemap.xml- 站点地图
图片优化
Image 组件
import Image from "next/image";
export default function Page() {
return (
<>
{/* Local image */}
<Image src="/profile.png" alt="Profile" width={500} height={500} />
{/* Remote image */}
<Image
src="https://example.com/image.jpg"
alt="Remote"
width={500}
height={500}
/>
{/* Responsive fill */}
<div style={{ position: "relative", width: "100%", height: "400px" }}>
<Image src="/hero.jpg" alt="Hero" fill style={{ objectFit: "cover" }} />
</div>
{/* Priority loading */}
<Image src="/hero.jpg" alt="Hero" width={1200} height={600} priority />
</>
);
}
图片属性:
src- 图片路径(本地或 URL)alt- 替代文本(必填)width、height- 尺寸(必填,除非使用 fill)fill- 填充父容器sizes- 响应式尺寸quality- 1-100(默认值为 75)priority- 预加载图片placeholder- 'blur-sm' | 'empty'blurDataURL- 用于模糊效果的 Data URL
远程图片配置
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "example.com",
pathname: "/images/**",
},
],
},
};
字体优化
Google Fonts
// app/layout.tsx
import { Inter, Roboto_Mono } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
display: "swap",
});
const robotoMono = Roboto_Mono({
subsets: ["latin"],
display: "swap",
variable: "--font-roboto-mono",
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={`${inter.className} ${robotoMono.variable}`}>
<body>{children}</body>
</html>
);
}
本地字体
import localFont from "next/font/local";
const myFont = localFont({
src: "./fonts/my-font.woff2",
display: "swap",
variable: "--font-my-font",
});
加载状态
Loading 文件
// app/dashboard/loading.tsx
export default function Loading() {
return <div>Loading dashboard...</div>;
}
使用 Suspense 实现流式传输
// app/page.tsx
import { Suspense } from "react";
async function Posts() {
const posts = await getPosts();
return (
<ul>
{posts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
);
}
export default function Page() {
return (
<div>
<h1>My Posts</h1>
<Suspense fallback={<div>Loading posts...</div>}>
<Posts />
</Suspense>
</div>
);
}
错误处理
Error 文件
// app/error.tsx
"use client";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
全局错误
// app/global-error.tsx
"use client";
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
);
}
未找到
// app/not-found.tsx
export default function NotFound() {
return (
<div>
<h2>404 - Not Found</h2>
<p>Could not find requested resource</p>
</div>
);
}
// Trigger programmatically
import { notFound } from "next/navigation";
export default async function Page({ params }) {
const post = await getPost(params.id);
if (!post) {
notFound();
}
return <div>{post.title}</div>;
}
中间件
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
// Authentication check
const token = request.cookies.get("token");
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
// Add custom header
const response = NextResponse.next();
response.headers.set("x-custom-header", "value");
return response;
}
export const config = {
matcher: ["/dashboard/:path*", "/api/:path*"],
};
环境变量
# .env.local
DATABASE_URL=postgresql://...
NEXT_PUBLIC_API_URL=https://api.example.com
// Server-side only
const dbUrl = process.env.DATABASE_URL;
// Client and server (NEXT_PUBLIC_ prefix)
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
配置
next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// React strict mode
reactStrictMode: true,
// Image domains
images: {
remotePatterns: [{ protocol: "https", hostname: "example.com" }],
},
// Redirects
async redirects() {
return [
{
source: "/old-page",
destination: "/new-page",
permanent: true,
},
];
},
// Rewrites
async rewrites() {
return [
{
source: "/api/:path*",
destination: "https://api.example.com/:path*",
},
];
},
// Headers
async headers() {
return [
{
source: "/(.*)",
headers: [{ key: "X-Frame-Options", value: "DENY" }],
},
];
},
// Environment variables
env: {
CUSTOM_KEY: "value",
},
};
module.exports = nextConfig;
最佳实践
- 使用服务端组件:默认使用服务端组件,仅在需要时使用客户端组件
- 优化图片:始终使用
next/image进行自动优化 - 元数据:设置适当的元数据以优化 SEO
- 加载状态:使用 Suspense 提供加载 UI
- 错误处理:实现错误边界
- 路由处理器:用于 API 端点,而非单独的后端
- 缓存:利用内置缓存策略
- 布局:使用嵌套布局共享 UI
- TypeScript:启用 TypeScript,确保类型安全
- 性能:对首屏图片使用
priority,对首屏以下图片启用延迟加载
常见模式
受保护的路由
// app/dashboard/layout.tsx
import { redirect } from "next/navigation";
import { getSession } from "@/lib/auth";
export default async function DashboardLayout({ children }) {
const session = await getSession();
if (!session) {
redirect("/login");
}
return <>{children}</>;
}
数据变更(服务端操作)
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
const title = formData.get("title");
await db.post.create({ data: { title } });
revalidatePath("/posts");
}
// app/posts/new/page.tsx
import { createPost } from "@/app/actions";
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" type="text" required />
<button type="submit">Create</button>
</form>
);
}
静态生成
// Generate static params for dynamic routes
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
export default async function Post({ params }) {
const post = await getPost(params.slug);
return <article>{post.content}</article>;
}
部署
Vercel(推荐)
# Install Vercel CLI
npm i -g vercel
# Deploy
vercel
自托管
# Build
npm run build
# Start production server
npm start
要求:
- Node.js 18.17 或更高版本
- 在 next.config.js 中设置
output: 'standalone'(可选,可减小体积)
Docker
FROM node:18-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]
故障排除
常见问题
-
水合错误
- 确保服务端和客户端渲染相同的内容
- 检查服务端组件中是否包含仅在浏览器中运行的代码
- 确认没有基于浏览器 API 的条件渲染
-
图片无法加载
- 将远程域名添加到
next.config.js - 检查图片路径(public 目录中的路径应以
/开头) - 确认已提供宽度和高度
- 将远程域名添加到
-
API 路由返回 404
- 检查文件名是否为
route.ts/js,而非index.ts - 确认导出的是命名导出 GET/POST,而非默认导出
- 确保文件位于
app/api/目录中
- 检查文件名是否为
-
"use client" 错误
- 在使用 Hooks 的组件中添加
'use client' - 在服务端组件中导入客户端组件,而非反过来
- 检查事件处理器所在组件是否添加了
'use client'
- 在使用 Hooks 的组件中添加
-
元数据未更新
- 清除浏览器缓存
- 检查元数据导出的名称是否正确
- 确认异步 generateMetadata 返回 Promise
资源
- 文档:https://nextjs.org/docs
- 学习课程:https://nextjs.org/learn
- 示例:https://github.com/vercel/next.js/tree/canary/examples
- 博客:https://nextjs.org/blog
- GitHub:https://github.com/vercel/next.js
实现检查清单
使用 Next.js 构建时:
- 使用
create-next-app创建项目 - 配置 TypeScript 和 ESLint
- 使用元数据设置根布局
- 实现路由结构
- 添加加载和错误状态
- 配置图片优化
- 设置字体优化
- 实现数据获取模式
- 根据需要添加 API 路由
- 配置环境变量
- 根据需要设置中间件
- 针对生产构建进行优化
- 在生产模式下测试
- 配置部署平台
- 设置监控和分析
评论
支持 Markdown