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 的响应式系统。
javascript
// 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 组织在一起,并在需要时批量停止或清理它们。
javascript
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 实际应用场景
场景一:组件生命周期管理
javascript
// MyComponent.vue
import { effectScope, onMounted, onUnmounted } from 'vue'
export default {
setup() {
let scope
onMounted(() => {
// 创建 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(() => {
// 组件卸载时停止所有 effects
scope?.stop()
})
}
}
场景二:异步操作管理
javascript
// 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 = null
const execute = async (params) => {
// 取消之前的请求
abortController?.abort()
abortController = new AbortController()
loading.value = true
error.value = null
try {
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 最佳实践与性能优化
javascript
// 最佳实践: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] * 2
return acc
}, {})
})
// 方法
const fetchData = async () => {
loading.value = true
try {
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 = false
const scheduleUpdate = (fn) => {
pendingUpdates.add(fn)
if (!updateScheduled) {
updateScheduled = true
nextTick(() => {
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+ 作为示例。
bash
# 创建 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 基础
服务端组件(默认)
typescript
// app/components/ServerComponent.tsx
import { Suspense } from 'react'
// 这是一个服务端组件
async function getData() {
// 直接访问数据库或外部 API
const 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>
)
}
客户端组件
typescript
// 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>
)
}
共享组件
typescript
// 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 数据获取策略
服务端数据获取
typescript
// app/lib/data.ts
export interface User {
id: number
name: string
email: string
posts: Post[]
}
export interface Post {
id: number
title: string
content: string
authorId: 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)
}
服务端组件中使用数据
typescript
// 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>
)
}
流式渲染
typescript
// 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 客户端与服务端交互
服务端组件传递数据给客户端组件
typescript
// app/components/InteractiveChart.tsx
'use client'
import { useState, useEffect } from 'react'
import { Line } from 'react-chartjs-2'
interface ChartData {
labels: string[]
datasets: {
label: string
data: number[]
borderColor: string
backgroundColor: string
}[]
}
export default function InteractiveChart({
initialData,
title
}: {
initialData: ChartData
title: 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">
<button
onClick={() => handleFilterChange('all')}
className={`px-4 py-2 mr-2 rounded ${
filter === 'all' ? 'bg-blue-500 text-white' : 'bg-gray-200'
}`}
>
All Data
</button>
<button
onClick={() => 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>
)
}
服务端组件使用客户端组件
typescript
// 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. 组件级缓存
typescript
// 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 优化
typescript
// 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. 选择性水合
typescript
// 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>
<input
type="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 作用域适用场景
-
复杂组件状态管理
javascript// 大型表单组件 function useComplexForm() { const scope = effectScope() return scope.run(() => { const formData = reactive({}) const validationErrors = ref({}) const isSubmitting = ref(false) // 多个相关的 watchers watch(() => formData.email, validateEmail) watch(() => formData.password, validatePassword) watchEffect(() => { // 自动保存逻辑 autoSave(formData) }) return { formData, validationErrors, isSubmitting, submit: () => { /* 提交逻辑 */ }, stop: () => scope.stop() } }) } -
异步操作管理
javascript// 多个相关的异步操作 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 适用场景
-
内容密集型应用
typescript// 博客系统 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 的应用
typescript// 管理后台 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 混合使用策略
在实际项目中,我们可以结合使用两种技术:
typescript
// 结合 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 核心功能实现
服务端组件:博客文章页面
typescript
// 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:文章编辑器
typescript
// vue-components/composables/usePostEditor.ts
import { effectScope, ref, reactive, watch, computed } from 'vue'
export interface PostEditorState {
title: string
content: string
excerpt: string
tags: string[]
coverImage: string | null
published: 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 = 200
return 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 = null
const 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) return
isSaving.value = true
try {
// 调用 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 = true
try {
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 = true
try {
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
<!-- 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: string
url: string
title: string
alt: string
size: number
type: string
uploadedAt: Date
}
// Props
const props = defineProps<{
maxSelection?: number
accept?: 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 = true
errorMessage.value = null
try {
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 = 1024
const 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 = false
emit('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
typescript
// ❌ 错误:在服务端组件中使用 window
export default async function ServerComponent() {
const width = window.innerWidth // 错误!服务端没有 window
return <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>
}
问题:服务端组件中的状态管理
typescript
// ❌ 错误:在服务端组件中使用 useState
export default async function ServerComponent() {
const [count, setCount] = useState(0) // 错误!服务端组件不能使用 hooks
return <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 作用域的内存泄漏
javascript
// ❌ 错误:忘记清理 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() }
}
问题:嵌套作用域的管理
javascript
// ❌ 错误:嵌套作用域管理不当
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 性能优化
typescript
// 使用缓存优化数据获取
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 作用域性能优化
javascript
// 批量更新优化
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.value
return 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 = null
watch(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 时的经验和心得!