Next.js 最佳实践指南
Next.js, 技术, 前端

Next.js 最佳实践指南

分享在 Next.js 14 项目中的一些最佳实践和优化技巧。

0次点击5分钟阅读

引言

Next.js 14 带来了许多新特性和改进,本文将分享一些在实际项目中的最佳实践。

App Router vs Pages Router

Next.js 13+ 引入了新的 App Router,带来了许多优势:

  • 更好的布局系统
  • Server Components 支持
  • 改进的数据获取方式
  • 更灵活的路由配置

性能优化

1. 图片优化

使用 Next.js 的 Image 组件:

1import Image from 'next/image'
2
3export function MyImage() {
4  return (
5    <Image
6      src="/my-image.jpg"
7      alt="描述"
8      width={800}
9      height={600}
10      priority
11    />
12  )
13}

2. 字体优化

使用 next/font 优化字体加载:

1import { Inter } from 'next/font/inter'
2
3const inter = Inter({ subsets: ['latin'] })

3. 代码分割

利用动态导入实现代码分割:

1const DynamicComponent = dynamic(() => import('./Component'), {
2  loading: () => <p>加载中...</p>,
3})

数据获取策略

Server Components (推荐)

1export default async function Page() {
2  const data = await fetch('https://api.example.com/data', {
3    // 缓存策略
4    next: { revalidate: 3600 } // 1小时后重新验证
5  })
6  const json = await data.json()
7  return <div>{json.title}</div>
8}

静态生成 (SSG)

1export async function generateStaticParams() {
2  const posts = await fetch('https://api.example.com/posts').then(res => res.json())
3  return posts.map((post) => ({
4    slug: post.slug,
5  }))
6}
7
8export default async function Post({ params }) {
9  const post = await fetch(`https://api.example.com/posts/${params.slug}`)
10  return <article>{post.content}</article>
11}

Streaming 和 Suspense

1import { Suspense } from 'react'
2
3async function DataComponent() {
4  const data = await fetchSlowData()
5  return <div>{data}</div>
6}
7
8export default function Page() {
9  return (
10    <div>
11      <h1>快速加载的内容</h1>
12      <Suspense fallback={<div>加载中...</div>}>
13        <DataComponent />
14      </Suspense>
15    </div>
16  )
17}

路由和导航最佳实践

路由组织

app/
├── (marketing)/        # 路由组(不影响 URL)
│   ├── about/
│   └── contact/
├── (app)/
│   ├── dashboard/
│   └── settings/
└── api/               # API 路由
    └── users/
        └── route.ts

动态路由

1// app/blog/[slug]/page.tsx
2export default function BlogPost({ params }) {
3  return <h1>文章: {params.slug}</h1>
4}
5
6// 可选捕获所有路由
7// app/docs/[...slug]/page.tsx
8export default function Docs({ params }) {
9  // params.slug 是数组:['intro', 'getting-started']
10  return <div>文档路径: {params.slug.join('/')}</div>
11}

并行路由和拦截路由

1// app/@modal/(.)photo/[id]/page.tsx
2export default function PhotoModal({ params }) {
3  return (
4    <div className="modal">
5      <img src={`/photos/${params.id}`} alt="Photo" />
6    </div>
7  )
8}
9
10// app/layout.tsx
11export default function Layout({ children, modal }) {
12  return (
13    <>
14      {children}
15      {modal}
16    </>
17  )
18}

缓存策略深入

数据缓存

1// 永久缓存
2fetch('https://api.example.com/data', {
3  cache: 'force-cache'
4})
5
6// 不缓存
7fetch('https://api.example.com/data', {
8  cache: 'no-store'
9})
10
11// 定时重新验证
12fetch('https://api.example.com/data', {
13  next: { revalidate: 60 } // 60秒
14})
15
16// 按需重新验证
17import { revalidatePath, revalidateTag } from 'next/cache'
18
19// 重新验证特定路径
20revalidatePath('/blog/[slug]')
21
22// 使用标签重新验证
23fetch('https://api.example.com/posts', {
24  next: { tags: ['posts'] }
25})
26revalidateTag('posts')

路由缓存

1// app/blog/page.tsx
2export const dynamic = 'force-dynamic' // 禁用路由缓存
3export const revalidate = 0 // 禁用数据缓存

中间件使用

认证中间件

1// middleware.ts
2import { NextResponse } from 'next/server'
3import type { NextRequest } from 'next/server'
4
5export function middleware(request: NextRequest) {
6  const token = request.cookies.get('token')
7
8  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
9    return NextResponse.redirect(new URL('/login', request.url))
10  }
11
12  // 添加自定义请求头
13  const response = NextResponse.next()
14  response.headers.set('x-custom-header', 'value')
15
16  return response
17}
18
19export const config = {
20  matcher: ['/dashboard/:path*', '/api/:path*']
21}

国际化中间件

