5.1 Vuex在SSR中的应用

5.1.1 Vuex Store配置

// src/store/index.js
import { createStore } from 'vuex'
import { auth } from './modules/auth'
import { blog } from './modules/blog'
import { user } from './modules/user'
import { ui } from './modules/ui'

// 创建store工厂函数
export function createStore() {
  return createStore({
    modules: {
      auth,
      blog,
      user,
      ui
    },
    
    // 严格模式在开发环境启用
    strict: process.env.NODE_ENV !== 'production',
    
    // 插件配置
    plugins: process.env.NODE_ENV === 'development' ? [createLogger()] : []
  })
}

// 开发环境日志插件
function createLogger() {
  return store => {
    store.subscribe((mutation, state) => {
      console.log('Mutation:', mutation.type)
      console.log('Payload:', mutation.payload)
      console.log('State:', state)
    })
  }
}

5.1.2 模块化状态管理

// src/store/modules/auth.js
export const auth = {
  namespaced: true,
  
  state: () => ({
    user: null,
    token: null,
    isAuthenticated: false,
    permissions: [],
    loginLoading: false,
    loginError: null
  }),
  
  mutations: {
    SET_USER(state, user) {
      state.user = user
      state.isAuthenticated = !!user
    },
    
    SET_TOKEN(state, token) {
      state.token = token
    },
    
    SET_PERMISSIONS(state, permissions) {
      state.permissions = permissions
    },
    
    SET_LOGIN_LOADING(state, loading) {
      state.loginLoading = loading
    },
    
    SET_LOGIN_ERROR(state, error) {
      state.loginError = error
    },
    
    CLEAR_AUTH(state) {
      state.user = null
      state.token = null
      state.isAuthenticated = false
      state.permissions = []
      state.loginError = null
    }
  },
  
  actions: {
    // 登录
    async login({ commit }, credentials) {
      commit('SET_LOGIN_LOADING', true)
      commit('SET_LOGIN_ERROR', null)
      
      try {
        const response = await fetch('/api/auth/login', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(credentials)
        })
        
        if (!response.ok) {
          throw new Error('登录失败')
        }
        
        const data = await response.json()
        
        commit('SET_USER', data.user)
        commit('SET_TOKEN', data.token)
        commit('SET_PERMISSIONS', data.permissions)
        
        // 存储到localStorage(仅客户端)
        if (typeof localStorage !== 'undefined') {
          localStorage.setItem('auth_token', data.token)
        }
        
        return data
      } catch (error) {
        commit('SET_LOGIN_ERROR', error.message)
        throw error
      } finally {
        commit('SET_LOGIN_LOADING', false)
      }
    },
    
    // 登出
    async logout({ commit }) {
      try {
        await fetch('/api/auth/logout', {
          method: 'POST'
        })
      } catch (error) {
        console.error('Logout error:', error)
      } finally {
        commit('CLEAR_AUTH')
        
        // 清除localStorage(仅客户端)
        if (typeof localStorage !== 'undefined') {
          localStorage.removeItem('auth_token')
        }
      }
    },
    
    // 验证token
    async validateToken({ commit }, token) {
      try {
        const response = await fetch('/api/auth/validate', {
          headers: {
            'Authorization': `Bearer ${token}`
          }
        })
        
        if (response.ok) {
          const data = await response.json()
          commit('SET_USER', data.user)
          commit('SET_TOKEN', token)
          commit('SET_PERMISSIONS', data.permissions)
          return true
        }
      } catch (error) {
        console.error('Token validation error:', error)
      }
      
      commit('CLEAR_AUTH')
      return false
    },
    
    // 初始化认证状态(客户端)
    async initAuth({ dispatch }) {
      if (typeof localStorage !== 'undefined') {
        const token = localStorage.getItem('auth_token')
        if (token) {
          await dispatch('validateToken', token)
        }
      }
    }
  },
  
  getters: {
    isLoggedIn: state => state.isAuthenticated,
    currentUser: state => state.user,
    userRole: state => state.user?.role || 'guest',
    hasPermission: state => permission => state.permissions.includes(permission),
    isAdmin: state => state.user?.role === 'admin'
  }
}
// src/store/modules/blog.js
export const blog = {
  namespaced: true,
  
  state: () => ({
    posts: [],
    currentPost: null,
    categories: [],
    tags: [],
    loading: false,
    error: null,
    pagination: {
      page: 1,
      limit: 10,
      total: 0,
      totalPages: 0
    }
  }),
  
  mutations: {
    SET_POSTS(state, posts) {
      state.posts = posts
    },
    
    SET_CURRENT_POST(state, post) {
      state.currentPost = post
    },
    
    SET_CATEGORIES(state, categories) {
      state.categories = categories
    },
    
    SET_TAGS(state, tags) {
      state.tags = tags
    },
    
    SET_LOADING(state, loading) {
      state.loading = loading
    },
    
    SET_ERROR(state, error) {
      state.error = error
    },
    
    SET_PAGINATION(state, pagination) {
      state.pagination = { ...state.pagination, ...pagination }
    },
    
    ADD_POST(state, post) {
      state.posts.unshift(post)
    },
    
    UPDATE_POST(state, updatedPost) {
      const index = state.posts.findIndex(post => post.id === updatedPost.id)
      if (index !== -1) {
        state.posts.splice(index, 1, updatedPost)
      }
      
      if (state.currentPost && state.currentPost.id === updatedPost.id) {
        state.currentPost = updatedPost
      }
    },
    
    DELETE_POST(state, postId) {
      state.posts = state.posts.filter(post => post.id !== postId)
      
      if (state.currentPost && state.currentPost.id === postId) {
        state.currentPost = null
      }
    }
  },
  
  actions: {
    // 获取文章列表
    async fetchPosts({ commit }, params = {}) {
      commit('SET_LOADING', true)
      commit('SET_ERROR', null)
      
      try {
        const queryString = new URLSearchParams(params).toString()
        const response = await fetch(`/api/posts?${queryString}`)
        
        if (!response.ok) {
          throw new Error('Failed to fetch posts')
        }
        
        const data = await response.json()
        
        commit('SET_POSTS', data.posts)
        commit('SET_PAGINATION', {
          page: data.page,
          limit: data.limit,
          total: data.total,
          totalPages: data.totalPages
        })
        
        return data
      } catch (error) {
        commit('SET_ERROR', error.message)
        throw error
      } finally {
        commit('SET_LOADING', false)
      }
    },
    
    // 获取单篇文章
    async fetchPost({ commit }, postId) {
      commit('SET_LOADING', true)
      commit('SET_ERROR', null)
      
      try {
        const response = await fetch(`/api/posts/${postId}`)
        
        if (!response.ok) {
          if (response.status === 404) {
            throw new Error('Post not found')
          }
          throw new Error('Failed to fetch post')
        }
        
        const post = await response.json()
        commit('SET_CURRENT_POST', post)
        
        return post
      } catch (error) {
        commit('SET_ERROR', error.message)
        throw error
      } finally {
        commit('SET_LOADING', false)
      }
    },
    
    // 获取分类列表
    async fetchCategories({ commit }) {
      try {
        const response = await fetch('/api/categories')
        const categories = await response.json()
        commit('SET_CATEGORIES', categories)
        return categories
      } catch (error) {
        console.error('Failed to fetch categories:', error)
      }
    },
    
    // 获取标签列表
    async fetchTags({ commit }) {
      try {
        const response = await fetch('/api/tags')
        const tags = await response.json()
        commit('SET_TAGS', tags)
        return tags
      } catch (error) {
        console.error('Failed to fetch tags:', error)
      }
    }
  },
  
  getters: {
    publishedPosts: state => state.posts.filter(post => post.published),
    postsByCategory: state => categoryId => 
      state.posts.filter(post => post.categoryId === categoryId),
    postsByTag: state => tagId => 
      state.posts.filter(post => post.tags.includes(tagId)),
    featuredPosts: state => state.posts.filter(post => post.featured),
    recentPosts: state => state.posts.slice(0, 5)
  }
}

