首页
/ Airi 项目实践:基于 Pinia v3 的 Vue 状态管理完整指南

Airi 项目实践:基于 Pinia v3 的 Vue 状态管理完整指南

2026-09-07 15:33:18作者:钟日瑜

本指南以仓库 .agents/skills/pinia/SKILL.md 技能文档(基于 Pinia v3.0.4 整理)为骨架,结合 Airi 仓库中 stage-webstage-pocketstage-tamagotchistage-ui 等真实源码场景展开。读完你将掌握:Option/Setup 两种 Store 的定义方式、state/getters/actions 的正确使用姿势、storeToRefs 响应式解构、Store 组合与循环依赖规避、Pinia 插件机制、@pinia/testing 单元测试,以及 SSR/Nuxt/HMR 与组件外使用 Store 的工程化要点。

一、Pinia 是什么:官方状态库的定位与设计

Pinia 是 Vue 官方推荐的状态管理库,与 Vuex 相比最大变化是去掉 mutations、全面拥抱 Composition API、原生 TypeScript 类型推导,并内置 Vue Devtools 支持。它同时支持 Options API 与 Composition API(Setup)两种写法,是 defineStore 之后"即用即所得"的轻量方案。

在 Airi 仓库中,Pinia 被广泛用于多个 Vue 应用/包:

原技能文档位于 .agents/skills/pinia/SKILL.md,本仓库 Store 相关技能引用可直接查看其 references/ 子目录中的 9 篇主题文档(下文每节均标注对应参考文档链接),便于按需深入。

二、定义 Store:Option Store 与 Setup Store 的选择

Store 通过 defineStore() 定义,必须有一个全局唯一名称(建议文件名与 store id 一致)。核心是三大概念:state(状态)、getters(计算派生值)、actions(业务动作)

2.1 Option Store:把 Pinia 当作增强版 Options API

Option Store 写法与 Vue 的 Options API 一一对应:state 相当于 datagetters 相当于 computedactions 相当于 methods

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Eduardo',
  }),
  getters: {
    doubleCount: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++
    },
  },
})

2.2 Setup Store:官方技能文档推荐的主流写法

Setup Store 使用 Composition API 语法,本质是一个普通函数:函数体内的 ref()/reactive()statecomputed()getters,普通 functionactions

import { ref, computed } from 'vue'
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const name = ref('Eduardo')
  const doubleCount = computed(() => count.value * 2)

  function increment() {
    count.value++
  }

  return { count, name, doubleCount, increment }
})

关键约束:setup 内创建的响应式状态必须全部通过 return 暴露,否则 Pinia 无法跟踪它们。

Airi 仓库绝大多数 Store 采用 Setup Store。例如 apps/stage-web/src/stores/pwa.ts 定义了 PWA 更新感知 Store,state 是 ref<(() => void)[]>([]) 的更新钩子队列,action 则借助 Vue 生命周期 onMounted 动态注册 Service Worker 更新流程:

export const usePWAStore = defineStore('pwa', () => {
  const updateReadyHooks = ref<(() => void)[]>([])
  const breakpoints = useBreakpoints(breakpointsTailwind)
  const isMobile = breakpoints.smaller('md')

  onMounted(async () => {
    if (import.meta.env.SSR) return
    // ... 动态 import 注册 SW 并 push 更新钩子
    updateReadyHooks.value.push(updateSW)
  })
})

apps/stage-web/src/stores/devtools-lag.ts 则是一个复杂得多、完全面向逻辑的 Setup Store:它用 reactive() 维护 fps/帧耗时/longtask/memory 采样缓冲区,用 shallowRef 保存采样器支持性,配合 watchonScopeDispose 清理定时器——这在 Option Store 里很难组织,正是 Setup Store 适合"复杂逻辑、组合式与 watcher"的典型证据(对应 .agents/skills/pinia/references/core-stores.md)。

2.3 在组件中使用 Store

<script setup> 中直接调用以 useXxx 命名的 useStore() 即可获得 Store 单例,Pinia 会把当前应用上下文自动注入:

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

const store = useCounterStore()
// 访问:store.count / store.doubleCount / store.increment()
</script>

Actions 本身就是绑定到 store 上的,因此可以直接解构;但 state/getters 解构会丢失响应性,必须用 storeToRefs() 包裹:

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

