状态管理应用实践与进阶模式

掌握了 useState、Pinia、响应式 API 的基础之后,真正的挑战在于实际场景中如何组合运用。本章从 6 个高频场景出发,每个只给核心思路和关键代码片段,重在讲清何时用、为什么这样用

1. 表单状态管理

表单是 reactive 最合适的场景——多字段、不需要整体替换、直接 v-model 绑定无需 .value

推荐做法是封装一个通用 useForm Composable,统一管理 values / errors / dirty / submitting 四个维度的状态:

// app/composables/useForm.ts
export function useForm<T extends Record<string, any>>(initialValues: T) {
  const values = reactive({ ...initialValues }) as T
  const errors = reactive(
    Object.fromEntries(Object.keys(initialValues).map(k => [k, ''])) as Record<keyof T, string>
  )
  const isDirty = ref(false)
  const isSubmitting = ref(false)
 
  watch(() => ({ ...values }), () => { isDirty.value = true }, { deep: true })
 
  function reset() { Object.assign(values, initialValues); isDirty.value = false }
 
  async function handleSubmit(
    onSubmit: (values: T) => Promise<void>,
    onValidate?: (values: T) => Record<string, string> | null,
  ) {
    // 清空错误 → 校验 → 提交
    Object.keys(errors).forEach(k => { (errors as any)[k] = '' })
    const validationErrors = onValidate?.(values)
    if (validationErrors) {
      Object.entries(validationErrors).forEach(([k, v]) => { (errors as any)[k] = v })
      return
    }
    isSubmitting.value = true
    try { await onSubmit(values); isDirty.value = false } finally { isSubmitting.value = false }
  }
 
  return { values, errors, isDirty, isSubmitting, reset, handleSubmit }
}

使用时只需传入初始值、校验函数和提交函数,模板中 v-model="values.title" + errors.title 即可完成表单绑定。对于复杂表单(动态字段、跨步骤),建议直接用 vee-validateFormKit

2. 乐观更新模式

核心思想:不等服务端响应,先更新 UI,失败时回滚——让用户感受到即时反馈。

典型场景:点赞、收藏、关注等高频交互操作。实现套路固定为三步:

  1. 记录旧值 → 2. 立即更新 UI → 3. 请求失败时回滚到旧值
async function toggleLike() {
  const wasLiked = isLiked.value
  isLiked.value = !wasLiked                    // 立即更新
  likeCount.value += wasLiked ? -1 : 1
 
  try {
    await $fetch(`/api/videos/${videoId}/like`, { method: wasLiked ? 'DELETE' : 'POST' })
  } catch {
    isLiked.value = wasLiked                   // 回滚
    likeCount.value += wasLiked ? 1 : -1
    useNotificationStore().error('操作失败,请重试')
  }
}

如果项目中乐观更新场景多,可以抽象一个 useOptimistic(currentValue, updateFn) Composable,将"保存旧值 → 乐观写入 → try/catch 回滚"的通用逻辑封装起来。

3. 列表状态与无限滚动

列表场景的状态需求很固定:items / page / total / loading / error + hasMore / isEmpty 派生状态。推荐封装为通用 Composable:

// app/composables/useInfiniteList.ts(核心结构)
export function useInfiniteList<T>(
  fetcher: (params: { page: number }) => Promise<{ items: T[]; total: number }>,
) {
  const items = ref<T[]>([]) as Ref<T[]>
  const page = ref(1)
  const total = ref(0)
  const loading = ref(false)
 
  const hasMore = computed(() => items.value.length < total.value)
 
  async function load(reset = false) {
    if (loading.value) return
    if (reset) { page.value = 1; items.value = [] }
    loading.value = true
    try {
      const result = await fetcher({ page: page.value })
      items.value = reset ? result.items : [...items.value, ...result.items]
      total.value = result.total
    } finally { loading.value = false }
  }
 
  async function loadMore() {
    if (!hasMore.value) return
    page.value++
    await load()
  }
 
  return { items, loading, hasMore, load, loadMore }
}

触发加载的方式推荐 IntersectionObserver 哨兵元素——在列表底部放一个不可见的 &lt;div ref="sentinel"&gt;,当它进入视口时调用 loadMore()。设置 rootMargin: '200px' 可以提前 200px 触发,用户几乎感知不到加载间隙。

4. 实时状态同步