5.2 Pinia状态管理

5.2.1 Pinia Store配置

// src/stores/index.js
import { createPinia } from 'pinia'

export function createStore() {
  const pinia = createPinia()
  
  // 添加插件
  if (process.env.NODE_ENV === 'development') {
    pinia.use(({ store }) => {
      store.$subscribe((mutation, state) => {
        console.log('Store mutation:', mutation)
        console.log('New state:', state)
      })
    })
  }
  
  return pinia
}

5.2.2 Pinia Store定义

// src/stores/auth.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useAuthStore = defineStore('auth', () => {
  // State
  const user = ref(null)
  const token = ref(null)
  const permissions = ref([])
  const loading = ref(false)
  const error = ref(null)
  
  // Getters
  const isAuthenticated = computed(() => !!user.value)
  const isAdmin = computed(() => user.value?.role === 'admin')
  const hasPermission = computed(() => (permission) => {
    return permissions.value.includes(permission)
  })
  
  // Actions
  async function login(credentials) {
    loading.value = true
    error.value = null
    
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(credentials)
      })
      
      if (!response.ok) {
        throw new Error('登录失败')
      }
      
      const data = await response.json()
      
      user.value = data.user
      token.value = data.token
      permissions.value = data.permissions
      
      // 存储到localStorage(仅客户端)
      if (typeof localStorage !== 'undefined') {
        localStorage.setItem('auth_token', data.token)
      }
      
      return data
    } catch (err) {
      error.value = err.message
      throw err
    } finally {
      loading.value = false
    }
  }
  
  async function logout() {
    try {
      await fetch('/api/auth/logout', {
        method: 'POST'
      })
    } catch (err) {
      console.error('Logout error:', err)
    } finally {
      user.value = null
      token.value = null
      permissions.value = []
      
      if (typeof localStorage !== 'undefined') {
        localStorage.removeItem('auth_token')
      }
    }
  }
  
  async function validateToken(tokenValue) {
    try {
      const response = await fetch('/api/auth/validate', {
        headers: {
          'Authorization': `Bearer ${tokenValue}`
        }
      })
      
      if (response.ok) {
        const data = await response.json()
        user.value = data.user
        token.value = tokenValue
        permissions.value = data.permissions
        return true
      }
    } catch (err) {
      console.error('Token validation error:', err)
    }
    
    user.value = null
    token.value = null
    permissions.value = []
    return false
  }
  
  async function initAuth() {
    if (typeof localStorage !== 'undefined') {
      const storedToken = localStorage.getItem('auth_token')
      if (storedToken) {
        await validateToken(storedToken)
      }
    }
  }
  
  return {
    // State
    user,
    token,
    permissions,
    loading,
    error,
    
    // Getters
    isAuthenticated,
    isAdmin,
    hasPermission,
    
    // Actions
    login,
    logout,
    validateToken,
    initAuth
  }
})
// src/stores/blog.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useBlogStore = defineStore('blog', () => {
  // State
  const posts = ref([])
  const currentPost = ref(null)
  const categories = ref([])
  const tags = ref([])
  const loading = ref(false)
  const error = ref(null)
  const pagination = ref({
    page: 1,
    limit: 10,
    total: 0,
    totalPages: 0
  })
  
  // Getters
  const publishedPosts = computed(() => 
    posts.value.filter(post => post.published)
  )
  
  const featuredPosts = computed(() => 
    posts.value.filter(post => post.featured)
  )
  
  const recentPosts = computed(() => 
    posts.value.slice(0, 5)
  )
  
  const postsByCategory = computed(() => (categoryId) => 
    posts.value.filter(post => post.categoryId === categoryId)
  )
  
  // Actions
  async function fetchPosts(params = {}) {
    loading.value = true
    error.value = null
    
    try {
      const queryString = new URLSearchParams(params).toString()
      const response = await fetch(`/api/posts?${queryString}`)
      
      if (!response.ok) {
        throw new Error('Failed to fetch posts')
      }
      
      const data = await response.json()
      
      posts.value = data.posts
      pagination.value = {
        page: data.page,
        limit: data.limit,
        total: data.total,
        totalPages: data.totalPages
      }
      
      return data
    } catch (err) {
      error.value = err.message
      throw err
    } finally {
      loading.value = false
    }
  }
  
  async function fetchPost(postId) {
    loading.value = true
    error.value = null
    
    try {
      const response = await fetch(`/api/posts/${postId}`)
      
      if (!response.ok) {
        if (response.status === 404) {
          throw new Error('Post not found')
        }
        throw new Error('Failed to fetch post')
      }
      
      const post = await response.json()
      currentPost.value = post
      
      return post
    } catch (err) {
      error.value = err.message
      throw err
    } finally {
      loading.value = false
    }
  }
  
  async function fetchCategories() {
    try {
      const response = await fetch('/api/categories')
      const data = await response.json()
      categories.value = data
      return data
    } catch (err) {
      console.error('Failed to fetch categories:', err)
    }
  }
  
  function addPost(post) {
    posts.value.unshift(post)
  }
  
  function updatePost(updatedPost) {
    const index = posts.value.findIndex(post => post.id === updatedPost.id)
    if (index !== -1) {
      posts.value[index] = updatedPost
    }
    
    if (currentPost.value && currentPost.value.id === updatedPost.id) {
      currentPost.value = updatedPost
    }
  }
  
  function deletePost(postId) {
    posts.value = posts.value.filter(post => post.id !== postId)
    
    if (currentPost.value && currentPost.value.id === postId) {
      currentPost.value = null
    }
  }
  
  return {
    // State
    posts,
    currentPost,
    categories,
    tags,
    loading,
    error,
    pagination,
    
    // Getters
    publishedPosts,
    featuredPosts,
    recentPosts,
    postsByCategory,
    
    // Actions
    fetchPosts,
    fetchPost,
    fetchCategories,
    addPost,
    updatePost,
    deletePost
  }
})