const store = useCounterStore()

// ❌ 破坏响应性
const { name, doubleCount } = store

// ✅ state / getters 用 storeToRefs 保持响应
const { name, doubleCount } = storeToRefs(store)

// ✅ actions 可以直接解构(已绑定 store)
const { increment } = store
</script>

Airi 仓库中对这一模式的应用十分密集,如 apps/stage-web/src/pages/index.vue 一次从多个 store 中解构出响应式状态与动作:

const backgroundStore = useBackgroundStore()
const { selectedOption, sampledColor } = storeToRefs(backgroundStore)

const settingsAudioDeviceStore = useSettingsAudioDevice()
const { stream, enabled } = storeToRefs(settingsAudioDeviceStore)

const { activeProvider: activeChatProvider, activeModel: activeChatModel } = storeToRefs(consciousnessStore)
const chatStore = useChatStore()

这里 activeProvider 还演示了 getters 用别名重命名后交给组件消费的常见模式。

三、State:类型、变更、重置与订阅

3.1 TypeScript 下的 State 声明

TypeScript 推断可自动工作;复杂结构建议声明接口或使用类型断言:

interface UserInfo {
  name: string
  age: number
}

// 方式一:内联类型断言
export const useUserStore = defineStore('user', {
  state: () => ({
    userList: [] as UserInfo[],
    user: null as UserInfo | null,
  }),
})

// 方式二:为 state 函数标注返回类型(推荐,语义更清晰)
interface State {
  userList: UserInfo[]
  user: UserInfo | null
}

export const useUserStore = defineStore('user', {
  state: (): State => ({
    userList: [],
    user: null,
  }),
})

3.2 读取与直接修改

Pinia 中 state 是"可写的",可以直接赋值,模板中也可直接 v-model 绑定到 store 状态:

const store = useStore()
store.count++
<input v-model="store.count" type="number" />

3.3 批量变更 $patch

多字段同时变更用 $patch,会一次性触发更新且更利于 Devtools 追踪:

// 对象语法(适合扁平状态)
store.$patch({
  count: store.count + 1,
  name: 'DIO',
})

// 函数语法(适合数组 push / 复合逻辑)
store.$patch((state) => {
  state.items.push({ name: 'shoes', quantity: 1 })
  state.hasChanged = true
})

3.4 重置 $reset

  • Option Store 内置 $reset(),会把状态重置到 state 函数的初始值;
  • Setup Store 没有内置 $reset,需要自行实现同名函数(与生命周期/内部 ref 协作):
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)

  function $reset() {
    count.value = 0
  }

  return { count, $reset }
})

3.5 订阅 $subscribe(持久化等场景)

$subscribe 与 Vue 的 watch 类似但只观察该 store 的 state 变化,回调能拿到 mutation 元信息:

cartStore.$subscribe((mutation, state) => {
  mutation.type     // 'direct' | 'patch object' | 'patch function'
  mutation.storeId  // 'cart'
  mutation.payload  // 仅 'patch object' 时有值

  localStorage.setItem('cart', JSON.stringify(state))
})

// 选项:默认 flush 为 'pre'(组件更新前)
cartStore.$subscribe(callback, { flush: 'sync' })    // 立即执行
cartStore.$subscribe(callback, { detached: true })   // 组件卸载后仍保留

Airi 中 background 相关的持久化偏好就是借助 VueUse useLocalStorage + computed 可写 getter 实现的(见 packages/stage-layouts/src/stores/background.ts),这类"订阅即持久化"的思路在生产项目里非常实用。

四、Getters:派生状态的正确姿势

Getter 等价于 Vue 的 computed,适合放依赖 state 的派生值,避免在多个组件里重复计算。

4.1 基础与访问其他 getter

getters: {
  doubleCount: (state) => state.count * 2,
  // 访问同 store 的其他 getter,必须用 this 并显式标注返回类型
  doublePlusOne(): number {
    return this.doubleCount + 1
  },
}

4.2 带参数的 getter(注意缓存丢失)

返回一个函数的写法可以让 getter 接收参数,但每次调用都不走缓存

getters: {
  getUserById: (state) => {
    return (userId: string) => state.users.find((user) => user.id === userId)
  },
}

