Vue3.4 Effect 作用域 API 与 React Server Components 实战解析

Vue3.4 Effect 作用域 API 与 React Server ***ponents 实战解析

随着前端技术的快速发展,Vue 3.4 引入的 Effect 作用域 API 和 React 的 Server ***ponents 为现代前端开发带来了新的可能性。本文将深入探讨这两项技术的原理、实战应用以及它们在不同场景下的最佳实践。

一、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, ***puted, watch, onScopeDispose } from 'vue'

// 创建一个 effect 作用域
const scope = effectScope()

// 在作用域内创建 effects
scope.run(() => {
  const doubled = ***puted(() => 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 实际应用场景

场景一:组件生命周期管理
// My***ponent.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('***ponent effects cleaned up')
        })
      })
    })
    
    onUnmounted(() => {
      // 组件卸载时停止所有 effects
      scope?.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 = 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 最佳实践与性能优化

// 最佳实践:Effect 作用域组合
import { effectScope, ***puted } from 'vue'

export function createDataScope() {
  const scope = effectScope()
  
  return scope.run(() => {
    const data = ref({})
    const loading = ref(false)
    const error = ref(null)
    
    // 计算属性
    const processedData = ***puted(() => {
      return Object.keys(data.value).reduce((a***, key) => {
        a***[key] = data.value[key] * 2
        return a***
      }, {})
    })
    
    // 方法
    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 ***ponents 实战

2.1 React Server ***ponents 简介

React Server ***ponents(RSC)是 React 18 引入的一项革命性特性,它允许组件在服务器上渲染,将渲染结果以特殊格式发送到客户端。这与传统的 SSR(服务端渲染)有本质区别。

2.2 环境搭建

首先,我们需要搭建支持 React Server ***ponents 的开发环境。这里使用 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            # 首页(服务端组件)
│   ├── ***ponents/
│   │   ├── Server***ponent.tsx   # 服务端组件
│   │   ├── Client***ponent.tsx   # 客户端组件
│   │   └── Shared***ponent.tsx   # 共享组件
│   └── lib/
│       ├── data.ts         # 数据获取函数
│       └── utils.ts        # 工具函数
├── package.json
└── tsconfig.json

2.3 Server ***ponents 基础

服务端组件(默认)
// app/***ponents/Server***ponent.tsx
import { Suspense } from 'react'

// 这是一个服务端组件
async function getData() {
  // 直接访问数据库或外部 API
  const res = await fetch('https://api.example.***/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 Server***ponent() {
  // 服务端直接获取数据
  const data = await getData()
  
  return (
    <div>
      <h2>Server ***ponent Data</h2>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  )
}
客户端组件
// app/***ponents/Client***ponent.tsx
'use client'

import { useState, useEffect } from 'react'

export default function Client***ponent() {
  const [count, setCount] = useState(0)
  const [isClient, setIsClient] = useState(false)
  
  // 确保只在客户端执行
  useEffect(() => {
    setIsClient(true)
  }, [])
  
  return (
    <div>
      <h2>Client ***ponent</h2>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
      {isClient && (
        <p>Client-side only: {window.navigator.userAgent}</p>
      )}
    </div>
  )
}
共享组件
// app/***ponents/Shared***ponent.tsx
import { Server***ponent } from './Server***ponent'
import Client***ponent from './Client***ponent'

// 这个组件可以在服务端和客户端运行
export default function Shared***ponent({ initialData }: { initialData: any }) {
  return (
    <div>
      <h1>Shared ***ponent</h1>
      <Server***ponent />
      <Client***ponent />
    </div>
  )
}

2.4 数据获取策略

服务端数据获取
// 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.***',
    posts: [
      { id: 1, title: 'First Post', content: 'Hello World', authorId: 1 },
      { id: 2, title: 'Second Post', content: 'React Server ***ponents', 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 Slow***ponent() {
  await new Promise(resolve => setTimeout(resolve, 3000))
  return <div>Loaded after 3 seconds</div>
}

// 快速组件
async function Fast***ponent() {
  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 ***ponent...</p>}>
        <Fast***ponent />
      </Suspense>
      
      {/* 慢速组件独立加载 */}
      <Suspense fallback={<p>Loading slow ***ponent...</p>}>
        <Slow***ponent />
      </Suspense>
    </div>
  )
}

2.5 客户端与服务端交互

服务端组件传递数据给客户端组件
// app/***ponents/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>
  )
}
服务端组件使用客户端组件
// app/dashboard/page.tsx
import InteractiveChart from '../***ponents/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.***/data/${key}`)
    return data.json()
  },
  ['data-cache'], // 缓存 key
  {
    revalidate: 3600, // 1小时重新验证
    tags: ['data'], // 缓存标签,用于批量失效
  }
)

// 在组件中使用
export default async function Cached***ponent({ 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 Re***mendations() {
  const re***mendations = await getRe***mendations()
  
  return (
    <div className="mt-8">
      <h2 className="text-2xl font-bold mb-4">Re***mended for You</h2>
      <div className="grid grid-cols-4 gap-4">
        {re***mendations.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 re***mendations...</div>}>
        <Re***mendations />
      </Suspense>
    </div>
  )
}
3. 选择性水合
// app/***ponents/Heavy***ponent.tsx
'use client'

import { startTransition, useDeferredValue } from 'react'

export default function Heavy***ponent({ 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 ***ponents

3.1 核心概念对比

特性 Vue Effect 作用域 React Server ***ponents
主要目的 管理响应式副作用生命周期 服务端渲染优化
运行环境 客户端 服务端 + 客户端
数据获取 客户端异步 服务端直接获取
性能优势 精确控制 effect 生命周期 减少 JS Bundle 大小
使用场景 复杂状态管理 内容为主的页面

3.2 适用场景分析

Vue Effect 作用域适用场景
  1. 复杂组件状态管理

    // 大型表单组件
    function use***plexForm() {
      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()
        }
      })
    }
    
  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 ***ponents 适用场景
  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 ***ponents)
├── ***ponents/
│   ├── server/             # 服务端组件
│   │   ├── BlogPost.tsx
│   │   ├── BlogList.tsx
│   │   └── AdminPanel.tsx
│   ├── client/             # 客户端组件
│   │   ├── ***mentSection.tsx
│   │   ├── LikeButton.tsx
│   │   └── RichEditor.tsx
│   └── shared/             # 共享组件
│       ├── Button.tsx
│       └── Card.tsx
├── lib/
│   ├── data.ts            # 数据层
│   ├── auth.ts            # 认证
│   └── utils.ts           # 工具函数
└── vue-***ponents/         # Vue 组件(用于管理后台)
    ├── admin/
    │   ├── PostEditor.vue
    │   ├── MediaManager.vue
    │   └── Analytics.vue
    └── ***posables/        # Vue ***posables
        ├── 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 '@/***ponents/server/BlogPost'
import ***mentSection from '@/***ponents/client/***mentSection'
import LikeButton from '@/***ponents/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 ***ments...</div>}>
          <***mentSection 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 ***posables:文章编辑器
// vue-***ponents/***posables/usePostEditor.ts
import { effectScope, ref, reactive, watch, ***puted } 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 = ***puted(() => {
      return state.content.split(/\s+/).filter(word => word.length > 0).length
    })
    
    const readingTime = ***puted(() => {
      const wordsPerMinute = 200
      return Math.ceil(wordCount.value / wordsPerMinute)
    })
    
    const isValid = ***puted(() => {
      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-***ponents/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, ***puted, onMounted, effectScope, watch } from 'vue'
import { useMediaUpload } from '../***posables/useMediaUpload'

interface MediaItem {
  id: string
  url: string
  title: string
  alt: string
  size: number
  type: string
  uploadedAt: Date
}

// Props
const props = defineProps<{
  maxSelection?: number
  a***ept?: 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 = ***puted(() => 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 ***ponents 中的常见问题

问题:在服务端组件中使用浏览器 API

// ❌ 错误:在服务端组件中使用 window
export default async function Server***ponent() {
  const width = window.innerWidth // 错误!服务端没有 window
  return <div>Window width: {width}</div>
}

// ✅ 正确:将客户端逻辑提取到客户端组件
'use client'

export default function Client***ponent() {
  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 Server***ponent() {
  const [count, setCount] = useState(0) // 错误!服务端组件不能使用 hooks
  return <button onClick={() => setCount(count + 1)}>{count}</button>
}

// ✅ 正确:使用服务端组件获取数据,客户端组件处理交互
// 服务端组件
export default async function Server***ponent() {
  const data = await getData()
  return <Interactive***ponent initialData={data} />
}

// 客户端组件
'use client'

export default function Interactive***ponent({ 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 ***ponents 性能优化

// 使用缓存优化数据获取
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('')
    
    // 使用 ***puted 缓存计算结果
    const filteredItems = ***puted(() => {
      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 ***ponents 代表了现代前端框架发展的两个重要方向:更精细的副作用管理和更智能的服务端渲染。

技术对比总结

方面 Vue Effect 作用域 React Server ***ponents
核心优势 精确的副作用生命周期管理 零 JS Bundle 的服务端组件
适用场景 复杂状态管理、异步操作 内容密集型应用、SEO 优化
学习曲线 相对较低,概念清晰 较高,需要理解新的心智模型
生态系统 Vue 生态,工具链成熟 Next.js 生态,快速发展中

实际应用建议

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

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

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

未来发展

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

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

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


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

转载请说明出处内容投诉
CSS教程网 » Vue3.4 Effect 作用域 API 与 React Server Components 实战解析

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买