5.3 服务端状态同步

5.3.1 状态序列化

// src/utils/stateSync.js
import serialize from 'serialize-javascript'

// 序列化状态
export function serializeState(state) {
  return serialize(state, {
    isJSON: true,
    space: 0
  })
}

// 反序列化状态
export function deserializeState(serializedState) {
  try {
    return JSON.parse(serializedState)
  } catch (error) {
    console.error('Failed to deserialize state:', error)
    return {}
  }
}

// 状态注入HTML
export function injectStateToHTML(html, state) {
  const serializedState = serializeState(state)
  const stateScript = `
    <script>
      window.__INITIAL_STATE__ = ${serializedState};
    </script>
  `
  
  // 在</body>标签前插入状态脚本
  return html.replace('</body>', `${stateScript}</body>`)
}

// 清理敏感数据
export function sanitizeState(state) {
  const sanitized = JSON.parse(JSON.stringify(state))
  
  // 移除敏感信息
  if (sanitized.auth) {
    delete sanitized.auth.token
    if (sanitized.auth.user) {
      delete sanitized.auth.user.password
      delete sanitized.auth.user.email
    }
  }
  
  return sanitized
}

5.3.2 服务端渲染状态管理

// src/entry-server.js
import { createApp } from './app.js'
import { sanitizeState } from './utils/stateSync.js'