若想保留一部分缓存收益,可先在 getter 内部做一次过滤得到稳定集合,再返回查找函数:

getters: {
  getActiveUserById(state) {
    const activeUsers = state.users.filter((user) => user.active)
    return (userId: string) => activeUsers.find((user) => user.id === userId)
  },
}

4.3 在 getter 中访问其他 store

import { useOtherStore } from './other-store'

getters: {
  combined(state) {
    const otherStore = useOtherStore()
    return state.localData + otherStore.data
  },
}

五、Actions:业务逻辑与异步流程

与 getters 不同,actions 可以是异步的,是组织业务逻辑的推荐位置。

actions: {
  increment() {
    this.count++
  },
  randomizeCounter() {
    this.count = Math.round(100 * Math.random())
  },
  // 异步 action
  async registerUser(login: string, password: string) {
    try {
      this.userData = await api.post({ login, password })
    } catch (error) {
      return error
    }
  },
  // 在 action 中组合其他 store
  async fetchUserPreferences() {
    const auth = useAuthStore()
    if (auth.isAuthenticated) {
      this.preferences = await fetchPreferences()
    }
  },
}

SSR 黄金法则:所有 useStore() 调用都必须发生在任意 await 之前,以保证使用正确的 Pinia 实例:

async orderCart() {
  // ✅ 先取 store
  const user = useUserStore()

  await apiOrderCart(user.token, this.items)
  // ❌ 不要在 await 之后再调用 useStore()(SSR 下拿不到正确实例)
}

Airi 仓库中 packages/stage-ui/src/stores/characters.ts 是一个重量级实战范本:它在 Setup Store 里通过 @pinia/coladauseQuery / useMutation 封装远程角色数据,实现"先写本地、失败回退、成功再同步远端"的 local-first 乐观更新,并将一整套可变逻辑收敛进 createCharacterStoreController 纯函数工厂,保证控制器可脱离 Pinia 单独测试。

5.1 订阅 actions:$onAction

const unsubscribe = someStore.$onAction(
  ({ name, store, args, after, onError }) => {
    const startTime = Date.now()
    console.log(`Start "${name}" with params [${args.join(', ')}]`)

    after((result) => {
      console.log(`Finished "${name}" after ${Date.now() - startTime}ms`)
    })

    onError((error) => {
      console.warn(`Failed "${name}": ${error}`)
    })
  }
)

unsubscribe() // 返回的取消函数,组件卸载时应调用

// 传第二个参数 true 可在组件卸载后继续保留
someStore.$onAction(callback, true)

这正是 packages/stage-ui/src/libs/pinia/pinia-plugin-tracing.ts 这类"性能/错误追踪插件"的基础:它对所有 action 进行观测、按 piniaActionTracingChannelName 广播,并附带失败率限流统计(每 5 秒窗口最多记录 10 条),在 apps/stage-web/src/main.ts 中仅 DEV 环境注册。

六、Options API 辅助函数与 Setup Store 注入

  • 若仍在使用 Options API 组件,可用 mapState / mapWritableState / mapActions 把 store 映射到组件的 computed / methods
import { mapState, mapWritableState, mapActions } from 'pinia'
import { useCounterStore } from '../stores/counter'

export default {
  computed: {
    ...mapState(useCounterStore, ['count', 'doubleCount']), // 只读
    ...mapWritableState(useCounterStore, ['count']),        // 可写
  },
  methods: {
    ...mapActions(useCounterStore, ['increment']),
  },
}
  • Setup Store 内也能直接消费组件级全局注入(injectuseRoute 等),返回给组件的应是业务状态而不是这些内部句柄:
import { inject } from 'vue'
import { useRoute } from 'vue-router'

export const useSearchFilters = defineStore('search-filters', () => {
  const route = useRoute()
  const appProvided = inject('appProvided')
  // 不要 return 这些内部引用
  return { /* ... */ }
})

七、在 Store 中使用组合式函数(Composables)

Setup Store 的最大红利之一是可以直接调用任意 VueUse 组合式函数,把"模块级共享状态"装进 Pinia 语义里。

7.1 Option Store:仅限返回可写 ref 的 composable

