Pinia的设计模式

pinia的核心原理是使用vue的核心响应式系统包装一个状态对象,用闭包管理store实例,这个和composable的思想是一致的,用插件机制扩展能力

单例模式的实现

Pinia是通过单例模式实现,无论在应用的哪个组件调用useStore,拿到的都是同一个store实例,状态共享、响应式同步,它的单例实现比较巧妙,核心是用全局变量缓存已经创建的store实例,而不是靠javascript的类单例或者模块单例。

Pinia会在内部维护一个全局的Map,比如pinia._s用来缓存所有已经创建了的store,当调用useStore的时候,会通过id去pinia._s里查找,如果有就返回,没有就创建,这样无论调用多少次都用于返回一个实例。

下面是一个简化后的逻辑:

// Pinia 内部简化逻辑
function createPinia() {
  const pinia = {
    _s: new Map(),  // 全局缓存:store id → store 实例

    install(app) {
      // 把 pinia 挂到 app 上,供全局访问
      app.config.globalProperties.$pinia = pinia
    },
  }
  return pinia
}

function useStore(id) {
  const pinia = getCurrentPinia()  // 获取当前应用的 pinia 实例

  // 1. 先查缓存
  if (pinia._s.has(id)) {
    return pinia._s.get(id)
  }

  // 2. 没有就创建
  const store = createStore(id)
  pinia._s.set(id, store)  // 存入缓存
  return store
}

每个store定义的时候都有一个唯一的id,比如defineStore('counter', ...)里面的 coutner,这个Id就是缓存的key,同id的store永远只有一个实例,缓存挂在pinia实例上,而不是模块级的变量。这样就算一个页面里有多个Vue应用,每个应用也可以有独立的Pinia实例,互相不干扰。

ssr支持

pinia是支持ssr的,服务端渲染的时候,每个请求都会创建独立的pinia实例,避免不同请求之间的状态串扰。什么叫做pinia每个请求都会创建独立的pinia实例,ssr下,你什么时候又支持这个代码会在服务端执行?

因为ssr下,同一个vue应用的代码会在两个地方执行,首次请求是服务端,执行vue组件代码生成html字符串,首次响应之后客户端接管服务器生成的html,继续交互,后续导航也是在客户端执行。

我们知道,在一个vue应用,我们会

我们在vue组件的setup位置里执行的代码比如下面:

<script setup>
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()  // ← 这行在服务端会执行

// 服务端会执行:获取数据
await counter.fetchData()          // ← 这行在服务端会执行

// 客户端才会执行:操作 DOM
onMounted(() => {
  console.log('只在客户端执行')    // ← 服务端不执行
})
</script>

<template>
  <div>{{ counter.count }}</div>   <!-- 服务端会渲染成 HTML -->
</template>

里面的useCounterStore在服务端,可能就表现为:

app.get('*', async (req, res) => {
  const app = createSSRApp(App)       // 1. 创建 Vue 应用
  const pinia = createPinia()          // 2. 每个请求新建 Pinia 实例
  app.use(pinia)                       // 3. 把 Pinia 挂到应用上

  const html = await renderToString(app)  // 4. 执行组件 setup(),这里会调用 useCounterStore()
  res.send(html)
})

setup里的pinia的代码可能就会在服务端里创建这个实例,就会执行setup,创建和填充store,然后收集了所有的store状态之后,把状态序列化后注入html,返回给浏览器。

如果我们手写server.js,那么注入的是自己控制,如果在nuxt中的话,我们不需要手动序列化和注入,内部会有nitro和payload机制自动完成,具体是nuxt会在服务端执行页面组件之后,pinia的store状态会被nuxt的payload系统收集,我们只需要在nuxt.config.ts里面配置pinia模块就行。注入的位置可能是_prelaod.json或者内联__NUXT__脚本。

Nuxt 把状态合并进了它自己的 payload 系统,不会单独暴露一个 __PINIA_STATE__。你可能会在 HTML 里看到类似:

<script type="application/json" id="__NUXT_DATA__">[...]</script>

pinia的持久化

Pinia 官方没有内置持久化,但社区有成熟的插件 pinia-plugin-persistedstate,它把 store 状态自动同步到 localStorage 或 sessionStorage。

import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)

// store里启用
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0, name: 'Alice' }),
  persist: true,  // 整个 store 都持久化
})

// 精细控制
export const useUserStore = defineStore('user', {
  state: () => ({
    token: '',
    profile: {},
    tempData: {},
  }),
  persist: {
    key: 'my-user',              // 存储的 key,默认是 store id
    storage: sessionStorage,     // 存储方式,默认 localStorage
    pick: ['token', 'profile'],  // 只持久化这两个字段
    // 或者用 omit 排除
    // omit: ['tempData'],
    serializer: {                // 自定义序列化
      serialize: JSON.stringify,
      deserialize: JSON.parse,
    },
  },
})