export default async function(context) {
  const { app, router, store } = createApp()
  
  // 设置路由
  await router.push(context.url)
  await router.isReady()
  
  // 获取匹配的组件
  const matchedComponents = router.currentRoute.value.matched
    .flatMap(record => Object.values(record.components || {}))
  
  // 预取数据
  await Promise.all(
    matchedComponents.map(component => {
      if (component.asyncData) {
        return component.asyncData({
          store,
          route: router.currentRoute.value,
          req: context.req,
          res: context.res
        })
      }
    })
  )
  
  // 获取状态并清理敏感数据
  const state = store.state || store.$state
  context.state = sanitizeState(state)
  
  return app
}

5.3.3 客户端状态恢复

// src/entry-client.js
import { createApp } from './app.js'

const { app, router, store } = createApp()

// 恢复服务端状态
if (window.__INITIAL_STATE__) {
  // Vuex
  if (store.replaceState) {
    store.replaceState(window.__INITIAL_STATE__)
  }
  // Pinia
  else if (store.$state) {
    Object.assign(store.$state, window.__INITIAL_STATE__)
  }
}

// 等待路由准备就绪
router.isReady().then(() => {
  // 添加客户端路由钩子
  router.beforeResolve(async (to, from, next) => {
    const matched = router.resolve(to).matched
    const prevMatched = router.resolve(from).matched
    
    // 找出新增的组件
    let diffed = false
    const activated = matched.filter((c, i) => {
      return diffed || (diffed = (prevMatched[i] !== c))
    })
    
    if (!activated.length) {
      return next()
    }
    
    // 显示加载状态
    showLoadingIndicator()
    
    try {
      // 预取数据
      await Promise.all(
        activated
          .flatMap(record => Object.values(record.components || {}))
          .filter(component => component.asyncData)
          .map(component => component.asyncData({
            store,
            route: to
          }))
      )
      
      hideLoadingIndicator()
      next()
    } catch (error) {
      hideLoadingIndicator()
      next(error)
    }
  })
  
  // 挂载应用
  app.mount('#app')
})

function showLoadingIndicator() {
  // 显示加载指示器
  const indicator = document.getElementById('loading-indicator')
  if (indicator) {
    indicator.style.display = 'block'
  }
}

function hideLoadingIndicator() {
  // 隐藏加载指示器
  const indicator = document.getElementById('loading-indicator')
  if (indicator) {
    indicator.style.display = 'none'
  }
}

5.4 数据预取策略

5.4.1 组件级数据预取

<!-- src/views/BlogPost.vue -->
<template>
  <article class="blog-post" v-if="post">
    <header class="post-header">
      <h1>{{ post.title }}</h1>
      <div class="post-meta">
        <span>作者: {{ post.author.name }}</span>
        <span>发布时间: {{ formatDate(post.publishedAt) }}</span>
        <span>分类: {{ post.category.name }}</span>
      </div>
    </header>
    
    <div class="post-content" v-html="post.content"></div>
    
    <footer class="post-footer">
      <div class="tags">
        <span v-for="tag in post.tags" :key="tag.id" class="tag">
          {{ tag.name }}
        </span>
      </div>
      
      <div class="actions">
        <button @click="likePost" :disabled="liking">
          {{ post.liked ? '取消点赞' : '点赞' }} ({{ post.likesCount }})
        </button>
        <button @click="sharePost">分享</button>
      </div>
    </footer>
    
    <!-- 相关文章 -->
    <section class="related-posts" v-if="relatedPosts.length">
      <h3>相关文章</h3>
      <div class="posts-grid">
        <PostCard 
          v-for="relatedPost in relatedPosts" 
          :key="relatedPost.id" 
          :post="relatedPost"
        />
      </div>
    </section>
  </article>
  
  <div v-else-if="loading" class="loading">
    正在加载文章...
  </div>
  
  <div v-else class="error">
    文章未找到
  </div>
</template>

<script>
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { useBlogStore } from '@/stores/blog'
import PostCard from '@/components/PostCard.vue'