import { defineStore } from 'pinia'
import { useLocalStorage } from '@vueuse/core'

export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: useLocalStorage('pinia/auth/login', 'bob'), // ✅ useLocalStorage 返回可写 ref
  }),
})

可以useLocalStorageuseAsyncState 这类返回可写 ref 的组合式函数。 不行:返回函数、或只暴露只读数据的 composable(无法放入 state 函数)。

7.2 Setup Store:几乎任何 composable 都行

import { defineStore } from 'pinia'
import { useMediaControls } from '@vueuse/core'
import { ref } from 'vue'

export const useVideoPlayer = defineStore('video', () => {
  const videoElement = ref<HTMLVideoElement>()
  const src = ref('/data/video.mp4')
  const { playing, volume, currentTime, togglePictureInPicture } =
    useMediaControls(videoElement, { src })

  function loadVideo(element: HTMLVideoElement, newSrc: string) {
    videoElement.value = element
    src.value = newSrc
  }

  return { src, playing, volume, currentTime, loadVideo, togglePictureInPicture }
})

注意:不要返回 videoElement 这类不可序列化的 DOM 引用——它们是内部实现细节。

Airi 对这种"composable + Store"的组合运用得淋漓尽致:

详细说明见 .agents/skills/pinia/references/features-composables.md

八、组合 Store:跨 Store 通信与循环依赖规避

Store 之间可以互相调用共享状态,但有一个铁律:

两个 Store 不能在各自 setup 阶段直接读取对方的 state,否则会无限循环:

// ❌ 无限循环
const useX = defineStore('x', () => {
  const y = useY()
  y.name // 不要在 setup 阶段读取!
  return { name: ref('X') }
})

const useY = defineStore('y', () => {
  const x = useX()
  x.name // 同样禁止!
  return { name: ref('Y') }
})

解法:把跨 Store 读取推迟到 getter / computed / action 里执行:

const useX = defineStore('x', () => {
  const y = useY()
  function doSomething() {
    const yName = y.name // ✅ 运行时读取
  }
  return { name: ref('X'), doSomething }
})

在 Setup Store 中组合其他 Store 时,把 useUserStore() 放在顶部:

export const useCartStore = defineStore('cart', () => {
  const user = useUserStore()
  const list = ref([])

  const summary = computed(() =>
    `Hi ${user.name}, you have ${list.value.length} items`,
  )

  function purchase() {
    return apiPurchase(user.id, list.value)
  }

  return { list, summary, purchase }
})

Option Store 则在 getter / action 内部调用 useUserStore()。跨 store 的 action 调用同样如此(例如角色 Store 内部就引用了 useAuthStore() 读取 userId,见 packages/stage-ui/src/stores/characters.ts)。SSR 下记住:所有 useStore() 必须在 await 前完成。详见 .agents/skills/pinia/references/features-composing-stores.md

九、插件机制:给所有 Store 统一注入能力

Pinia 插件通过 pinia.use(plugin) 注册,作用于每一个之后创建的 store,可用来注入属性、状态、订阅甚至封装第三库对象。

9.1 基础插件与上下文

import { createPinia } from 'pinia'

function SecretPiniaPlugin() {
  return { secret: 'the cake is a lie' }
}

const pinia = createPinia()
pinia.use(SecretPiniaPlugin)

// 任意 store 都能访问 store.secret

插件回调会收到一个上下文对象,含 pinia(实例)、app(Vue 应用)、store(正在被扩展的 store)、options(该 store 的定义选项):

import { PiniaPluginContext } from 'pinia'

export function myPiniaPlugin(context: PiniaPluginContext) {
  context.pinia   // pinia 实例
  context.app     // Vue app 实例
  context.store   // 被扩展的 store
  context.options // store 定义选项
}

9.2 注入属性 / 状态 / 外部对象

// 注入属性(return 对象会自动纳入 Devtools 追踪)
pinia.use(() => ({ hello: 'world' }))

// 或在回调里直接赋值,并手动登记到 _customProperties 让 dev 下可见
pinia.use(({ store }) => {
  store.hello = 'world'
  if (process.env.NODE_ENV === 'development')
    store._customProperties.add('hello')
})

