Vue3.4 Effect 作用域 API 与 React Server Components 实战解析
Vue3.4 Effect 作用域 API 与 React Server Components 实战解析
随着前端技术的快速发展,Vue 3.4 引入的 Effect 作用域 API 和 React 的 Server Components 为现代前端开发带来了新的可能性。本文将深入探讨这两项技术的原理、实战应用以及它们在不同场景下的最佳实践。
一、Vue 3.4 Effect 作用域 API 详解
1.1 什么是 Effect 作用域
Effect 作用域是 Vue 3.4 引入的一个重要概念,它提供了一种更精细的方式来管理和控制响应式副作用(effects)的生命周期。在深入了解 Effect 作用域之前,我们先回顾一下 Vue 的响应式系统。
// Vue 3 响应式系统基础
import { reactive, effect } from 'vue'const state = reactive({ count: 0 })// 创建一个 effect(副作用)
effect(() => {console.log('Count changed:', state.count)
})state.count++ // 触发 effect 重新执行
1.2 Effect 作用域的核心概念
Effect 作用域允许我们将相关的 effects 组织在一起,并在需要时批量停止或清理它们。
import { effectScope, computed, watch, onScopeDispose } from 'vue'// 创建一个 effect 作用域
const scope = effectScope()// 在作用域内创建 effects
scope.run(() => {const doubled = computed(() => counter.value * 2)watch(doubled, () => console.log(doubled.value))watchEffect(() => console.log('Count: ', counter.value))// 注册作用域销毁时的清理函数onScopeDispose(() => {console.log('Scope disposed')})
})// 停止作用域内的所有 effects
scope.stop()
1.3 实际应用场景
场景一:组件生命周期管理
// MyComponent.vue
import { effectScope, onMounted, onUnmounted } from 'vue'export default {setup() {let scopeonMounted(() => {// 创建 effect 作用域scope = effectScope()scope.run(() => {// 组件内的所有响应式副作用watchEffect(() => {// 监听数据变化updateChart(data.value)})watch(() => props.id, (newId) => {// 监听 props 变化fetchData(newId)})// 定时器管理const timer = setInterval(() => {refreshData()}, 5000)// 清理函数onScopeDispose(() => {clearInterval(timer)console.log('Component effects cleaned up')})})})onUnmounted(() => {// 组件卸载时停止所有 effectsscope?.stop()})}
}
场景二:异步操作管理
// useAsyncData.js
import { ref, effectScope, watch, onScopeDispose } from 'vue'export function useAsyncData(fetchFn, options = {}) {const data = ref(null)const error = ref(null)const loading = ref(false)// 创建 effect 作用域const scope = effectScope()let abortController = nullconst execute = async (params) => {// 取消之前的请求abortController?.abort()abortController = new AbortController()loading.value = trueerror.value = nulltry {const result = await fetchFn(params, {signal: abortController.signal})data.value = result} catch (err) {if (err.name !== 'AbortError') {error.value = err}} finally {loading.value = false}}// 在作用域内运行scope.run(() => {// 监听依赖变化if (options.deps) {watch(() => options.deps(),(newDeps) => {if (newDeps && options.immediate !== false) {execute(newDeps)}},{ immediate: options.immediate !== false })}// 注册清理函数onScopeDispose(() => {abortController?.abort()})})// 提供停止方法const stop = () => scope.stop()return {data,error,loading,execute,stop}
}// 使用示例
export default {setup() {const { data, error, loading, stop } = useAsyncData(async (userId) => {const response = await fetch(`/api/users/${userId}`)return response.json()},{deps: () => route.params.userId,immediate: true})// 组件卸载时清理onUnmounted(() => {stop()})return { data, error, loading }}
}
1.4 最佳实践与性能优化
// 最佳实践:Effect 作用域组合
import { effectScope, computed } from 'vue'export function createDataScope() {const scope = effectScope()return scope.run(() => {const data = ref({})const loading = ref(false)const error = ref(null)// 计算属性const processedData = computed(() => {return Object.keys(data.value).reduce((acc, key) => {acc[key] = data.value[key] * 2return acc}, {})})// 方法const fetchData = async () => {loading.value = truetry {const response = await fetch('/api/data')data.value = await response.json()} catch (err) {error.value = err} finally {loading.value = false}}return {data,loading,error,processedData,fetchData,stop: () => scope.stop()}})
}// 性能优化:批量更新
import { effectScope, nextTick } from 'vue'export function useBatchUpdate() {const scope = effectScope()const pendingUpdates = new Set()scope.run(() => {let updateScheduled = falseconst scheduleUpdate = (fn) => {pendingUpdates.add(fn)if (!updateScheduled) {updateScheduled = truenextTick(() => {pendingUpdates.forEach(update => update())pendingUpdates.clear()updateScheduled = false})}}return { scheduleUpdate }})return {scheduleUpdate: scope.run(() => scheduleUpdate),flush: () => {pendingUpdates.forEach(update => update())pendingUpdates.clear()},stop: () => scope.stop()}
}
二、React Server Components 实战
2.1 React Server Components 简介
React Server Components(RSC)是 React 18 引入的一项革命性特性,它允许组件在服务器上渲染,将渲染结果以特殊格式发送到客户端。这与传统的 SSR(服务端渲染)有本质区别。
2.2 环境搭建
首先,我们需要搭建支持 React Server Components 的开发环境。这里使用 Next.js 13+ 作为示例。
# 创建 Next.js 项目(支持 RSC)
npx create-next-app@latest my-rsc-app --typescript --app# 进入项目目录
cd my-rsc-app# 安装依赖
npm install# 启动开发服务器
npm run dev
项目结构:
my-rsc-app/
├── app/
│ ├── layout.tsx # 根布局(服务端组件)
│ ├── page.tsx # 首页(服务端组件)
│ ├── components/
│ │ ├── ServerComponent.tsx # 服务端组件
│ │ ├── ClientComponent.tsx # 客户端组件
│ │ └── SharedComponent.tsx # 共享组件
│ └── lib/
│ ├── data.ts # 数据获取函数
│ └── utils.ts # 工具函数
├── package.json
└── tsconfig.json
2.3 Server Components 基础
服务端组件(默认)
// app/components/ServerComponent.tsx
import { Suspense } from 'react'// 这是一个服务端组件
async function getData() {// 直接访问数据库或外部 APIconst res = await fetch('https://api.example.com/data', {// 可以包含 API keys 等敏感信息headers: {'Authorization': `Bearer ${process.env.API_SECRET_KEY}`}})if (!res.ok) {throw new Error('Failed to fetch data')}return res.json()
}export default async function ServerComponent() {// 服务端直接获取数据const data = await getData()return (<div><h2>Server Component Data</h2><pre>{JSON.stringify(data, null, 2)}</pre></div>)
}
客户端组件
// app/components/ClientComponent.tsx
'use client'import { useState, useEffect } from 'react'export default function ClientComponent() {const [count, setCount] = useState(0)const [isClient, setIsClient] = useState(false)// 确保只在客户端执行useEffect(() => {setIsClient(true)}, [])return (<div><h2>Client Component</h2><p>Count: {count}</p><button onClick={() => setCount(count + 1)}>Increment</button>{isClient && (<p>Client-side only: {window.navigator.userAgent}</p>)}</div>)
}
共享组件
// app/components/SharedComponent.tsx
import { ServerComponent } from './ServerComponent'
import ClientComponent from './ClientComponent'// 这个组件可以在服务端和客户端运行
export default function SharedComponent({ initialData }: { initialData: any }) {return (<div><h1>Shared Component</h1><ServerComponent /><ClientComponent /></div>)
}
2.4 数据获取策略
服务端数据获取
// app/lib/data.ts
export interface User {id: numbername: stringemail: stringposts: Post[]
}export interface Post {id: numbertitle: stringcontent: stringauthorId: number
}// 模拟数据库
const users: User[] = [{id: 1,name: 'John Doe',email: 'john@example.com',posts: [{ id: 1, title: 'First Post', content: 'Hello World', authorId: 1 },{ id: 2, title: 'Second Post', content: 'React Server Components', authorId: 1 }]}
]export async function getUsers(): Promise<User[]> {// 模拟异步操作await new Promise(resolve => setTimeout(resolve, 1000))return users
}export async function getUserById(id: number): Promise<User | undefined> {await new Promise(resolve => setTimeout(resolve, 500))return users.find(user => user.id === id)
}export async function getPosts(): Promise<Post[]> {await new Promise(resolve => setTimeout(resolve, 800))return users.flatMap(user => user.posts)
}
服务端组件中使用数据
// app/users/page.tsx
import { getUsers, getUserById } from '../lib/data'
import { Suspense } from 'react'// 加载组件
function LoadingSpinner() {return <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
}// 用户列表组件
async function UsersList() {const users = await getUsers()return (<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">{users.map(user => (<UserCard key={user.id} user={user} />))}</div>)
}// 用户卡片组件
async function UserCard({ user }: { user: any }) {return (<div className="border rounded-lg p-4 shadow-sm"><h3 className="font-semibold text-lg">{user.name}</h3><p className="text-gray-600">{user.email}</p><p className="text-sm text-gray-500">{user.posts.length} posts</p></div>)
}// 主页面组件
export default async function UsersPage() {return (<div className="container mx-auto p-4"><h1 className="text-3xl font-bold mb-6">Users</h1>{/* 使用 Suspense 处理异步加载 */}<Suspense fallback={<LoadingSpinner />}><UsersList /></Suspense></div>)
}
流式渲染
// app/streaming/page.tsx
import { Suspense } from 'react'// 慢速组件
async function SlowComponent() {await new Promise(resolve => setTimeout(resolve, 3000))return <div>Loaded after 3 seconds</div>
}// 快速组件
async function FastComponent() {await new Promise(resolve => setTimeout(resolve, 1000))return <div>Loaded after 1 second</div>
}export default function StreamingPage() {return (<div><h1>Streaming Example</h1>{/* 快速组件立即显示 */}<Suspense fallback={<p>Loading fast component...</p>}><FastComponent /></Suspense>{/* 慢速组件独立加载 */}<Suspense fallback={<p>Loading slow component...</p>}><SlowComponent /></Suspense></div>)
}
2.5 客户端与服务端交互
服务端组件传递数据给客户端组件
// app/components/InteractiveChart.tsx
'use client'import { useState, useEffect } from 'react'
import { Line } from 'react-chartjs-2'interface ChartData {labels: string[]datasets: {label: stringdata: number[]borderColor: stringbackgroundColor: string}[]
}export default function InteractiveChart({ initialData,title
}: { initialData: ChartDatatitle: string
}) {const [data, setData] = useState<ChartData>(initialData)const [filter, setFilter] = useState('all')// 客户端交互逻辑const handleFilterChange = (newFilter: string) => {setFilter(newFilter)// 根据筛选条件更新数据const filteredData = filterData(initialData, newFilter)setData(filteredData)}return (<div className="p-4 border rounded-lg"><h3 className="text-xl font-semibold mb-4">{title}</h3><div className="mb-4"><buttononClick={() => handleFilterChange('all')}className={`px-4 py-2 mr-2 rounded ${filter === 'all' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}>All Data</button><buttononClick={() => handleFilterChange('recent')}className={`px-4 py-2 mr-2 rounded ${filter === 'recent' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}>Recent</button></div><div style={{ height: '300px' }}><Line data={data} /></div></div>)
}
服务端组件使用客户端组件
// app/dashboard/page.tsx
import InteractiveChart from '../components/InteractiveChart'
import { getChartData } from '../lib/data'export default async function DashboardPage() {// 服务端获取数据const chartData = await getChartData()return (<div className="container mx-auto p-4"><h1 className="text-3xl font-bold mb-6">Dashboard</h1>{/* 将服务端数据传递给客户端组件 */}<InteractiveChart initialData={chartData}title="Sales Analytics"/></div>)
}
2.6 性能优化策略
1. 组件级缓存
// app/lib/cache.ts
import { unstable_cache } from 'next/cache'// 创建缓存函数
export const getCachedData = unstable_cache(async (key: string) => {// 模拟数据库查询const data = await fetch(`https://api.example.com/data/${key}`)return data.json()},['data-cache'], // 缓存 key{revalidate: 3600, // 1小时重新验证tags: ['data'], // 缓存标签,用于批量失效}
)// 在组件中使用
export default async function CachedComponent({ id }: { id: string }) {const data = await getCachedData(id)return (<div><h2>Cached Data</h2><pre>{JSON.stringify(data, null, 2)}</pre></div>)
}
2. 流式 SSR 优化
// app/products/page.tsx
import { Suspense } from 'react'// 产品列表(可以流式传输)
async function ProductList() {const products = await getProducts()return (<div className="grid grid-cols-3 gap-4">{products.map(product => (<ProductCard key={product.id} product={product} />))}</div>)
}// 推荐商品(可能需要更长时间)
async function Recommendations() {const recommendations = await getRecommendations()return (<div className="mt-8"><h2 className="text-2xl font-bold mb-4">Recommended for You</h2><div className="grid grid-cols-4 gap-4">{recommendations.map(item => (<div key={item.id} className="border p-4 rounded"><h3>{item.name}</h3><p>${item.price}</p></div>))}</div></div>)
}export default function ProductsPage() {return (<div className="container mx-auto p-4"><h1 className="text-3xl font-bold mb-6">Products</h1>{/* 主要产品列表立即开始加载 */}<Suspense fallback={<div>Loading products...</div>}><ProductList /></Suspense>{/* 推荐部分可以独立加载 */}<Suspense fallback={<div>Loading recommendations...</div>}><Recommendations /></Suspense></div>)
}
3. 选择性水合
// app/components/HeavyComponent.tsx
'use client'import { startTransition, useDeferredValue } from 'react'export default function HeavyComponent({ data }: { data: any[] }) {const [filter, setFilter] = useState('')const deferredFilter = useDeferredValue(filter)// 昂贵的计算const filteredData = useMemo(() => {return data.filter(item => item.name.toLowerCase().includes(deferredFilter.toLowerCase()))}, [data, deferredFilter])return (<div><inputtype="text"value={filter}onChange={(e) => startTransition(() => setFilter(e.target.value))}placeholder="Filter items..."className="border p-2 mb-4 w-full"/>{/* 使用 deferred value 避免阻塞渲染 */}<div className="grid gap-2">{filteredData.map(item => (<div key={item.id} className="border p-2 rounded">{item.name}</div>))}</div></div>)
}
三、Vue Effect 作用域 vs React Server Components
3.1 核心概念对比
| 特性 | Vue Effect 作用域 | React Server Components |
|---|---|---|
| 主要目的 | 管理响应式副作用生命周期 | 服务端渲染优化 |
| 运行环境 | 客户端 | 服务端 + 客户端 |
| 数据获取 | 客户端异步 | 服务端直接获取 |
| 性能优势 | 精确控制 effect 生命周期 | 减少 JS Bundle 大小 |
| 使用场景 | 复杂状态管理 | 内容为主的页面 |
3.2 适用场景分析
Vue Effect 作用域适用场景
-
复杂组件状态管理
// 大型表单组件 function useComplexForm() {const scope = effectScope()return scope.run(() => {const formData = reactive({})const validationErrors = ref({})const isSubmitting = ref(false)// 多个相关的 watcherswatch(() => formData.email, validateEmail)watch(() => formData.password, validatePassword)watchEffect(() => {// 自动保存逻辑autoSave(formData)})return {formData,validationErrors,isSubmitting,submit: () => { /* 提交逻辑 */ },stop: () => scope.stop()}}) } -
异步操作管理
// 多个相关的异步操作 function useAsyncOperations() {const scope = effectScope()return scope.run(() => {const operations = reactive({upload: { loading: false, error: null },download: { loading: false, error: null },sync: { loading: false, error: null }})// 批量管理所有异步操作const cancelAll = () => {scope.stop()}return { operations, cancelAll }}) }
React Server Components 适用场景
-
内容密集型应用
// 博客系统 export default async function BlogPost({ params }: { params: { slug: string } }) {// 服务端直接获取文章内容const post = await getPostBySlug(params.slug)const relatedPosts = await getRelatedPosts(post.id)return (<article><h1>{post.title}</h1><div>{post.content}</div><aside><h2>Related Posts</h2>{relatedPosts.map(post => (<RelatedPost key={post.id} post={post} />))}</aside></article>) } -
需要访问数据库或内部 API 的应用
// 管理后台 export default async function AdminDashboard() {// 服务端可以直接访问数据库const stats = await getDashboardStats()const recentUsers = await getRecentUsers(10)const systemHealth = await checkSystemHealth()return (<DashboardLayout><StatsGrid stats={stats} /><UserTable users={recentUsers} /><SystemStatus health={systemHealth} /></DashboardLayout>) }
3.3 混合使用策略
在实际项目中,我们可以结合使用两种技术:
// 结合 Vue Effect 作用域和 SSR
// app.vue
<template><div><ServerRenderedContent :initial-data="serverData" /></div>
</template><script setup>
import { effectScope } from 'vue'// 服务端渲染的数据
const serverData = ref(__INITIAL_DATA__)// 使用 Effect 作用域管理客户端状态
const scope = effectScope()scope.run(() => {const clientState = reactive({isInteractive: false,userPreferences: {}})// 客户端特定的响应式逻辑watchEffect(() => {if (clientState.isInteractive) {// 启用交互功能enableInteractiveFeatures()}})
})onUnmounted(() => {scope.stop()
})
</script>
四、实战项目:构建一个现代博客系统
4.1 项目架构
我们将构建一个结合两种技术的现代博客系统:
modern-blog/
├── app/ # Next.js App Router
│ ├── layout.tsx # 根布局
│ ├── page.tsx # 首页
│ ├── blog/
│ │ ├── [slug]/
│ │ │ └── page.tsx # 博客文章页面(RSC)
│ │ └── page.tsx # 博客列表页面(RSC)
│ └── admin/
│ ├── page.tsx # 管理后台(RSC)
│ └── edit/
│ └── [id]/
│ └── page.tsx # 编辑页面(RSC + Client Components)
├── components/
│ ├── server/ # 服务端组件
│ │ ├── BlogPost.tsx
│ │ ├── BlogList.tsx
│ │ └── AdminPanel.tsx
│ ├── client/ # 客户端组件
│ │ ├── CommentSection.tsx
│ │ ├── LikeButton.tsx
│ │ └── RichEditor.tsx
│ └── shared/ # 共享组件
│ ├── Button.tsx
│ └── Card.tsx
├── lib/
│ ├── data.ts # 数据层
│ ├── auth.ts # 认证
│ └── utils.ts # 工具函数
└── vue-components/ # Vue 组件(用于管理后台)├── admin/│ ├── PostEditor.vue│ ├── MediaManager.vue│ └── Analytics.vue└── composables/ # Vue Composables├── usePostEditor.ts├── useMediaUpload.ts└── useAnalytics.ts
4.2 核心功能实现
服务端组件:博客文章页面
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'
import { Suspense } from 'react'
import BlogPost from '@/components/server/BlogPost'
import CommentSection from '@/components/client/CommentSection'
import LikeButton from '@/components/client/LikeButton'
import { getPostBySlug, incrementViewCount } from '@/lib/data'interface PageProps {params: { slug: string }searchParams: { [key: string]: string | string[] | undefined }
}export default async function BlogPostPage({ params }: PageProps) {// 并行获取数据const [post, relatedPosts] = await Promise.all([getPostBySlug(params.slug),getRelatedPosts(params.slug)])if (!post) {notFound()}// 增加浏览量(服务端操作)await incrementViewCount(post.id)return (<article className="max-w-4xl mx-auto p-6">{/* 文章头部 */}<header className="mb-8"><h1 className="text-4xl font-bold mb-4">{post.title}</h1><div className="flex items-center text-gray-600 mb-4"><time dateTime={post.createdAt}>{new Date(post.createdAt).toLocaleDateString()}</time><span className="mx-2">•</span><span>{post.readingTime} min read</span><span className="mx-2">•</span><span>{post.views} views</span></div><div className="flex gap-2">{post.tags.map(tag => (<span key={tag} className="bg-blue-100 text-blue-800 px-2 py-1 rounded text-sm">{tag}</span>))}</div></header>{/* 文章内容 */}<div className="prose prose-lg max-w-none mb-8"><div dangerouslySetInnerHTML={{ __html: post.content }} /></div>{/* 互动区域 */}<div className="border-t pt-8"><div className="flex items-center justify-between mb-6"><h2 className="text-2xl font-semibold">互动</h2><div className="flex gap-4">{/* 客户端点赞组件 */}<LikeButton postId={post.id} initialLikes={post.likes} /><ShareButton post={post} /></div></div>{/* 评论区域 */}<Suspense fallback={<div>Loading comments...</div>}><CommentSection postId={post.id} /></Suspense></div>{/* 相关文章 */}{relatedPosts.length > 0 && (<section className="mt-12 border-t pt-8"><h2 className="text-2xl font-semibold mb-6">相关文章</h2><div className="grid gap-6 md:grid-cols-2">{relatedPosts.map(relatedPost => (<RelatedPostCard key={relatedPost.id} post={relatedPost} />))}</div></section>)}</article>)
}
Vue Composables:文章编辑器
// vue-components/composables/usePostEditor.ts
import { effectScope, ref, reactive, watch, computed } from 'vue'export interface PostEditorState {title: stringcontent: stringexcerpt: stringtags: string[]coverImage: string | nullpublished: boolean
}export function usePostEditor(initialPost?: PostEditorState) {const scope = effectScope()return scope.run(() => {const state = reactive<PostEditorState>({title: initialPost?.title || '',content: initialPost?.content || '',excerpt: initialPost?.excerpt || '',tags: initialPost?.tags || [],coverImage: initialPost?.coverImage || null,published: initialPost?.published || false})const isSaving = ref(false)const lastSaved = ref<Date | null>(null)const wordCount = computed(() => {return state.content.split(/\s+/).filter(word => word.length > 0).length})const readingTime = computed(() => {const wordsPerMinute = 200return Math.ceil(wordCount.value / wordsPerMinute)})const isValid = computed(() => {return state.title.trim().length > 0 && state.content.trim().length > 0 &&state.excerpt.trim().length > 0})// 自动保存逻辑let autoSaveTimer: NodeJS.Timeout | null = nullconst scheduleAutoSave = () => {if (autoSaveTimer) {clearTimeout(autoSaveTimer)}autoSaveTimer = setTimeout(() => {if (isValid.value) {autoSave()}}, 30000) // 30秒自动保存}// 监听内容变化watch(() => [state.title, state.content, state.excerpt, state.tags],() => {scheduleAutoSave()},{ deep: true })// 自动保存函数const autoSave = async () => {if (isSaving.value) returnisSaving.value = truetry {// 调用 API 保存草稿await saveDraft(state)lastSaved.value = new Date()} catch (error) {console.error('Auto-save failed:', error)} finally {isSaving.value = false}}// 手动保存const save = async () => {if (!isValid.value) {throw new Error('Please fill in all required fields')}isSaving.value = truetry {await savePost(state)lastSaved.value = new Date()} catch (error) {console.error('Save failed:', error)throw error} finally {isSaving.value = false}}// 发布const publish = async () => {if (!isValid.value) {throw new Error('Please fill in all required fields')}isSaving.value = truetry {await publishPost({ ...state, published: true })state.published = true} catch (error) {console.error('Publish failed:', error)throw error} finally {isSaving.value = false}}// 添加标签const addTag = (tag: string) => {if (tag.trim() && !state.tags.includes(tag.trim())) {state.tags.push(tag.trim())}}// 移除标签const removeTag = (tag: string) => {const index = state.tags.indexOf(tag)if (index > -1) {state.tags.splice(index, 1)}}// 上传封面图片const uploadCoverImage = async (file: File) => {try {const url = await uploadImage(file)state.coverImage = url} catch (error) {console.error('Image upload failed:', error)throw error}}// 清理函数const dispose = () => {if (autoSaveTimer) {clearTimeout(autoSaveTimer)}scope.stop()}return {state,isSaving,lastSaved,wordCount,readingTime,isValid,save,publish,addTag,removeTag,uploadCoverImage,dispose}})
}
Vue 组件:媒体管理器
<!-- vue-components/admin/MediaManager.vue -->
<template><div class="media-manager"><div class="media-header"><h3>Media Library</h3><button @click="showUploadDialog = true" class="upload-btn">Upload New</button></div><div class="media-grid" v-if="mediaItems.length > 0"><div v-for="item in mediaItems" :key="item.id"class="media-item":class="{ selected: selectedItems.includes(item.id) }"@click="toggleSelection(item.id)"><img :src="item.url" :alt="item.alt" /><div class="media-info"><p class="media-title">{{ item.title }}</p><p class="media-size">{{ formatFileSize(item.size) }}</p></div><button @click.stop="deleteMedia(item.id)" class="delete-btn">Delete</button></div></div><div v-else class="empty-state"><p>No media items found</p></div><!-- 上传对话框 --><UploadDialog v-if="showUploadDialog"@close="showUploadDialog = false"@uploaded="handleUploaded"/></div>
</template><script setup lang="ts">
import { ref, computed, onMounted, effectScope, watch } from 'vue'
import { useMediaUpload } from '../composables/useMediaUpload'interface MediaItem {id: stringurl: stringtitle: stringalt: stringsize: numbertype: stringuploadedAt: Date
}// Props
const props = defineProps<{maxSelection?: numberaccept?: string[]
}>()// Emits
const emit = defineEmits<{select: [items: MediaItem[]]upload: [item: MediaItem]
}>()// 使用 effect 作用域管理副作用
const scope = effectScope()const { mediaItems, loading, error,fetchMedia,deleteMedia,uploadMedia
} = scope.run(() => {const items = ref<MediaItem[]>([])const isLoading = ref(false)const errorMessage = ref<string | null>(null)// 获取媒体列表const fetchMedia = async () => {isLoading.value = trueerrorMessage.value = nulltry {const response = await fetch('/api/media')if (!response.ok) throw new Error('Failed to fetch media')const data = await response.json()items.value = data.map((item: any) => ({...item,uploadedAt: new Date(item.uploadedAt)}))} catch (err) {errorMessage.value = err instanceof Error ? err.message : 'Unknown error'} finally {isLoading.value = false}}// 删除媒体const deleteMedia = async (id: string) => {try {const response = await fetch(`/api/media/${id}`, {method: 'DELETE'})if (!response.ok) throw new Error('Failed to delete media')// 本地删除const index = items.value.findIndex(item => item.id === id)if (index > -1) {items.value.splice(index, 1)}} catch (err) {errorMessage.value = err instanceof Error ? err.message : 'Delete failed'}}// 上传媒体const uploadMedia = async (file: File) => {const formData = new FormData()formData.append('file', file)try {const response = await fetch('/api/media/upload', {method: 'POST',body: formData})if (!response.ok) throw new Error('Upload failed')const newItem = await response.json()items.value.unshift({...newItem,uploadedAt: new Date(newItem.uploadedAt)})return newItem} catch (err) {errorMessage.value = err instanceof Error ? err.message : 'Upload failed'throw err}}// 格式化文件大小const formatFileSize = (bytes: number): string => {if (bytes === 0) return '0 Bytes'const k = 1024const sizes = ['Bytes', 'KB', 'MB', 'GB']const i = Math.floor(Math.log(bytes) / Math.log(k))return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]}return {mediaItems: items,loading: isLoading,error: errorMessage,fetchMedia,deleteMedia,uploadMedia,formatFileSize}
})// 组件状态
const selectedItems = ref<string[]>([])
const showUploadDialog = ref(false)// 计算属性
const mediaItems = computed(() => items.value)// 方法
const toggleSelection = (id: string) => {const index = selectedItems.value.indexOf(id)if (index > -1) {selectedItems.value.splice(index, 1)} else if (selectedItems.value.length < (props.maxSelection || Infinity)) {selectedItems.value.push(id)}
}const handleUploaded = (item: MediaItem) => {showUploadDialog.value = falseemit('upload', item)
}// 监听选择变化
watch(selectedItems, (newSelection) => {const selectedMedia = mediaItems.value.filter(item => newSelection.includes(item.id))emit('select', selectedMedia)
})// 生命周期
onMounted(() => {fetchMedia()
})onUnmounted(() => {scope.stop()
})
</script><style scoped>
.media-manager {@apply p-4;
}.media-header {@apply flex justify-between items-center mb-4;
}.media-grid {@apply grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4;
}.media-item {@apply border rounded-lg p-2 cursor-pointer transition-all;
}.media-item.selected {@apply ring-2 ring-blue-500;
}.media-item img {@apply w-full h-32 object-cover rounded;
}.media-info {@apply mt-2;
}.media-title {@apply font-medium text-sm truncate;
}.media-size {@apply text-xs text-gray-500;
}.delete-btn {@apply mt-2 w-full bg-red-500 text-white py-1 px-2 rounded text-sm;\}.delete-btn:hover {@apply bg-red-600;
}.empty-state {@apply text-center py-8 text-gray-500;
}
</style>
4.3 踩坑经验与解决方案
1. React Server Components 中的常见问题
问题:在服务端组件中使用浏览器 API
// ❌ 错误:在服务端组件中使用 window
export default async function ServerComponent() {const width = window.innerWidth // 错误!服务端没有 windowreturn <div>Window width: {width}</div>
}// ✅ 正确:将客户端逻辑提取到客户端组件
'use client'export default function ClientComponent() {const [width, setWidth] = useState(window.innerWidth)useEffect(() => {const handleResize = () => setWidth(window.innerWidth)window.addEventListener('resize', handleResize)return () => window.removeEventListener('resize', handleResize)}, [])return <div>Window width: {width}</div>
}
问题:服务端组件中的状态管理
// ❌ 错误:在服务端组件中使用 useState
export default async function ServerComponent() {const [count, setCount] = useState(0) // 错误!服务端组件不能使用 hooksreturn <button onClick={() => setCount(count + 1)}>{count}</button>
}// ✅ 正确:使用服务端组件获取数据,客户端组件处理交互
// 服务端组件
export default async function ServerComponent() {const data = await getData()return <InteractiveComponent initialData={data} />
}// 客户端组件
'use client'export default function InteractiveComponent({ initialData }) {const [count, setCount] = useState(0)return (<div><p>Data: {initialData}</p><button onClick={() => setCount(count + 1)}>{count}</button></div>)
}
2. Vue Effect 作用域的常见问题
问题:Effect 作用域的内存泄漏
// ❌ 错误:忘记清理 effect 作用域
function useAsyncData() {const scope = effectScope()const data = ref(null)scope.run(() => {watchEffect(async () => {const response = await fetch('/api/data')data.value = await response.json()})})return { data } // 没有返回 stop 方法,可能导致内存泄漏
}// ✅ 正确:提供清理机制
function useAsyncData() {const scope = effectScope()const data = ref(null)scope.run(() => {watchEffect(async () => {const response = await fetch('/api/data')data.value = await response.json()})})// 提供清理函数onScopeDispose(() => {scope.stop()})return { data, stop: () => scope.stop() }
}
问题:嵌套作用域的管理
// ❌ 错误:嵌套作用域管理不当
function useNestedScopes() {const parentScope = effectScope()parentScope.run(() => {const parentData = ref('parent')const childScope = effectScope()childScope.run(() => {const childData = ref('child')watchEffect(() => {console.log(parentData.value, childData.value)})})// 忘记停止子作用域})return { stop: () => parentScope.stop() } // 子作用域可能没有被正确清理
}// ✅ 正确:使用 onScopeDispose 管理嵌套作用域
function useNestedScopes() {const parentScope = effectScope()parentScope.run(() => {const parentData = ref('parent')const childScope = effectScope(true) // 分离的作用域childScope.run(() => {const childData = ref('child')watchEffect(() => {console.log(parentData.value, childData.value)})// 注册清理函数onScopeDispose(() => {childScope.stop()})})// 父作用域清理时也清理子作用域onScopeDispose(() => {childScope.stop()})})return { stop: () => parentScope.stop() }
}
3. 性能优化最佳实践
React Server Components 性能优化
// 使用缓存优化数据获取
import { unstable_cache } from 'next/cache'const getCachedPost = unstable_cache(async (slug: string) => {return await getPostBySlug(slug)},['post-cache'],{revalidate: 3600, // 1小时缓存tags: ['posts']}
)// 在组件中使用缓存数据
export default async function BlogPost({ params }: { params: { slug: string } }) {const post = await getCachedPost(params.slug) // 使用缓存版本return (<article><h1>{post.title}</h1><div>{post.content}</div></article>)
}
Vue Effect 作用域性能优化
// 批量更新优化
function useOptimizedList() {const scope = effectScope()return scope.run(() => {const items = ref([])const selectedItems = ref(new Set())const filter = ref('')// 使用 computed 缓存计算结果const filteredItems = computed(() => {if (!filter.value) return items.valuereturn items.value.filter(item => item.name.toLowerCase().includes(filter.value.toLowerCase()))})// 批量选择优化const selectAll = () => {// 避免逐个触发更新const newSelection = new Set(filteredItems.value.map(item => item.id))selectedItems.value = newSelection}// 防抖搜索const debouncedFilter = ref('')let filterTimeout: NodeJS.Timeout | null = nullwatch(filter, (newFilter) => {if (filterTimeout) clearTimeout(filterTimeout)filterTimeout = setTimeout(() => {debouncedFilter.value = newFilter}, 300)})return {items,selectedItems,filter,filteredItems,selectAll,stop: () => scope.stop()}})
}
五、总结与展望
Vue 3.4 的 Effect 作用域 API 和 React Server Components 代表了现代前端框架发展的两个重要方向:更精细的副作用管理和更智能的服务端渲染。
技术对比总结
| 方面 | Vue Effect 作用域 | React Server Components |
|---|---|---|
| 核心优势 | 精确的副作用生命周期管理 | 零 JS Bundle 的服务端组件 |
| 适用场景 | 复杂状态管理、异步操作 | 内容密集型应用、SEO 优化 |
| 学习曲线 | 相对较低,概念清晰 | 较高,需要理解新的心智模型 |
| 生态系统 | Vue 生态,工具链成熟 | Next.js 生态,快速发展中 |
实际应用建议
-
Vue Effect 作用域:适用于需要精确控制响应式副作用生命周期的场景,特别是大型表单、复杂状态管理和异步操作协调。
-
React Server Components:适用于内容为主的网站、博客、电商等需要良好 SEO 和快速首屏加载的应用。
-
混合使用:在大型项目中,可以根据不同模块的特点选择合适的技术,甚至可以在同一个项目中结合使用两种技术。
未来发展
随着前端技术的不断演进,我们可以期待:
- 更智能的编译时优化:框架会在编译时做更多优化,减少运行时开销
- 更好的开发体验:调试工具和开发体验会不断改进
- 更丰富的生态系统:相关工具和库会越来越完善
作为前端开发者,保持对新技术的学习和实践,理解其背后的设计思想和适用场景,才能在技术选型时做出更明智的决策。
欢迎在评论区分享你在使用 Vue Effect 作用域和 React Server Components 时的经验和心得!