export default {
  name: 'BlogPost',
  components: {
    PostCard
  },
  
  // 服务端数据预取
  async asyncData({ store, route }) {
    const postId = route.params.id
    
    // 获取文章详情
    await store.dispatch('blog/fetchPost', postId)
    
    // 获取相关文章
    const post = store.state.blog.currentPost
    if (post) {
      await store.dispatch('blog/fetchRelatedPosts', {
        categoryId: post.categoryId,
        excludeId: post.id,
        limit: 4
      })
    }
  },
  
  setup() {
    const route = useRoute()
    const blogStore = useBlogStore()
    const liking = ref(false)
    
    const post = computed(() => blogStore.currentPost)
    const relatedPosts = computed(() => blogStore.relatedPosts)
    const loading = computed(() => blogStore.loading)
    
    const formatDate = (date) => {
      return new Date(date).toLocaleDateString('zh-CN')
    }
    
    const likePost = async () => {
      if (liking.value || !post.value) return
      
      liking.value = true
      try {
        await blogStore.toggleLike(post.value.id)
      } catch (error) {
        console.error('Failed to like post:', error)
      } finally {
        liking.value = false
      }
    }
    
    const sharePost = () => {
      if (navigator.share && post.value) {
        navigator.share({
          title: post.value.title,
          text: post.value.excerpt,
          url: window.location.href
        })
      } else {
        // 复制链接到剪贴板
        navigator.clipboard.writeText(window.location.href)
        alert('链接已复制到剪贴板')
      }
    }
    
    // 客户端路由切换时重新获取数据
    onMounted(async () => {
      if (!post.value || post.value.id !== route.params.id) {
        await blogStore.fetchPost(route.params.id)
      }
    })
    
    return {
      post,
      relatedPosts,
      loading,
      liking,
      formatDate,
      likePost,
      sharePost
    }
  }
}
</script>

5.4.2 路由级数据预取

// src/utils/dataFetching.js
export class DataFetcher {
  constructor(store) {
    this.store = store
    this.cache = new Map()
  }
  
  // 预取路由数据
  async prefetchRouteData(route) {
    const cacheKey = this.generateCacheKey(route)
    
    // 检查缓存
    if (this.cache.has(cacheKey)) {
      const cached = this.cache.get(cacheKey)
      if (Date.now() - cached.timestamp < 5 * 60 * 1000) { // 5分钟缓存
        return cached.data
      }
    }
    
    // 获取匹配的组件
    const matchedComponents = route.matched
      .flatMap(record => Object.values(record.components || {}))
    
    // 执行数据预取
    const promises = matchedComponents
      .filter(component => component.asyncData)
      .map(component => component.asyncData({
        store: this.store,
        route
      }))
    
    const results = await Promise.allSettled(promises)
    
    // 处理错误
    const errors = results
      .filter(result => result.status === 'rejected')
      .map(result => result.reason)
    
    if (errors.length > 0) {
      console.error('Data fetching errors:', errors)
    }
    
    // 缓存结果
    const data = results
      .filter(result => result.status === 'fulfilled')
      .map(result => result.value)
    
    this.cache.set(cacheKey, {
      data,
      timestamp: Date.now()
    })
    
    return data
  }
  
  // 生成缓存键
  generateCacheKey(route) {
    return `${route.path}?${JSON.stringify(route.query)}`
  }
  
  // 清除缓存
  clearCache() {
    this.cache.clear()
  }
  
  // 清除过期缓存
  clearExpiredCache() {
    const now = Date.now()
    for (const [key, value] of this.cache.entries()) {
      if (now - value.timestamp > 5 * 60 * 1000) {
        this.cache.delete(key)
      }
    }
  }
}

// 全局数据预取中间件
export function setupDataFetching(router, store) {
  const dataFetcher = new DataFetcher(store)
  
  router.beforeResolve(async (to, from, next) => {
    try {
      await dataFetcher.prefetchRouteData(to)
      next()
    } catch (error) {
      console.error('Failed to prefetch route data:', error)
      next(error)
    }
  })
  
  // 定期清理过期缓存
  if (typeof setInterval !== 'undefined') {
    setInterval(() => {
      dataFetcher.clearExpiredCache()
    }, 10 * 60 * 1000) // 每10分钟清理一次
  }
  
  return dataFetcher
}

5.4.3 智能数据预取

// src/utils/smartPrefetch.js
export class SmartPrefetcher {
  constructor(router, store) {
    this.router = router
    this.store = store
    this.prefetchQueue = new Set()
    this.prefetching = false
    this.setupIntersectionObserver()
  }
  
