Vuex在SSR中的配置
基础Store配置
// src/store/index.js
import { createStore } from 'vuex'
import posts from './modules/posts'
import users from './modules/users'
import auth from './modules/auth'
export function createStore() {
return createStore({
modules: {
posts,
users,
auth
},
state: {
loading: false,
error: null
},
mutations: {
SET_LOADING(state, loading) {
state.loading = loading
},
SET_ERROR(state, error) {
state.error = error
},
CLEAR_ERROR(state) {
state.error = null
}
},
actions: {
setLoading({ commit }, loading) {
commit('SET_LOADING', loading)
},
setError({ commit }, error) {
commit('SET_ERROR', error)
},
clearError({ commit }) {
commit('CLEAR_ERROR')
}
}
})
}
Posts模块
// src/store/modules/posts.js
import api from '@/api'
const state = {
posts: [],
currentPost: null,
pagination: {
page: 1,
limit: 10,
total: 0
}
}
const mutations = {
SET_POSTS(state, posts) {
state.posts = posts
},
SET_CURRENT_POST(state, post) {
state.currentPost = post
},
ADD_POST(state, post) {
state.posts.unshift(post)
},
UPDATE_POST(state, updatedPost) {
const index = state.posts.findIndex(p => p.id === updatedPost.id)
if (index !== -1) {
state.posts.splice(index, 1, updatedPost)
}
},
DELETE_POST(state, postId) {
state.posts = state.posts.filter(p => p.id !== postId)
},
SET_PAGINATION(state, pagination) {
state.pagination = { ...state.pagination, ...pagination }
}
}
const actions = {
async fetchPosts({ commit, state }, { page = 1, limit = 10 } = {}) {
try {
commit('SET_LOADING', true, { root: true })
const response = await api.get('/posts', {
params: { page, limit }
})
commit('SET_POSTS', response.data.posts)
commit('SET_PAGINATION', {
page: response.data.page,
limit: response.data.limit,
total: response.data.total
})
return response.data
} catch (error) {
commit('SET_ERROR', error.message, { root: true })
throw error
} finally {
commit('SET_LOADING', false, { root: true })
}
},
async fetchPost({ commit }, postId) {
try {
commit('SET_LOADING', true, { root: true })
const response = await api.get(`/posts/${postId}`)
commit('SET_CURRENT_POST', response.data)
return response.data
} catch (error) {
commit('SET_ERROR', error.message, { root: true })
throw error
} finally {
commit('SET_LOADING', false, { root: true })
}
},
async createPost({ commit }, postData) {
try {
const response = await api.post('/posts', postData)
commit('ADD_POST', response.data)
return response.data
} catch (error) {
commit('SET_ERROR', error.message, { root: true })
throw error
}
}
}
const getters = {
getPostById: (state) => (id) => {
return state.posts.find(post => post.id === parseInt(id))
},
getPostsByCategory: (state) => (category) => {
return state.posts.filter(post => post.category === category)
},
totalPages: (state) => {
return Math.ceil(state.pagination.total / state.pagination.limit)
}
}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}
Pinia在SSR中的配置
Pinia Store设置
// src/store/index.js
import { createPinia } from 'pinia'
export function createStore() {
const pinia = createPinia()
// SSR状态序列化支持
if (import.meta.env.SSR) {
pinia.use(({ store }) => {
// 服务端状态序列化
store.$serialize = () => {
return JSON.stringify(store.$state)
}
// 客户端状态恢复
store.$hydrate = (state) => {
store.$patch(JSON.parse(state))
}
})
}
return pinia
}
Pinia Store定义
// src/store/posts.js
import { defineStore } from 'pinia'
import api from '@/api'
export const usePostsStore = defineStore('posts', {
state: () => ({
posts: [],
currentPost: null,
loading: false,
error: null,
pagination: {
page: 1,
limit: 10,
total: 0
}
}),
getters: {
getPostById: (state) => {
return (id) => state.posts.find(post => post.id === parseInt(id))
},
getPostsByCategory: (state) => {
return (category) => state.posts.filter(post => post.category === category)
},
totalPages: (state) => {
return Math.ceil(state.pagination.total / state.pagination.limit)
}
},
actions: {
async fetchPosts({ page = 1, limit = 10 } = {}) {
this.loading = true
this.error = null
try {
const response = await api.get('/posts', {
params: { page, limit }
})
this.posts = response.data.posts
this.pagination = {
page: response.data.page,
limit: response.data.limit,
total: response.data.total
}
return response.data
} catch (error) {
this.error = error.message
throw error
} finally {
this.loading = false
}
},
async fetchPost(postId) {
this.loading = true
this.error = null
try {
const response = await api.get(`/posts/${postId}`)
this.currentPost = response.data
return response.data
} catch (error) {
this.error = error.message
throw error
} finally {
this.loading = false
}
},
async createPost(postData) {
try {
const response = await api.post('/posts', postData)
this.posts.unshift(response.data)
return response.data
} catch (error) {
this.error = error.message
throw error
}
}
}
})
数据预取策略
组件级数据预取
<!-- src/pages/PostDetail.vue -->
<template>
<div class="post-detail">
<div v-if="loading">加载中...</div>
<div v-else-if="error" class="error">
{{ error }}
</div>
<article v-else-if="post">
<header>
<h1>{{ post.title }}</h1>
<div class="meta">
<span>作者: {{ post.author }}</span>
<span>发布时间: {{ formatDate(post.createdAt) }}</span>
<span>分类: {{ post.category }}</span>
</div>
</header>
<div class="content" v-html="post.content"></div>
<footer>
<div class="tags">
<span v-for="tag in post.tags" :key="tag" class="tag">
{{ tag }}
</span>
</div>
</footer>
</article>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { usePostsStore } from '@/store/posts'
export default {
name: 'PostDetail',
// SSR数据预取
async asyncData({ store, route }) {
const postId = route.params.id
// Vuex版本
// await store.dispatch('posts/fetchPost', postId)
// Pinia版本
const postsStore = usePostsStore(store)
await postsStore.fetchPost(postId)
},
setup() {
const route = useRoute()
const postsStore = usePostsStore()
const post = computed(() => postsStore.currentPost)
const loading = computed(() => postsStore.loading)
const error = computed(() => postsStore.error)
const formatDate = (date) => {
return new Date(date).toLocaleDateString('zh-CN')
}
// 客户端数据获取
onMounted(async () => {
const postId = route.params.id
if (!post.value || post.value.id !== parseInt(postId)) {
await postsStore.fetchPost(postId)
}
})
return {
post,
loading,
error,
formatDate
}
}
}
</script>
路由级数据预取
// src/router/index.js
import { usePostsStore } from '@/store/posts'
router.beforeResolve(async (to, from, next) => {
const matched = router.resolve(to).matched
const prevMatched = router.resolve(from).matched
const activated = matched.filter((c, i) => {
return prevMatched[i] !== c
})
if (!activated.length) {
return next()
}
try {
// 并行执行所有数据预取
await Promise.all(activated.map(async (component) => {
if (component.components.default.asyncData) {
// 获取store实例
const store = router.app.config.globalProperties.$store
return component.components.default.asyncData({
store,
route: to,
router
})
}
}))
next()
} catch (error) {
console.error('数据预取失败:', error)
next(error)
}
})
状态序列化与恢复
服务端状态序列化
// src/entry-server.js
export async function render(url, context = {}) {
const { app, router, store } = createApp()
await router.push(url)
await router.isReady()
const matchedComponents = router.currentRoute.value.matched
// 执行数据预取
await Promise.all(
matchedComponents.map(component => {
if (component.components.default.asyncData) {
return component.components.default.asyncData({
store,
route: router.currentRoute.value
})
}
})
)
// 序列化状态
if (store.state) {
// Vuex
context.state = store.state
} else {
// Pinia
context.state = {}
store._s.forEach((storeInstance, key) => {
context.state[key] = storeInstance.$state
})
}
return app
}
客户端状态恢复
// src/entry-client.js
import { createApp } from './app'
const { app, router, store } = createApp()
// 恢复服务端状态
if (window.__INITIAL_STATE__) {
if (store.replaceState) {
// Vuex
store.replaceState(window.__INITIAL_STATE__)
} else {
// Pinia
Object.keys(window.__INITIAL_STATE__).forEach(key => {
const storeInstance = store._s.get(key)
if (storeInstance) {
storeInstance.$patch(window.__INITIAL_STATE__[key])
}
})
}
}
router.isReady().then(() => {
app.mount('#app', true)
})
API客户端配置
通用API客户端
// src/api/index.js
import axios from 'axios'
// 创建axios实例
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3001/api',
timeout: 10000
})
// 请求拦截器
api.interceptors.request.use(
(config) => {
// 添加认证token
const token = getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
api.interceptors.response.use(
(response) => {
return response
},
(error) => {
if (error.response?.status === 401) {
// 处理认证失败
clearToken()
if (typeof window !== 'undefined') {
window.location.href = '/login'
}
}
return Promise.reject(error)
}
)
// 获取token的通用方法
function getToken() {
if (typeof window !== 'undefined') {
// 客户端从localStorage获取
return localStorage.getItem('token')
} else {
// 服务端从请求头获取
// 这里需要通过context传递
return null
}
}
function clearToken() {
if (typeof window !== 'undefined') {
localStorage.removeItem('token')
}
}
export default api
SSR中的API调用
// src/api/ssr.js
import axios from 'axios'
// 为SSR创建专门的API客户端
export function createApiClient(context = {}) {
const api = axios.create({
baseURL: process.env.API_URL || 'http://localhost:3001/api',
timeout: 10000
})
// 在服务端添加请求头
if (context.req) {
const { req } = context
// 转发认证头
if (req.headers.authorization) {
api.defaults.headers.Authorization = req.headers.authorization
}
// 转发cookie
if (req.headers.cookie) {
api.defaults.headers.Cookie = req.headers.cookie
}
// 转发用户IP
if (req.headers['x-forwarded-for'] || req.connection.remoteAddress) {
api.defaults.headers['X-Forwarded-For'] =
req.headers['x-forwarded-for'] || req.connection.remoteAddress
}
}
return api
}
错误处理
全局错误处理
// src/store/modules/error.js
const state = {
errors: [],
globalError: null
}
const mutations = {
ADD_ERROR(state, error) {
state.errors.push({
id: Date.now(),
message: error.message,
type: error.type || 'error',
timestamp: new Date()
})
},
REMOVE_ERROR(state, errorId) {
state.errors = state.errors.filter(error => error.id !== errorId)
},
SET_GLOBAL_ERROR(state, error) {
state.globalError = error
},
CLEAR_ERRORS(state) {
state.errors = []
state.globalError = null
}
}
const actions = {
addError({ commit }, error) {
commit('ADD_ERROR', error)
// 自动清除错误
setTimeout(() => {
commit('REMOVE_ERROR', error.id)
}, 5000)
},
setGlobalError({ commit }, error) {
commit('SET_GLOBAL_ERROR', error)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
下一步
在下一章节中,我们将学习如何配置服务端渲染的Express服务器。