Vue3组合式API状态管理进阶:Pinia持久化插件开发与跨组件通信方案设计

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/

(0)
小编小编
上一篇 13小时前
下一篇 13小时前

相关推荐