  // 设置交叉观察器
  setupIntersectionObserver() {
    if (typeof IntersectionObserver === 'undefined') return
    
    this.observer = new IntersectionObserver(
      (entries) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            const link = entry.target
            const href = link.getAttribute('href')
            if (href && href.startsWith('/')) {
              this.queuePrefetch(href)
            }
          }
        })
      },
      {
        rootMargin: '100px'
      }
    )
  }
  
  // 观察链接
  observeLinks() {
    if (!this.observer) return
    
    document.querySelectorAll('a[href^="/"]').forEach(link => {
      this.observer.observe(link)
    })
  }
  
  // 队列预取
  queuePrefetch(path) {
    if (this.prefetchQueue.has(path)) return
    
    this.prefetchQueue.add(path)
    this.processPrefetchQueue()
  }
  
  // 处理预取队列
  async processPrefetchQueue() {
    if (this.prefetching || this.prefetchQueue.size === 0) return
    
    this.prefetching = true
    
    try {
      // 限制并发数量
      const batch = Array.from(this.prefetchQueue).slice(0, 3)
      
      await Promise.all(
        batch.map(path => this.prefetchRoute(path))
      )
      
      // 从队列中移除已处理的项目
      batch.forEach(path => this.prefetchQueue.delete(path))
    } catch (error) {
      console.error('Prefetch error:', error)
    } finally {
      this.prefetching = false
      
      // 继续处理剩余队列
      if (this.prefetchQueue.size > 0) {
        setTimeout(() => this.processPrefetchQueue(), 100)
      }
    }
  }
  
  // 预取路由
  async prefetchRoute(path) {
    try {
      const route = this.router.resolve(path)
      
      // 预加载组件
      const componentPromises = route.matched
        .flatMap(record => Object.values(record.components || {}))
        .filter(component => typeof component === 'function')
        .map(component => component())
      
      await Promise.all(componentPromises)
      
      // 预取数据(可选)
      if (this.shouldPrefetchData(route)) {
        await this.prefetchRouteData(route)
      }
      
      console.log(`Prefetched route: ${path}`)
    } catch (error) {
      console.error(`Failed to prefetch route ${path}:`, error)
    }
  }
  
  // 判断是否应该预取数据
  shouldPrefetchData(route) {
    // 只为重要页面预取数据
    const importantRoutes = ['BlogPost', 'UserProfile', 'ProductDetail']
    return importantRoutes.includes(route.name)
  }
  
  // 预取路由数据
  async prefetchRouteData(route) {
    const matchedComponents = route.matched
      .flatMap(record => Object.values(record.components || {}))
    
    const promises = matchedComponents
      .filter(component => component.asyncData)
      .map(component => component.asyncData({
        store: this.store,
        route
      }))
    
    await Promise.allSettled(promises)
  }
  
  // 销毁
  destroy() {
    if (this.observer) {
      this.observer.disconnect()
    }
  }
}

// 使用智能预取
export function setupSmartPrefetch(router, store) {
  const prefetcher = new SmartPrefetcher(router, store)
  
  // 在路由切换后观察新的链接
  router.afterEach(() => {
    setTimeout(() => {
      prefetcher.observeLinks()
    }, 100)
  })
  
  return prefetcher
}

5.5 状态持久化

5.5.1 本地存储持久化

// src/utils/persistence.js
export class StatePersistence {
  constructor(key = 'app_state') {
    this.key = key
    this.storage = typeof localStorage !== 'undefined' ? localStorage : null
  }
  
  // 保存状态
  saveState(state) {
    if (!this.storage) return
    
    try {
      const serialized = JSON.stringify(this.sanitizeForStorage(state))
      this.storage.setItem(this.key, serialized)
    } catch (error) {
      console.error('Failed to save state:', error)
    }
  }
  
  // 加载状态
  loadState() {
    if (!this.storage) return null
    
    try {
      const serialized = this.storage.getItem(this.key)
      return serialized ? JSON.parse(serialized) : null
    } catch (error) {
      console.error('Failed to load state:', error)
      return null
    }
  }
  
  // 清除状态
  clearState() {
    if (!this.storage) return
    
    this.storage.removeItem(this.key)
  }
  
  // 清理存储数据(移除敏感信息)
  sanitizeForStorage(state) {
    const sanitized = JSON.parse(JSON.stringify(state))
    
    // 移除敏感数据
    if (sanitized.auth) {
      delete sanitized.auth.token
    }
    
    // 只保留必要的用户信息
    if (sanitized.user) {
      sanitized.user = {
        id: sanitized.user.id,
        name: sanitized.user.name,
        avatar: sanitized.user.avatar
      }
    }
    
    return sanitized
  }
}

// Vuex持久化插件
export function createPersistencePlugin(options = {}) {
  const persistence = new StatePersistence(options.key)
  
  return store => {
    // 初始化时加载状态
    const savedState = persistence.loadState()
    if (savedState) {
      store.replaceState({
        ...store.state,
        ...savedState
      })
    }
    
    // 监听状态变化并保存
    store.subscribe((mutation, state) => {
      // 只在特定mutation后保存
      const saveTriggers = options.saveTriggers || [
        'auth/SET_USER',
        'user/UPDATE_PROFILE',
        'settings/UPDATE_PREFERENCES'
      ]
      
      if (saveTriggers.includes(mutation.type)) {
        persistence.saveState(state)
      }
    })
  }
}

// Pinia持久化插件
export function createPiniaPersistencePlugin(options = {}) {
  const persistence = new StatePersistence(options.key)
  
  return ({ store }) => {
    // 初始化时加载状态
    const savedState = persistence.loadState()
    if (savedState && savedState[store.$id]) {
      store.$patch(savedState[store.$id])
    }
    
    // 监听状态变化并保存
    store.$subscribe((mutation, state) => {
      const currentSaved = persistence.loadState() || {}
      currentSaved[store.$id] = state
      persistence.saveState(currentSaved)
    })
  }
}

5.5.2 会话存储

// src/utils/sessionStorage.js
export class SessionStorage {
  constructor() {
    this.storage = typeof sessionStorage !== 'undefined' ? sessionStorage : null
  }
  
  // 保存会话数据
  setItem(key, value) {
    if (!this.storage) return
    
    try {
      this.storage.setItem(key, JSON.stringify(value))
    } catch (error) {
      console.error('Failed to save to session storage:', error)
    }
  }
  
