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 核心要点
- 状态管理选择:Vuex适合复杂应用,Pinia更现代化
- 服务端同步:状态序列化、清理敏感数据、客户端恢复
- 数据预取:组件级、路由级、智能预取策略
- 状态持久化:本地存储、会话存储、安全性考虑
- 实时更新:WebSocket、SSE、断线重连机制
5.7.2 最佳实践
- 合理设计状态结构,避免过度嵌套
- 实现完善的错误处理和重试机制
- 使用缓存策略优化数据获取性能
- 保护敏感数据,避免泄露到客户端
- 建立实时数据更新机制提升用户体验
5.7.3 下章预告
下一章我们将学习性能优化技巧: - 代码分割与懒加载 - 缓存策略 - 服务端渲染优化 - 客户端性能优化 - 监控与分析
练习作业:
- 使用Pinia重构一个现有的Vuex应用
- 实现一个智能的数据预取系统
- 配置状态持久化,处理敏感数据
- 集成WebSocket实现实时数据更新
- 建立完善的错误处理和重试机制
下一章: 性能优化技巧