Next.js 页面组件最佳实践

2025年2月27日·Admin·Next.js
REVIEWTypeScript

规则评分

0.0

规则描述

优化 Next.js 页面组件的结构、性能和可维护性的规则集合,包括服务器组件和客户端组件的正确使用方式。

提示词

分析这个 Next.js 页面组件,并根据以下最佳实践提供改进建议:
1. 服务器组件与客户端组件的正确分离
2. 数据获取的最佳方式(使用 fetch 或 React Query)
3. 组件结构和职责划分
4. 性能优化机会(如组件拆分、懒加载等)
5. 错误处理和加载状态管理
6. TypeScript 类型定义的完整性
7. 可访问性(a11y)问题

请提供详细的分析和具体的代码改进建议。

规则内容

# Next.js 页面组件最佳实践 ## 规则类型 代码审查 (REVIEW) ## 适用技术 Next.js, React, TypeScript ## 问题描述 Next.js 应用中的页面组件经常混合了过多的职责,导致代码难以维护和测试。常见问题包括: - 服务器组件和客户端组件职责混淆 - 数据获取逻辑不当 - 组件过大,职责不清晰 - 缺乏适当的错误处理和加载状态 - TypeScript 类型定义不完整 - 可访问性问题 ## 规则内容 Next.js 页面组件应遵循以下最佳实践: 1. 明确区分服务器组件和客户端组件 2. 使用适当的数据获取方法 3. 将大型组件拆分为更小的、职责单一的组件 4. 实现完善的错误处理和加载状态 5. 使用完整的 TypeScript 类型定义 6. 确保组件符合可访问性标准 ## 不良代码示例 ```tsx // app/products/page.tsx 'use client' import { useState, useEffect } from 'react' import Link from 'next/link' export default function ProductsPage() { const [products, setProducts] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { async function fetchProducts() { try { const res = await fetch('/api/products') if (!res.ok) throw new Error('Failed to fetch') const data = await res.json() setProducts(data) } catch (err) { setError(err.message) } finally { setLoading(false) } } fetchProducts() }, []) if (loading) return <p>Loading...</p> if (error) return <p>Error: {error}</p> return ( <div> <h1>Products</h1> <div className="grid"> {products.map(product => ( <div key={product.id} className="card"> <img src={product.image} alt={product.name} /> <h2>{product.name}</h2> <p>{product.price}</p> <p>{product.description}</p> <button onClick={() => alert(`Added ${product.name} to cart`)}> Add to Cart </button> <Link href={`/products/${product.id}`}>View Details</Link> </div> ))} </div> </div> ) } ``` ## 推荐代码示例 ```tsx // app/products/page.tsx (服务器组件) import { Suspense } from 'react' import ProductList from './components/ProductList' import ProductListSkeleton from './components/ProductListSkeleton' import { getProducts } from '@/lib/products' import ErrorBoundary from '@/components/ErrorBoundary' export const metadata = { title: 'Products | Our Store', description: 'Browse our latest products', } export default async function ProductsPage() { return ( <div className="container mx-auto py-8"> <h1 className="text-3xl font-bold mb-6">Products</h1> <ErrorBoundary fallback={<p className="text-red-500">Failed to load products</p>}> <Suspense fallback={<ProductListSkeleton />}> <ProductList /> </Suspense> </ErrorBoundary> </div> ) } // app/products/components/ProductList.tsx (服务器组件) import { getProducts } from '@/lib/products' import ProductCard from './ProductCard' import type { Product } from '@/types/product' export default async function ProductList() { const products = await getProducts() return ( <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> {products.map((product: Product) => ( <ProductCard key={product.id} product={product} /> ))} </div> ) } // app/products/components/ProductCard.tsx (客户端组件) 'use client' import { useState } from 'react' import Image from 'next/image' import Link from 'next/link' import { addToCart } from '@/lib/cart-actions' import type { Product } from '@/types/product' interface ProductCardProps { product: Product } export default function ProductCard({ product }: ProductCardProps) { const [isAdding, setIsAdding] = useState(false) async function handleAddToCart() { setIsAdding(true) await addToCart(product.id) setIsAdding(false) } return ( <div className="border rounded-lg overflow-hidden shadow-sm hover:shadow-md transition-shadow"> <div className="relative h-48"> <Image src={product.image} alt={product.name} fill sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" className="object-cover" /> </div> <div className="p-4"> <h2 className="text-xl font-semibold">{product.name}</h2> <p className="text-lg font-bold text-blue-600">{product.price}</p> <p className="text-gray-600 line-clamp-2">{product.description}</p> <div className="mt-4 flex justify-between"> <button onClick={handleAddToCart} disabled={isAdding} aria-label={`Add ${product.name} to cart`} className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50" > {isAdding ? 'Adding...' : 'Add to Cart'} </button> <Link href={`/products/${product.id}`} className="text-blue-600 px-4 py-2 border border-blue-600 rounded hover:bg-blue-50" > View Details </Link> </div> </div> </div> ) } ``` ## 解释说明 改进后的代码有以下优势: 1. **服务器组件与客户端组件分离**: - 页面主体和数据获取使用服务器组件 - 交互部分(如添加到购物车按钮)使用客户端组件 2. **更好的数据获取**: - 使用服务器组件直接获取数据,减少客户端 JavaScript - 避免了客户端的 useEffect 获取数据,减少了瀑布流请求 3. **组件拆分**: - 将页面拆分为多个职责单一的组件 - ProductList 负责展示产品列表 - ProductCard 负责单个产品的展示和交互 4. **改进的错误处理和加载状态**: - 使用 Suspense 和 ErrorBoundary 处理加载和错误状态 - 提供了骨架屏组件作为加载状态 5. **TypeScript 类型增强**: - 为组件 props 和数据添加了明确的类型定义 - 使用类型导入确保类型安全 6. **可访问性改进**: - 添加了 aria-label 属性 - 使用语义化 HTML 结构 - 添加了禁用状态的视觉反馈 7. **性能优化**: - 使用 Image 组件进行图片优化 - 添加了 sizes 属性以适应不同屏幕尺寸 - 使用 line-clamp 限制文本长度 ## 例外情况 在以下情况下可以适当调整这些规则: - 非常简单的页面可以不需要拆分组件 - 某些特殊场景可能需要在客户端组件中获取数据 - 原型开发阶段可以简化错误处理和加载状态 ## 相关规则 - Next.js 数据获取最佳实践 - React 组件拆分原则 - TypeScript 类型定义最佳实践 - React 可访问性指南 ## 参考资料 - [Next.js 官方文档 - 服务器组件](https://nextjs.org/docs/app/building-your-application/rendering/server-components) - [Next.js 官方文档 - 数据获取](https://nextjs.org/docs/app/building-your-application/data-fetching) - [React 官方文档 - 错误边界](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) - [Web 可访问性指南 (WCAG)](https://www.w3.org/WAI/standards-guidelines/wcag/)