05-服务端渲染与元框架——02. Server Components - 服务端组件

05-服务端渲染与元框架——02. Server Components - 服务端组件 02. Server Components - 服务端组件概述Server Components 是 React 19 和 Next.js App Router 的核心特性组件在服务器端渲染不发送客户端 JavaScript。这可以显著减少客户端 bundle 大小并允许组件直接访问后端资源。维度内容What在服务器端渲染的 React 组件不发送客户端 JavaScriptWhy减少客户端 bundle 大小直接访问后端资源提升性能When数据获取、静态内容、SEO 关键页面WhereNext.js App Router 中默认使用Who需要优化性能和 SEO 的开发者How默认组件就是 Server Componentuse client标记客户端组件1. Server Components 是什么1.1 核心概念// ✅ Server Component默认 // 这个组件在服务器上渲染不会发送到客户端 async function ServerComponent() { // 可以直接使用 async/await const data await fetch(https://api.example.com/data).then(res res.json()); return ( div h1{data.title}/h1 p{data.description}/p /div ); } export default ServerComponent;1.2 Server vs Client 对比特性Server ComponentClient Component渲染位置服务器客户端发送 JS❌ 不发送✅ 发送访问后端✅ 直接访问❌ 需要 API使用 Hooks❌ 不能✅ 可以使用事件❌ 不能✅ 可以使用浏览器 API❌ 不能✅ 可以SEO✅ 完全支持⚠️ 部分支持2. Server Components 的优势2.1 零客户端 Bundle// ❌ 传统方式整个库都会打包到客户端 import moment from moment; import { marked } from marked; import Prism from prismjs; function PostContent({ content }) { const html marked(content); return div dangerouslySetInnerHTML{{ __html: html }} /; } // 发送到客户端moment (300KB) marked (50KB) prism (200KB)// ✅ Server Component库只在服务器端使用 // 这些库不会发送到客户端 import moment from moment; import { marked } from marked; import Prism from prismjs; async function PostContent({ slug }) { const content await getPostContent(slug); const html marked(content); const formattedDate moment().format(YYYY-MM-DD); const highlightedCode Prism.highlight(code, Prism.languages.javascript, javascript); return ( div time{formattedDate}/time div dangerouslySetInnerHTML{{ __html: html }} / /div ); } // 发送到客户端只有 HTML 结果没有库代码2.2 直接访问后端资源// ✅ Server Component 可以直接访问数据库 import { db } from /lib/db; async function UserList() { // 直接查询数据库不需要 API 层 const users await db.user.findMany(); return ( ul {users.map(user ( li key{user.id}{user.name}/li ))} /ul ); }2.3 自动缓存和去重// ✅ Server Component 中的 fetch 自动缓存 async function Dashboard() { // 这个请求会自动缓存 const user await fetch(https://api.example.com/user, { cache: force-cache, }).then(res res.json()); // 多个组件请求相同数据会自动去重 const posts await fetch(https://api.example.com/posts).then(res res.json()); return ( div UserInfo user{user} / PostList posts{posts} / /div ); }3. Server Components 的限制3.1 不能使用的功能// ❌ Server Component 不能使用这些 use server; // 这不是 Server Component 标记 import { useState, useEffect } from react; // 不能使用 export default function ServerComponent() { // ❌ 不能使用 useState const [count, setCount] useState(0); // ❌ 不能使用 useEffect useEffect(() { console.log(mount); }, []); // ❌ 不能使用事件处理器 const handleClick () { console.log(click); }; // ❌ 不能使用浏览器 API const width window.innerWidth; // ❌ 不能使用 localStorage const token localStorage.getItem(token); return ( button onClick{handleClick}点击/button ); }3.2 需要 Client Component 的场景// 需要交互时使用 use client 标记 use client; import { useState } from react; export default function Counter() { const [count, setCount] useState(0); return ( button onClick{() setCount(count 1)} 点击次数: {count} /button ); }4. 数据获取4.1 在 Server Component 中获取数据// app/users/page.js async function UsersPage() { // 方式1使用 fetch const users await fetch(https://api.example.com/users, { cache: force-cache, // 强制缓存 }).then(res res.json()); // 方式2直接调用数据库 const posts await db.post.findMany(); // 方式3调用内部服务 const data await getDataFromInternalService(); return ( div UserList users{users} / PostList posts{posts} / /div ); }4.2 并行数据获取// app/dashboard/page.js async function DashboardPage() { // 并行获取数据 const [user, posts, notifications] await Promise.all([ fetch(https://api.example.com/user).then(res res.json()), fetch(https://api.example.com/posts).then(res res.json()), fetch(https://api.example.com/notifications).then(res res.json()), ]); return ( div UserCard user{user} / PostList posts{posts} / NotificationList notifications{notifications} / /div ); }4.3 串行数据获取// app/user/[id]/page.js async function UserProfilePage({ params }) { const { id } await params; // 先获取用户信息 const user await fetch(https://api.example.com/users/${id}).then(res res.json()); // 然后获取用户的文章依赖 user.id const posts await fetch(https://api.example.com/users/${user.id}/posts).then(res res.json()); return ( div h1{user.name}/h1 PostList posts{posts} / /div ); }5. 与 Client Component 组合5.1 在 Server Component 中使用 Client Component// app/page.js (Server Component) import ClientCounter from ./ClientCounter; import ClientThemeToggle from ./ClientThemeToggle; async function HomePage() { const data await fetchData(); return ( div {/* Server Component 渲染静态内容 */} h1{data.title}/h1 {/* Client Component 处理交互 */} ClientCounter initialCount{data.count} / ClientThemeToggle / /div ); }5.2 传递 Props 给 Client Component// app/components/ClientList.jsx use client; export default function ClientList({ items, onItemClick }) { return ( ul {items.map(item ( li key{item.id} onClick{() onItemClick(item.id)} {item.name} /li ))} /ul ); } // app/page.js (Server Component) async function Page() { const items await fetchItems(); // Server Component 传递数据给 Client Component return ( ClientList items{items} onItemClick{(id) console.log(id)} / ); }5.3 嵌套 Client Component// app/components/InteractiveCard.jsx use client; import { useState } from react; import ClientButton from ./ClientButton; export default function InteractiveCard({ children, title }) { const [isExpanded, setIsExpanded] useState(false); return ( div classNamecard h2{title}/h2 button onClick{() setIsExpanded(!isExpanded)} {isExpanded ? 收起 : 展开} /button {isExpanded ( div classNamecard-content {children} ClientButton / /div )} /div ); } // app/page.js (Server Component) async function Page() { const data await fetchData(); return ( InteractiveCard titleServer Data {/* children 可以是 Server Component 渲染的内容 */} div{data.content}/div /InteractiveCard ); }6. 模式和实践6.1 组合模式// app/products/page.js (Server Component) async function ProductsPage() { const products await fetchProducts(); return ( div h1商品列表/h1 ProductGrid products{products} / /div ); } // app/components/ProductGrid.jsx (Client Component) use client; import { useState } from react; export default function ProductGrid({ products }) { const [sortBy, setSortBy] useState(price); const sortedProducts [...products].sort((a, b) { if (sortBy price) return a.price - b.price; return a.name.localeCompare(b.name); }); return ( div div button onClick{() setSortBy(price)}按价格排序/button button onClick{() setSortBy(name)}按名称排序/button /div div classNamegrid {sortedProducts.map(product ( ProductCard key{product.id} product{product} / ))} /div /div ); } // app/components/ProductCard.jsx (Client Component) use client; export default function ProductCard({ product }) { const addToCart () { // 添加到购物车的逻辑 }; return ( div classNameproduct-card img src{product.image} alt{product.name} / h3{product.name}/h3 p¥{product.price}/p button onClick{addToCart}加入购物车/button /div ); }6.2 流式渲染// app/blog/page.js import { Suspense } from react; async function BlogPosts() { const posts await fetchPosts(); return PostList posts{posts} /; } async function Sidebar() { const categories await fetchCategories(); return CategoryList categories{categories} /; } async function RecommendedPosts() { const recommended await fetchRecommended(); return RecommendedList posts{recommended} /; } export default function BlogPage() { return ( div Suspense fallback{div加载文章.../div} BlogPosts / /Suspense Suspense fallback{div加载侧边栏.../div} Sidebar / /Suspense Suspense fallback{div加载推荐.../div} RecommendedPosts / /Suspense /div ); }6.3 使用 Server Actions// app/actions.js use server; import { revalidatePath } from next/cache; export async function createPost(formData) { const title formData.get(title); const content formData.get(content); // 在服务器上执行 await db.post.create({ data: { title, content } }); // 重新验证缓存 revalidatePath(/blog); } // app/blog/new/page.js import { createPost } from ../actions; export default function NewPostPage() { return ( form action{createPost} input nametitle placeholder标题 / textarea namecontent placeholder内容 / button typesubmit发布/button /form ); }7. 性能优化7.1 缓存策略// app/page.js async function Page() { // 静态缓存 const staticData await fetch(https://api.example.com/static, { cache: force-cache, }); // 动态数据 const dynamicData await fetch(https://api.example.com/dynamic, { cache: no-store, }); // 定期重新验证 const revalidatedData await fetch(https://api.example.com/revalidate, { next: { revalidate: 3600 }, // 1 小时后重新验证 }); return div{/* ... */}/div; }7.2 避免不必要的 Client Component// ❌ 过度使用 Client Component use client; import { useState } from react; export default function ProductPage() { // 这个页面大部分是静态内容不需要客户端交互 const [showModal, setShowModal] useState(false); return ( div h1Product Title/h1 pProduct description.../p button onClick{() setShowModal(true)}Buy Now/button /div ); } // ✅ 优化将交互部分单独提取为 Client Component // app/product/page.js (Server Component) export default function ProductPage() { return ( div h1Product Title/h1 pProduct description.../p BuyButton / /div ); } // app/product/BuyButton.jsx (Client Component) use client; export default function BuyButton() { const [showModal, setShowModal] useState(false); return button onClick{() setShowModal(true)}Buy Now/button; }8. 完整示例电商产品页// app/products/[id]/page.js (Server Component) import { Suspense } from react; import Image from next/image; import AddToCartButton from ./AddToCartButton; import ReviewSection from ./ReviewSection; import RelatedProducts from ./RelatedProducts; async function getProduct(id) { const res await fetch(https://api.example.com/products/${id}, { next: { revalidate: 3600 }, }); return res.json(); } async function getProductReviews(id) { const res await fetch(https://api.example.com/products/${id}/reviews); return res.json(); } export default async function ProductPage({ params }) { const { id } await params; const product await getProduct(id); return ( div classNameproduct-page div classNameproduct-main Image src{product.image} alt{product.name} width{600} height{600} priority / div classNameproduct-info h1{product.name}/h1 p classNameprice¥{product.price}/p p classNamedescription{product.description}/p {/* 客户端交互组件 */} AddToCartButton productId{product.id} / /div /div {/* 流式加载评论 */} Suspense fallback{div加载评论.../div} ReviewSection productId{product.id} / /Suspense {/* 流式加载推荐商品 */} Suspense fallback{div加载推荐.../div} RelatedProducts productId{product.id} category{product.category} / /Suspense /div ); } // app/products/[id]/AddToCartButton.jsx (Client Component) use client; import { useState } from react; import { useRouter } from next/navigation; export default function AddToCartButton({ productId }) { const [isLoading, setIsLoading] useState(false); const router useRouter(); const handleAddToCart async () { setIsLoading(true); const res await fetch(/api/cart, { method: POST, body: JSON.stringify({ productId, quantity: 1 }), }); if (res.ok) { router.refresh(); } setIsLoading(false); }; return ( button onClick{handleAddToCart} disabled{isLoading} {isLoading ? 添加中... : 加入购物车} /button ); } // app/products/[id]/ReviewSection.jsx (Server Component) async function ReviewSection({ productId }) { const reviews await getProductReviews(productId); return ( div classNamereviews h2用户评价/h2 {reviews.map(review ( div key{review.id} classNamereview div classNamerating{⭐.repeat(review.rating)}/div p{review.content}/p span{review.userName}/span /div ))} /div ); } // app/products/[id]/RelatedProducts.jsx (Server Component) async function RelatedProducts({ productId, category }) { const products await getRelatedProducts(productId, category); return ( div classNamerelated h2相关推荐/h2 div classNameproduct-grid {products.map(product ( a key{product.id} href{/products/${product.id}} Image src{product.image} alt{product.name} width{200} height{200} / h3{product.name}/h3 p¥{product.price}/p /a ))} /div /div ); }9. 总结核心要点要点说明默认行为App Router 中的组件默认是 Server Component主要优势零客户端 bundle、直接访问后端、自动缓存限制不能使用 hooks、事件、浏览器 API标记use client声明 Client Component何时使用 Server vs Client场景使用数据获取ServerSEO 关键内容Server静态内容Server用户交互Client使用状态/效果Client使用浏览器 APIClient记忆口诀Server 组件默认配不发 JS 体积小数据获取直接查缓存去重自动好需要交互标 client各司其职性能高10. 相关资源React Server Components 文档Next.js Server ComponentsServer vs Client Components