  // 获取会话数据
  getItem(key) {
    if (!this.storage) return null
    
    try {
      const item = this.storage.getItem(key)
      return item ? JSON.parse(item) : null
    } catch (error) {
      console.error('Failed to load from session storage:', error)
      return null
    }
  }
  
  // 移除会话数据
  removeItem(key) {
    if (!this.storage) return
    
    this.storage.removeItem(key)
  }
  
  // 清空会话存储
  clear() {
    if (!this.storage) return
    
    this.storage.clear()
  }
}

// 会话状态管理
export class SessionStateManager {
  constructor() {
    this.sessionStorage = new SessionStorage()
  }
  
  // 保存表单数据
  saveFormData(formId, data) {
    this.sessionStorage.setItem(`form_${formId}`, {
      data,
      timestamp: Date.now()
    })
  }
  
  // 恢复表单数据
  restoreFormData(formId) {
    const saved = this.sessionStorage.getItem(`form_${formId}`)
    
    if (saved && Date.now() - saved.timestamp < 30 * 60 * 1000) { // 30分钟有效
      return saved.data
    }
    
    return null
  }
  
  // 清除表单数据
  clearFormData(formId) {
    this.sessionStorage.removeItem(`form_${formId}`)
  }
  
  // 保存滚动位置
  saveScrollPosition(path, position) {
    this.sessionStorage.setItem(`scroll_${path}`, position)
  }
  
  // 恢复滚动位置
  restoreScrollPosition(path) {
    return this.sessionStorage.getItem(`scroll_${path}`) || 0
  }
}

5.6 实时数据更新

5.6.1 WebSocket集成

// src/utils/websocket.js
export class WebSocketManager {
  constructor(url, store) {
    this.url = url
    this.store = store
    this.ws = null
    this.reconnectAttempts = 0
    this.maxReconnectAttempts = 5
    this.reconnectInterval = 1000
    this.heartbeatInterval = 30000
    this.heartbeatTimer = null
  }
  
  // 连接WebSocket
  connect() {
    if (typeof WebSocket === 'undefined') {
      console.warn('WebSocket not supported')
      return
    }
    
    try {
      this.ws = new WebSocket(this.url)
      
      this.ws.onopen = this.onOpen.bind(this)
      this.ws.onmessage = this.onMessage.bind(this)
      this.ws.onclose = this.onClose.bind(this)
      this.ws.onerror = this.onError.bind(this)
    } catch (error) {
      console.error('WebSocket connection error:', error)
    }
  }
  
  // 连接打开
  onOpen() {
    console.log('WebSocket connected')
    this.reconnectAttempts = 0
    this.startHeartbeat()
    
    // 发送认证信息
    const token = this.store.state?.auth?.token || this.store.token
    if (token) {
      this.send({
        type: 'auth',
        token
      })
    }
  }
  
  // 接收消息
  onMessage(event) {
    try {
      const message = JSON.parse(event.data)
      this.handleMessage(message)
    } catch (error) {
      console.error('Failed to parse WebSocket message:', error)
    }
  }
  
  // 处理消息
  handleMessage(message) {
    switch (message.type) {
      case 'post_updated':
        this.store.commit('blog/UPDATE_POST', message.data)
        break
        
      case 'new_comment':
        this.store.commit('blog/ADD_COMMENT', message.data)
        break
        
      case 'user_online':
        this.store.commit('user/SET_ONLINE_STATUS', {
          userId: message.userId,
          online: true
        })
        break
        
      case 'notification':
        this.store.commit('notifications/ADD_NOTIFICATION', message.data)
        break
        
      case 'heartbeat':
        // 心跳响应
        break
        
      default:
        console.log('Unknown message type:', message.type)
    }
  }
  
  // 连接关闭
  onClose() {
    console.log('WebSocket disconnected')
    this.stopHeartbeat()
    this.attemptReconnect()
  }
  
  // 连接错误
  onError(error) {
    console.error('WebSocket error:', error)
  }
  