1import { NextResponse } from 'next/server'
2import type { NextRequest } from 'next/server'
3
4const locales = ['en', 'zh', 'ja']
5const defaultLocale = 'en'
6
7export function middleware(request: NextRequest) {
8  const pathname = request.nextUrl.pathname
9
10  // 检查路径中是否已有语言
11  const pathnameHasLocale = locales.some(
12    (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
13  )
14
15  if (pathnameHasLocale) return
16
17  // 从 Accept-Language 获取首选语言
18  const locale = request.headers
19    .get('Accept-Language')
20    ?.split(',')[0]
21    .split('-')[0] || defaultLocale
22
23  return NextResponse.redirect(
24    new URL(`/${locale}${pathname}`, request.url)
25  )
26}

错误处理

错误边界

1// app/error.tsx
2'use client'
3
4export default function Error({
5  error,
6  reset,
7}: {
8  error: Error & { digest?: string }
9  reset: () => void
10}) {
11  return (
12    <div>
13      <h2>出错了!</h2>
14      <p>{error.message}</p>
15      <button onClick={() => reset()}>重试</button>
16    </div>
17  )
18}
19
20// app/global-error.tsx (根错误边界)
21'use client'
22
23export default function GlobalError({
24  error,
25  reset,
26}: {
27  error: Error & { digest?: string }
28  reset: () => void
29}) {
30  return (
31    <html>
32      <body>
33        <h2>全局错误</h2>
34        <button onClick={() => reset()}>重试</button>
35      </body>
36    </html>
37  )
38}

Not Found 处理

1// app/not-found.tsx
2export default function NotFound() {
3  return (
4    <div>
5      <h2>404 - 页面不存在</h2>
6      <p>抱歉,您访问的页面不存在。</p>
7    </div>
8  )
9}
10
11// 在页面中触发 404
12import { notFound } from 'next/navigation'
13
14export default async function Page({ params }) {
15  const data = await fetchData(params.id)
16
17  if (!data) {
18    notFound()
19  }
20
21  return <div>{data.title}</div>
22}

API 路由最佳实践

RESTful API

1// app/api/posts/route.ts
2import { NextResponse } from 'next/server'
3
4export async function GET(request: Request) {
5  const { searchParams } = new URL(request.url)
6  const page = searchParams.get('page') || '1'
7
8  const posts = await fetchPosts(parseInt(page))
9
10  return NextResponse.json(posts)
11}
12
13export async function POST(request: Request) {
14  const body = await request.json()
15
16  // 验证数据
17  if (!body.title || !body.content) {
18    return NextResponse.json(
19      { error: '标题和内容不能为空' },
20      { status: 400 }
21    )
22  }
23
24  const newPost = await createPost(body)
25
26  return NextResponse.json(newPost, { status: 201 })
27}
28
29// app/api/posts/[id]/route.ts
30export async function GET(
31  request: Request,
32  { params }: { params: { id: string } }
33) {
34  const post = await fetchPost(params.id)
35
36  if (!post) {
37    return NextResponse.json(
38      { error: '文章不存在' },
39      { status: 404 }
40    )
41  }
42
43  return NextResponse.json(post)
44}

认证 API

1import { NextResponse } from 'next/server'
2import { cookies } from 'next/headers'
3
4export async function POST(request: Request) {
5  const body = await request.json()
6  const { username, password } = body
7
8  // 验证用户
9  const user = await authenticateUser(username, password)
10
11  if (!user) {
12    return NextResponse.json(
13      { error: '用户名或密码错误' },
14      { status: 401 }
15    )
16  }
17
18  // 设置 Cookie
19  cookies().set('token', user.token, {
20    httpOnly: true,
21    secure: process.env.NODE_ENV === 'production',
22    sameSite: 'strict',
23    maxAge: 60 * 60 * 24 * 7 // 7天
24  })
25
26  return NextResponse.json({ success: true })
27}

元数据优化

静态元数据

1import { Metadata } from 'next'
2
3export const metadata: Metadata = {
4  title: 'My App',
5  description: 'App description',
6  openGraph: {
7    title: 'My App',
8    description: 'App description',
9    images: ['/og-image.jpg'],
10  },
11  twitter: {
12    card: 'summary_large_image',
13    title: 'My App',
14    description: 'App description',
15    images: ['/og-image.jpg'],
16  },
17}

动态元数据

1export async function generateMetadata({ params }): Promise<Metadata> {
2  const post = await fetchPost(params.id)
3
4  return {
5    title: post.title,
6    description: post.excerpt,
7    openGraph: {
8      images: [post.coverImage],
9    },
10  }
11}

部署和监控

环境变量

1// next.config.js
2module.exports = {
3  env: {
4    CUSTOM_KEY: process.env.CUSTOM_KEY,
5  },
6  // 公开给浏览器的变量需要 NEXT_PUBLIC_ 前缀
7}
8
9// .env.local
10DATABASE_URL=postgresql://...
11NEXT_PUBLIC_API_URL=https://api.example.com

性能监控

1// app/layout.tsx
2export default function RootLayout({ children }) {
3  return (
4    <html>
5      <body>
6        {children}
7        <Script
8          src="https://analytics.example.com/script.js"
9          strategy="afterInteractive"
10        />
11      </body>
12    </html>
13  )
14}

Web Vitals 监控

1// app/web-vitals.tsx
2'use client'
3
4import { useReportWebVitals } from 'next/web-vitals'
5
6export function WebVitals() {
7  useReportWebVitals((metric) => {
8    console.log(metric)
9    // 发送到分析服务
10    fetch('/api/analytics', {
11      method: 'POST',
12      body: JSON.stringify(metric),
13    })
14  })
15}

总结

Next.js 14 提供了强大的功能和优化:

  1. Server Components:默认服务端渲染,提升性能
  2. App Router:文件系统路由,更灵活的布局
  3. 数据获取:多种缓存策略,优化性能
  4. 中间件:统一处理认证、国际化等
  5. 错误处理:完善的错误边界系统
  6. API 路由:简洁的 API 开发体验
  7. 元数据:SEO 友好的元数据管理

遵循这些最佳实践,可以构建出高性能、可维护的 Next.js 应用。


相关阅读:

相关文章