当前位置: 首页 > news >正文

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(服务端渲染)有本质区别。

客户端请求
服务器
Server Components渲染
Client Components代码
序列化数据
JS Bundle
客户端
水合hydration
完整应用

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 作用域适用场景
  1. 复杂组件状态管理

    // 大型表单组件
    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()}})
    }
    
  2. 异步操作管理

    // 多个相关的异步操作
    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 适用场景
  1. 内容密集型应用

    // 博客系统
    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>)
    }
    
  2. 需要访问数据库或内部 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 生态,快速发展中

实际应用建议

  1. Vue Effect 作用域:适用于需要精确控制响应式副作用生命周期的场景,特别是大型表单、复杂状态管理和异步操作协调。

  2. React Server Components:适用于内容为主的网站、博客、电商等需要良好 SEO 和快速首屏加载的应用。

  3. 混合使用:在大型项目中,可以根据不同模块的特点选择合适的技术,甚至可以在同一个项目中结合使用两种技术。

未来发展

随着前端技术的不断演进,我们可以期待:

  • 更智能的编译时优化:框架会在编译时做更多优化,减少运行时开销
  • 更好的开发体验:调试工具和开发体验会不断改进
  • 更丰富的生态系统:相关工具和库会越来越完善

作为前端开发者,保持对新技术的学习和实践,理解其背后的设计思想和适用场景,才能在技术选型时做出更明智的决策。


欢迎在评论区分享你在使用 Vue Effect 作用域和 React Server Components 时的经验和心得!

http://www.dtcms.com/a/580932.html

相关文章:

  • 基于改进TransUNet的港口船只图像分割系统研究
  • LeetCode 4. 寻找两个正序数组的中位数(困难)
  • 宇宙的几何诗篇:当空间本身成为运动的主角
  • Javascript函数之函数的基本使用以及封装?
  • 力扣 寻找两个正序数组的中位数
  • 文库类网站建设建议及经验上海高风险区域最新
  • 建设工程自学网站网站建设及管理使用情况汇报
  • Java 多线程同步机制深度解析:从 synchronized 到 Lock
  • AR眼镜在核电操作智能监护应用技术方案|阿法龙XR云平台
  • Rust 练习册 :Nth Prime与素数算法
  • 杭州网站建设机构win7做网站服务器卡
  • 算法基础篇:(三)基础算法之枚举:暴力美学的艺术,从穷举到高效优化
  • 【大模型学习3】预训练语言模型详解
  • 《Linux系统编程之开发工具》【实战:倒计时 + 进度条】
  • 【Frida Android】实战篇1:环境准备
  • 【2025 CVPR】EmoEdit: Evoking Emotions through Image Manipulation
  • 如何创建网站内容网站名称不能涉及
  • 编写微服务api
  • Flutter Transform.rotate 与动画控制器 实现旋转动画
  • Flutter进行命令打包各版本程序(2025.11)
  • 【基于 WangEditor v5 + Vue2 封装 CSDN 风格富文本组件】
  • 网站建设的重要性意义徐州建站公司模板
  • Scrapy源码剖析:下载器中间件是如何工作的?
  • vi 编辑器命令大全
  • AI 预测 + 物联网融合:档案馆温湿度监控系统发展新趋势
  • Vue JSON结构编辑器组件设计与实现解析
  • 14_FastMCP 2.x 中文文档之FastMCP高级功能:MCP中间件详解
  • 软考中级软件设计师(下午题)--- UML建模
  • 机械臂时间最优规划
  • 【LeetCode刷题】两数之和