WebSocket 和 SSE 的状态同步 Composable 结构相似,核心要点:

  • SSR 安全import.meta.server 时跳过连接
  • 自动重连onclose / onerror 中延迟重试
  • 生命周期管理onMounted 连接,onUnmounted 关闭
// 通用模式(以 SSE 为例)
export function useSSEStream<T>(url: string) {
  const data = ref<T | null>(null)
  const status = ref<'connecting' | 'connected' | 'disconnected'>('connecting')
  let es: EventSource | null = null
 
  function connect() {
    if (import.meta.server) return
    es = new EventSource(url)
    es.onopen = () => { status.value = 'connected' }
    es.onmessage = (e) => { data.value = JSON.parse(e.data) }
    es.onerror = () => { status.value = 'disconnected'; es?.close(); setTimeout(connect, 5000) }
  }
 
  onMounted(connect)
  onUnmounted(() => es?.close())
  return { data, status }
}

WebSocket 版本结构类似,额外提供一个 send() 方法即可。选择依据:单向推送用 SSE(如 AI 生成进度),双向通信用 WebSocket(如实时协作编辑)。

5. 状态持久化进阶

不同数据需要不同的持久化策略,统一封装为一个 Composable:

存储方式SSR 可用容量适用场景
Cookie~4KBToken、主题、语言
localStorage~5MB搜索历史、草稿
sessionStorage~5MB当前会话临时数据
// app/composables/usePersisted.ts
export function usePersisted<T>(key: string, defaultValue: T, storage: 'cookie' | 'local' | 'session' = 'local') {
  if (storage === 'cookie') return useCookie<T>(key, { default: () => defaultValue })
 
  const data = ref<T>(defaultValue) as Ref<T>
  if (import.meta.client) {
    const store = storage === 'local' ? localStorage : sessionStorage
    const stored = store.getItem(key)
    if (stored) try { data.value = JSON.parse(stored) } catch {}
    watch(data, (val) => store.setItem(key, JSON.stringify(val)), { deep: true })
  }
  return data
}

使用时一行代码切换策略:usePersisted('theme', 'light', 'cookie') / usePersisted('history', [], 'local')

6. 状态分治与大型应用

6.1 领域驱动的 Store 拆分

当应用有 10+ 个状态模块时,按业务领域而非技术类型拆分目录:

app/stores/
├── auth/          ← 认证层:useAuthStore、usePermissionStore
├── video/         ← 视频领域:useVideoListStore、useVideoDetailStore、useVideoPlayerStore
├── studio/        ← 创作领域:useStudioStore、useUploadStore
├── ui/            ← UI 层:useNotificationStore、useModalStore
└── settings/      ← 配置层:useSettingsStore

6.2 Store 组合模式

页面级逻辑往往需要组合多个 Store 的数据。推荐用页面级 Composable 作为胶水层,避免页面组件直接引用过多 Store:

export function useVideoPage(videoId: string) {
  const videoStore = useVideoDetailStore()
  const authStore = useAuthStore()
 
  const canEdit = computed(() =>
    authStore.isLoggedIn && (videoStore.video?.authorId === authStore.user?.id || authStore.isAdmin)
  )
 
  return { video: computed(() => videoStore.video), canEdit }
}

6.3 状态重置

两种常见需求:页面离开时重置用户登出时全局重置

// 页面离开时自动重置
export function useStoreReset(...stores: Array<{ $reset?: () => void }>) {
  onUnmounted(() => stores.forEach(s => s.$reset?.()))
}
 
// 全局重置(登出时)
function resetAllStores() {
  getActivePinia()?._s.forEach(store => store.$reset?.())
}

本章小结

  • 表单状态reactive + useForm Composable 封装校验/提交/重置
  • 乐观更新:记录旧值 → 立即更新 UI → 失败回滚,提升交互体验
  • 无限滚动useInfiniteList + IntersectionObserver 哨兵元素
  • 实时同步:SSE/WebSocket Composable 统一封装,SSR 安全 + 自动重连
  • 多策略持久化usePersisted 一个 Composable 覆盖 Cookie/localStorage/sessionStorage
  • 状态分治:领域驱动拆分 Store,页面级 Composable 组合多 Store

至此,第五篇"状态管理"补充完毕。共 5 章全面覆盖了从基础到进阶的状态管理知识体系。