Pinia为什么比Vuex更适合Vue3项目
Pinia是Vue3官方推荐的状态管理方案,相比Vuex 4.x在TypeScript支持、模块化和API设计上有本质改进。Pinia移除了Mutations概念,Store定义更扁平,支持Composition API风格的Setup Store写法,天生与Vue3的响应式系统深度集成。当项目需要持久化、跨Store通信或SSR支持时,Pinia的插件机制比Vuex的Plugin系统灵活得多。本文从实际项目需求出发,拆解Pinia持久化插件的开发、跨Store通信方案以及SSR场景下的状态管理。
Setup Store与Options Store对比选型
Pinia支持两种Store定义风格:
// Options Store - 类似Vuex的写法
export const useUserStore = defineStore('user', {
state: () => ({
name: '',
token: '',
permissions: [] as string[],
}),
getters: {
isLoggedIn: (state) => !!state.token,
},
actions: {
logout() {
this.token = ''
this.permissions = []
},
},
})
// Setup Store - 组合式API风格(推荐)
export const useUserStore = defineStore('user', () => {
const name = ref('')
const token = ref('')
const permissions = ref<string[]>([])
const isLoggedIn = computed(() => !!token.value)
function logout() {
token.value = ''
permissions.value = []
}
return { name, token, permissions, isLoggedIn, logout }
})
Setup Store更灵活,可以在Store内部使用任意组合式函数、watch、computed,不受Options API的选项约束。项目中统一使用Setup Store风格,可维护性更高。
持久化插件开发实战
pinia-plugin-persistedstate是社区最常用的持久化方案,但某些场景需要自定义逻辑(如加密存储、按需持久化、过期清理)。以下从零开发一个支持加密和TTL的持久化插件:
// plugins/pinia-persist-encrypted.ts
import type { PiniaPluginContext } from 'pinia'
const ENCRYPT_KEY = import.meta.env.VITE_STORE_ENCRYPT_KEY || 'default-key'
interface PersistOptions {
key?: string
paths?: string[]
encrypt?: boolean
ttl?: number // 过期时间(毫秒)
}
function encrypt(data: string): string {
// 使用Web Crypto API或CryptoJS实现
return btoa(data) // 简化示例,生产环境请用AES
}
function decrypt(cipher: string): string {
return atob(cipher)
}
export function piniaPersistEncrypted({ store }: PiniaPluginContext) {
const persistOpts: PersistOptions = store.$state._persist || {
key: `pinia-${store.$id}`,
encrypt: false,
}
const storageKey = persistOpts.key || `pinia-${store.$id}`
// 恢复状态
try {
const raw = localStorage.getItem(storageKey)
if (raw) {
const parsed = JSON.parse(raw)
const data = parsed.__encrypted ? JSON.parse(decrypt(parsed.data)) : parsed.data
// TTL 检查
if (persistOpts.ttl && parsed.timestamp) {
const elapsed = Date.now() - parsed.timestamp
if (elapsed > persistOpts.ttl) {
localStorage.removeItem(storageKey)
return
}
}
if (persistOpts.paths) {
persistOpts.paths.forEach((path) => {
if (data[path] !== undefined) {
store.$patch({ [path]: data[path] })
}
})
} else {
store.$patch(data)
}
}
} catch (e) {
console.warn(`[Pinia Persist] 恢复 ${store.$id} 失败:`, e)
}
// 监听变更并持久化
store.$subscribe((_mutation, state) => {
try {
let data: Record<string, any>
if (persistOpts.paths) {
data = {}
persistOpts.paths.forEach((path) => {
data[path] = state[path]
})
} else {
data = { ...state }
}
const payload: Record<string, any> = { timestamp: Date.now() }
if (persistOpts.encrypt) {
payload.__encrypted = true
payload.data = encrypt(JSON.stringify(data))
} else {
payload.data = data
}
localStorage.setItem(storageKey, JSON.stringify(payload))
} catch (e) {
console.warn(`[Pinia Persist] 持久化 ${store.$id} 失败:`, e)
}
})
}
Store中使用方式:
export const useUserStore = defineStore('user', () => {
// ... state定义
}, {
_persist: {
key: 'app-user',
paths: ['token', 'permissions'],
encrypt: true,
ttl: 7 * 24 * 3600 * 1000, // 7天过期
},
})
跨Store通信与组合模式
多个Store之间存在依赖关系时,直接在action中调用另一个Store是一种方式,但容易产生循环依赖。推荐使用组合函数(Composable)封装跨Store逻辑:
// composables/useAuthFlow.ts
export function useAuthFlow() {
const userStore = useUserStore()
const cartStore = useCartStore()
const permissionStore = usePermissionStore()
async function login(credentials: LoginParams) {
const res = await authApi.login(credentials)
userStore.token = res.token
userStore.name = res.name
// 并行加载权限和购物车
await Promise.all([
permissionStore.fetchPermissions(res.token),
cartStore.fetchCart(res.token),
])
}
function logout() {
userStore.logout()
cartStore.clear()
permissionStore.reset()
}
return { login, logout }
}
在组件中使用:
// UserProfile.vue
const { login, logout } = useAuthFlow()
const userStore = useUserStore()
async function handleLogin() {
try {
await login({ username: 'admin', password: 'xxx' })
} catch (e) {
ElMessage.error('登录失败')
}
}
这种模式避免了Store间的直接耦合,通信逻辑集中在Composable层,便于测试和维护。
SSR场景下的状态管理注意事项
Vue3 SSR项目中,每个请求需要独立的Store实例,避免状态污染。Pinia通过app.use(pinia)注册时自动处理了这一点,但有几个细节需要注意:
// server-entry.ts
import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'
export async function render(url: string) {
const app = createSSRApp(App)
const pinia = createPinia()
app.use(pinia)
// 路由匹配和数据预取
const router = createRouter()
await router.push(url)
await router.isReady()
// 在服务端执行数据预取
const store = useUserStore(pinia)
await store.fetchUserProfile()
// 序列化状态传给客户端
const state = JSON.stringify(pinia.state.value)
const html = await renderToString(app)
return { html, state }
}
// client-entry.ts
const pinia = createPinia()
// 用服务端传来的状态初始化
if (window.__PINIA_STATE__) {
pinia.state.value = window.__PINIA_STATE__
}
app.use(pinia)
关键点:服务端创建的pinia实例必须与客户端一一对应,通过state.value的序列化/反序列化保持一致。持久化插件在SSR场景下应跳过localStorage操作,仅在客户端生效。可以在插件中判断typeof window !== ‘undefined’来区分环境。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-zhuang-tai-guan-li-jin-jie-pinia-chi-jiu/