// 注入响应式状态:需同时写 store 与 store.$state(SSR/Devtools 才正确)
pinia.use(({ store }) => {
  if (!store.$state.hasOwnProperty('hasError')) {
    const hasError = ref(false)
    store.$state.hasError = hasError
  }
  store.hasError = toRef(store.$state, 'hasError')
})

// 注入非响应式外部对象(router 等)用 markRaw 包裹,避免被代理成响应式
pinia.use(({ store }) => {
  store.router = markRaw(router)
})

9.3 自定义 Store 选项(把插件做成声明式)

可以在 store 定义里追加自定义字段(Option Store 直接加属性;Setup Store 通过 defineStore 的第三个参数传对象),由插件统一消费:

// Option Store 定义自定义选项
defineStore('search', {
  actions: {
    searchContacts() { /* ... */ },
  },
  debounce: {
    searchContacts: 300,
  },
})

// Setup Store 的写法:defineStore(id, setupFn, options)
defineStore('search', () => { /* ... */ }, {
  debounce: { searchContacts: 300 },
})

插件读取并加工:

pinia.use(({ options, store }) => {
  if (options.debounce) {
    return Object.keys(options.debounce).reduce((acc, action) => {
      acc[action] = debounce(store[action], options.debounce[action])
      return acc
    }, {})
  }
})

9.4 TypeScript 模块扩展

为了让插件注入的类型对使用者可见,需要对 pinia 模块做声明合并:

// 自定义属性
declare module 'pinia' {
  export interface PiniaCustomProperties {
    router: Router
    hello: string
  }
}

// 自定义状态
declare module 'pinia' {
  export interface PiniaCustomStateProperties<S> {
    hasError: boolean
  }
}

// 自定义选项
declare module 'pinia' {
  export interface DefineStoreOptionsBase<S, Store> {
    debounce?: Partial<Record<keyof StoreActions<Store>, number>>
  }
}

9.5 在插件里统一订阅

插件内可以直接调用 $subscribe / $onAction,实现"所有 Store 统一打点":

pinia.use(({ store }) => {
  store.$subscribe(() => { /* 响应 state 变化 */ })
  store.$onAction(() => { /* 响应 actions */ })
})

完整插件文档见 .agents/skills/pinia/references/features-plugins.md

Airi 仓库的插件实践:Airi 在运行时配置了两个 Pinia 插件(见 apps/stage-web/src/main.ts):

const pinia = createPinia()
const synced = setupSynced()
pinia.use(synced.pinia)                        // 跨标签页/窗口状态同步
if (import.meta.env.DEV)
  pinia.use(piniaPluginTracing)                // DEV 下的 action 追踪

其中 piniaPluginTracing 的实现位于 packages/stage-ui/src/libs/pinia/pinia-plugin-tracing.ts,并有对应的 pinia-plugin-tracing.test.ts 作为行为验证。此外依赖上还可见 pinia-plugin-synced@pinia/colada(声明在 packages/stage-ui/package.json),是"官方生态全家桶"的真实样板。

十、单元测试:@pinia/testing 的正确用法

10.1 纯 Store 单测:每个用例新建 Pinia

import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '../src/stores/counter'

describe('Counter Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia()) // 每个用例独立实例,避免用例间状态串扰
  })

  it('increments', () => {
    const counter = useCounterStore()
    expect(counter.n).toBe(0)
    counter.increment()
    expect(counter.n).toBe(1)
  })
})

带插件的场景可先构造插件化 Pinia 再激活:

const app = createApp({})

beforeEach(() => {
  const pinia = createPinia().use(somePlugin)
  app.use(pinia)
  setActivePinia(pinia)
})

10.2 组件测试:createTestingPinia

npm i -D @pinia/testing
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'

const wrapper = mount(Counter, {
  global: {
    plugins: [createTestingPinia()],
  },
})

const store = useSomeStore()

// 直接改 state 或 $patch
store.name = 'new name'
store.$patch({ name: 'new name' })

// actions 默认被 stub 成 vi.fn,可用断言验证调用
store.someAction()
expect(store.someAction).toHaveBeenCalledTimes(1)

关键参数一览:

