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 提供了强大的功能和优化:
- Server Components:默认服务端渲染,提升性能
- App Router:文件系统路由,更灵活的布局
- 数据获取:多种缓存策略,优化性能
- 中间件:统一处理认证、国际化等
- 错误处理:完善的错误边界系统
- API 路由:简洁的 API 开发体验
- 元数据:SEO 友好的元数据管理
遵循这些最佳实践,可以构建出高性能、可维护的 Next.js 应用。
相关阅读:
