mcpField
N
nextjs

nextjs

github.com/timelessco开发101 次安装
代码仓库

关于 nextjs

面向 Next.js 开发者的参考技能,涵盖应用构建与维护,重点介绍 App Router、渲染、数据获取和优化。

主要功能

  1. 01App Router 路由与布局
  2. 02Server 和 Client Components
  3. 03数据获取与缓存
  4. 04路由处理器与导航
  5. 05元数据与 SEO 配置
  6. 06图像和字体优化

适用场景

  1. 01启动 Next.js 应用
  2. 02迁移 Pages Router 项目
  3. 03实现服务器渲染页面
  4. 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
  • 仍支持现有项目
关键架构原则
  1. 默认使用 Server Components:除非标记 'use client',否则 app/ 中的组件都是 Server Components
  2. 基于文件的路由:文件系统定义应用路由
  3. 嵌套布局:使用布局在多个路由间共享 UI
  4. 渐进增强:尽可能在没有 JavaScript 的情况下运行
  5. 自动优化:自动优化图片、字体和脚本

安装与设置

创建新项目
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 - 路由的页面 UI
  • layout.tsx - 片段及其子项共享的 UI
  • loading.tsx - 加载 UI(使用 Suspense 包裹页面)
  • error.tsx - 错误 UI(使用 Error Boundary 包裹页面)
  • not-found.tsx - 404 UI
  • route.ts - API 端点(路由处理程序)
  • template.tsx - 重新渲染的布局 UI
  • default.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;

最佳实践

  1. 使用服务端组件:默认使用服务端组件,仅在需要时使用客户端组件
  2. 优化图片:始终使用 next/image 进行自动优化
  3. 元数据:设置适当的元数据以优化 SEO
  4. 加载状态:使用 Suspense 提供加载 UI
  5. 错误处理:实现错误边界
  6. 路由处理器:用于 API 端点,而非单独的后端
  7. 缓存:利用内置缓存策略
  8. 布局:使用嵌套布局共享 UI
  9. TypeScript:启用 TypeScript,确保类型安全
  10. 性能:对首屏图片使用 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"]

故障排除

常见问题
  1. 水合错误

    • 确保服务端和客户端渲染相同的内容
    • 检查服务端组件中是否包含仅在浏览器中运行的代码
    • 确认没有基于浏览器 API 的条件渲染
  2. 图片无法加载

    • 将远程域名添加到 next.config.js
    • 检查图片路径(public 目录中的路径应以 / 开头)
    • 确认已提供宽度和高度
  3. API 路由返回 404

    • 检查文件名是否为 route.ts/js,而非 index.ts
    • 确认导出的是命名导出 GET/POST,而非默认导出
    • 确保文件位于 app/api/ 目录中
  4. "use client" 错误

    • 在使用 Hooks 的组件中添加 'use client'
    • 在服务端组件中导入客户端组件,而非反过来
    • 检查事件处理器所在组件是否添加了 'use client'
  5. 元数据未更新

    • 清除浏览器缓存
    • 检查元数据导出的名称是否正确
    • 确认异步 generateMetadata 返回 Promise

资源

实现检查清单

使用 Next.js 构建时:

  • 使用 create-next-app 创建项目
  • 配置 TypeScript 和 ESLint
  • 使用元数据设置根布局
  • 实现路由结构
  • 添加加载和错误状态
  • 配置图片优化
  • 设置字体优化
  • 实现数据获取模式
  • 根据需要添加 API 路由
  • 配置环境变量
  • 根据需要设置中间件
  • 针对生产构建进行优化
  • 在生产模式下测试
  • 配置部署平台
  • 设置监控和分析

评论

支持 Markdown