参数 取值/默认 作用
initialState { counter: { n: 20 } } 按 store id 预置初始状态
stubActions 默认 true false 执行真实 action;传数组或 (name, store) => boolean 可选择性 stub
createSpy 默认 vi.fn 无全局 spy 时可传 sinon.spy
plugins [] 在测试 Pinia 上注册真实插件,不要testingPinia.use(...)
// 选择性 stub:只 stub increment/reset
createTestingPinia({ stubActions: ['increment', 'reset'] })

// 函数式控制
createTestingPinia({
  stubActions: (actionName) => actionName.startsWith('set'),
})

// 真实执行部分 action,其余 stub
createTestingPinia({ stubActions: false })

// mock action 的返回值(action 即 Mock)
import type { Mock } from 'vitest'
store.someAction.mockResolvedValue('mocked value')

10.3 Mock getters 与类型安全

测试中 getter 是可写的(可覆盖其计算值,赋 undefined 恢复默认行为);需要类型安全的 Mock Store 时可用条件类型包装:

const pinia = createTestingPinia()
const counter = useCounterStore(pinia)
counter.double = 3 // 覆盖 computed
counter.double = undefined // 恢复默认计算

// 类型安全的 mocked store
function mockedStore<TStoreDef extends () => unknown>(
  useStore: TStoreDef,
): TStoreDef extends StoreDefinition<infer Id, infer State, infer Getters, infer Actions>
  ? Store<Id, State, Record<string, never>, {
      [K in keyof Actions]: Actions[K] extends (...args: any[]) => any
        ? Mock<Actions[K]>
        : Actions[K]
    }>
  : ReturnType<TStoreDef> {
  return useStore() as any
}

E2E 测试无需任何特殊处理——Pinia 在真实环境正常工作。仓库中 packages/stage-ui/package.json 即声明了 @pinia/testing 为开发依赖,且真实项目里也能看到 store 的 vitest 测试(例如 pinia-plugin-tracing.test.ts),可作参考。完整测试方法论见 .agents/skills/pinia/references/best-practices-testing.md

十一、组件之外使用 Store(导航守卫/插件/中间件)

Store 依赖 pinia 实例——组件内自动注入,组件外则需确保在 pinia 安装后调用,必要时显式传入实例

11.1 SPA:在安装 pinia 之后再调用

import { useUserStore } from '@/stores/user'
import { createPinia } from 'pinia'
import { createApp } from 'vue'
import App from './App.vue'

// ❌ pinia 尚未创建,报错
const userStore = useUserStore()

const pinia = createPinia()
const app = createApp(App)
app.use(pinia)

// ✅ 此时才能安全调用
const userStore = useUserStore()

11.2 路由守卫:把调用放进回调

模块顶层调用可能因 import 顺序失败,应延迟到守卫回调内:

router.beforeEach((to) => {
  // ✅ 守卫触发时 pinia 必然已安装
  const store = useUserStore()
  if (to.meta.requiresAuth && !store.isLoggedIn)
    return '/login'
})

11.3 SSR:始终显式传 pinia

const pinia = createPinia()
const app = createApp(App)
app.use(router)
app.use(pinia)

router.beforeEach((to) => {
  const main = useMainStore(pinia) // ✅ SSR 下必须显式传递
  if (to.meta.requiresAuth && !main.isLoggedIn)
    return '/login'
})

11.4 serverPrefetch / onServerPrefetch

Options 组件用 this.$pinia<script setup>onServerPrefetch 正常工作无需特殊处理。核心原则只有一条:useStore() 推迟到 pinia 安装之后运行的函数中,而不是模块作用域。详见 .agents/skills/pinia/references/best-practices-outside-component.md

十二、SSR:状态水合与跨请求隔离

Pinia 在 SSR 下要求 store 在 setup 顶部、getter 或 action 中调用(此时 Pinia 能识别应用上下文)。

12.1 序列化与注入

服务端渲染完成后,把 pinia.state.value 序列化注入 HTML(推荐使用 devalue 这类 XSS 安全的序列化工具,而非裸 JSON),客户端在任何 useStore() 之前反序列化回填:

// 服务端
import devalue from 'devalue'
import { createPinia } from 'pinia'

const pinia = createPinia()
// ... renderToString 完成后
const serializedState = devalue(pinia.state.value)
// 将 serializedState 注入 HTML 全局变量

// 客户端(首次 useStore() 之前)
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)