  // 发送消息
  send(message) {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(message))
    }
  }
  
  // 开始心跳
  startHeartbeat() {
    this.heartbeatTimer = setInterval(() => {
      this.send({ type: 'heartbeat' })
    }, this.heartbeatInterval)
  }
  
  // 停止心跳
  stopHeartbeat() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer)
      this.heartbeatTimer = null
    }
  }
  
  // 尝试重连
  attemptReconnect() {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      this.reconnectAttempts++
      console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`)
      
      setTimeout(() => {
        this.connect()
      }, this.reconnectInterval * this.reconnectAttempts)
    } else {
      console.error('Max reconnection attempts reached')
    }
  }
  
  // 断开连接
  disconnect() {
    this.stopHeartbeat()
    if (this.ws) {
      this.ws.close()
      this.ws = null
    }
  }
}

// 在应用中使用WebSocket
export function setupWebSocket(store) {
  if (typeof window === 'undefined') return null
  
  const wsUrl = process.env.VUE_APP_WS_URL || 'ws://localhost:3001'
  const wsManager = new WebSocketManager(wsUrl, store)
  
  // 在用户登录后连接
  store.watch(
    state => state.auth?.isAuthenticated,
    (isAuthenticated) => {
      if (isAuthenticated) {
        wsManager.connect()
      } else {
        wsManager.disconnect()
      }
    },
    { immediate: true }
  )
  
  return wsManager
}

5.6.2 Server-Sent Events

// src/utils/sse.js
export class SSEManager {
  constructor(url, store) {
    this.url = url
    this.store = store
    this.eventSource = null
    this.reconnectAttempts = 0
    this.maxReconnectAttempts = 5
  }
  
  // 连接SSE
  connect() {
    if (typeof EventSource === 'undefined') {
      console.warn('Server-Sent Events not supported')
      return
    }
    
    try {
      this.eventSource = new EventSource(this.url)
      
      this.eventSource.onopen = this.onOpen.bind(this)
      this.eventSource.onmessage = this.onMessage.bind(this)
      this.eventSource.onerror = this.onError.bind(this)
      
      // 监听自定义事件
      this.eventSource.addEventListener('post-update', this.onPostUpdate.bind(this))
      this.eventSource.addEventListener('new-comment', this.onNewComment.bind(this))
      this.eventSource.addEventListener('notification', this.onNotification.bind(this))
    } catch (error) {
      console.error('SSE connection error:', error)
    }
  }
  
  // 连接打开
  onOpen() {
    console.log('SSE connected')
    this.reconnectAttempts = 0
  }
  
  // 接收消息
  onMessage(event) {
    try {
      const data = JSON.parse(event.data)
      console.log('SSE message:', data)
    } catch (error) {
      console.error('Failed to parse SSE message:', error)
    }
  }
  
  // 文章更新事件
  onPostUpdate(event) {
    try {
      const post = JSON.parse(event.data)
      this.store.commit('blog/UPDATE_POST', post)
    } catch (error) {
      console.error('Failed to handle post update:', error)
    }
  }
  
  // 新评论事件
  onNewComment(event) {
    try {
      const comment = JSON.parse(event.data)
      this.store.commit('blog/ADD_COMMENT', comment)
    } catch (error) {
      console.error('Failed to handle new comment:', error)
    }
  }
  
  // 通知事件
  onNotification(event) {
    try {
      const notification = JSON.parse(event.data)
      this.store.commit('notifications/ADD_NOTIFICATION', notification)
    } catch (error) {
      console.error('Failed to handle notification:', error)
    }
  }
  
  // 连接错误
  onError(error) {
    console.error('SSE error:', error)
    
    if (this.eventSource.readyState === EventSource.CLOSED) {
      this.attemptReconnect()
    }
  }
  
  // 尝试重连
  attemptReconnect() {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      this.reconnectAttempts++
      console.log(`Attempting to reconnect SSE (${this.reconnectAttempts}/${this.maxReconnectAttempts})`)
      
      setTimeout(() => {
        this.connect()
      }, 1000 * this.reconnectAttempts)
    } else {
      console.error('Max SSE reconnection attempts reached')
    }
  }
  
  // 断开连接
  disconnect() {
    if (this.eventSource) {
      this.eventSource.close()
      this.eventSource = null
    }
  }
}

// 在应用中使用SSE
export function setupSSE(store) {
  if (typeof window === 'undefined') return null
  
  const sseUrl = process.env.VUE_APP_SSE_URL || '/api/events'
  const sseManager = new SSEManager(sseUrl, store)
  
  // 在用户登录后连接
  store.watch(
    state => state.auth?.isAuthenticated,
    (isAuthenticated) => {
      if (isAuthenticated) {
        sseManager.connect()
      } else {
        sseManager.disconnect()
      }
    },
    { immediate: true }
  )
  
  return sseManager
}

5.7 本章小结

5.7.1 核心要点

  1. 状态管理选择:Vuex适合复杂应用,Pinia更现代化
  2. 服务端同步:状态序列化、清理敏感数据、客户端恢复
  3. 数据预取:组件级、路由级、智能预取策略
  4. 状态持久化:本地存储、会话存储、安全性考虑
  5. 实时更新:WebSocket、SSE、断线重连机制

5.7.2 最佳实践

  • 合理设计状态结构,避免过度嵌套
  • 实现完善的错误处理和重试机制
  • 使用缓存策略优化数据获取性能
  • 保护敏感数据,避免泄露到客户端
  • 建立实时数据更新机制提升用户体验

5.7.3 下章预告

下一章我们将学习性能优化技巧: - 代码分割与懒加载 - 缓存策略 - 服务端渲染优化 - 客户端性能优化 - 监控与分析


练习作业:

  1. 使用Pinia重构一个现有的Vuex应用
  2. 实现一个智能的数据预取系统
  3. 配置状态持久化,处理敏感数据
  4. 集成WebSocket实现实时数据更新
  5. 建立完善的错误处理和重试机制

下一章: 性能优化技巧