if (typeof window !== 'undefined') {
  pinia.state.value = JSON.parse(window.__pinia)
}

12.2 composable 状态在 SSR 下的水合处理

  • Option Store:用 hydrate(state, initialState) 忽略服务端状态、改从浏览器读取(典型如 useLocalStorage 持久化的登录态);
  • Setup Store:用 skipHydrate() 标记不应从服务端水合的状态。它只作用于 ref 形式的 state,对函数与非响应式值无效。
export const useColorStore = defineStore('colors', () => {
  const { isSupported, open, sRGBHex } = useEyeDropper()
  const lastColor = useLocalStorage('lastColor', sRGBHex)
  return {
    lastColor: skipHydrate(lastColor), // 客户端专属,跳过水合
    open,        // 函数,无水合需求
    isSupported, // 布尔值非响应式
  }
})

12.3 SSR 关键点清单

  1. store 调用放在函数内而非模块作用域;
  2. SSR 下组件外使用 store 必须传 pinia 实例;
  3. 任何 useStore() 之前完成状态水合;
  4. devalue 等做安全序列化;
  5. 每个请求新建 Pinia 实例,杜绝跨请求状态污染。

详见 .agents/skills/pinia/references/advanced-ssr.md

十三、Nuxt 集成(供 SSR 场景扩展)

Airi 仓库本身是 Vite + Vue(非 Nuxt 应用),但技能文档的 Nuxt 章节对任何 SSR 或迁移到 Nuxt 的场景都值得保留。要点如下:

  • 安装:npx nuxi@latest module add pinia,会同时装好 @pinia/nuxtpinia;npm 用户若遇 ERESOLVE 可加 "overrides": { "vue": "latest" }
  • 配置 nuxt.config.tsmodules: ['@pinia/nuxt'];自定义 store 目录用 pinia.storesDirs,默认自动引入 app/stores/(Nuxt 4)或 stores/
  • 自动引入 usePinia()defineStore()storeToRefs()acceptHMRUpdate()
  • 页面数据抓取优先 await callOnce('user', () => store.fetchUser()),导航级刷新可加 { mode: 'navigation' }
  • 中间件 / 守卫中显式传 nuxtApp.$pinia
  • Pinia 插件统一写在 plugins/myPiniaPlugin.tsdefineNuxtPlugin(({ $pinia }) => { $pinia.use(...) }) 中。

完整内容见 .agents/skills/pinia/references/advanced-nuxt.md

十四、HMR:开发期无刷新改 Store

Pinia 支持在编辑 store 逻辑时通过热更新保留当前 state。做法是在每个 store 定义之后追加一段 HMR 代码:

import { defineStore, acceptHMRUpdate } from 'pinia'

export const useAuth = defineStore('auth', {
  // store options...
})

if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useAuth, import.meta.hot))
}

Setup Store 写法一致。兼容性方面:Vite 通过 import.meta.hot 官方支持,Webpack 走 import.meta.webpackHot,任何实现该规范的打包器均可工作;Nuxt 下 acceptHMRUpdate 会自动引入,但仍需手写 HMR 片段。收益是:改 state/getters/actions 时页面不重载、当前运行时状态不丢失,开发迭代显著提速。详见 .agents/skills/pinia/references/advanced-hmr.md

十五、官方技能文档给出的核心建议汇总

以下来自 .agents/skills/pinia/SKILL.md 的 Key Recommendations,是直接可用的团队规范:

  • 复杂逻辑、组合式函数、watcher 场景优先使用 Setup Store(本仓库的 pwa.tsdevtools-lag.tsbackground.tscharacters.ts 全部如此);
  • 解构 state/getters 必须用 storeToRefs() 以保持响应性;
  • actions 可直接解构,它们已绑定到 store 实例;
  • 在函数内部调用 store,而非模块顶层——尤其 SSR;
  • 每个 store 添加 HMR 支持以改善开发体验;
  • 组件测试用 @pinia/testing 获得 mock 化 store。

十六、在 Airi 仓库中进一步阅读

结合本篇指南与上述真实代码对照阅读,即可把 Pinia 的"定义—使用—扩展—测试—SSR—HMR"完整链路落到 Airi 这般规模的工程实践中。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388