shyarchershyarcher
首页
  • 学习笔记
  • 旧版迁移内容
Github
首页
  • 学习笔记
  • 旧版迁移内容
Github
  • 学习笔记

    • 博客搭建

      • Vuepress+githubPages搭建个人博客流程
    • AI部署

      • AI服务器配置指南
      • OpenCode 部署与使用完全指南
    • vue相关

      • 01vue的基本概念
      • 02侦听器和axios
      • 03vue生命周期和组件数据传递
      • 04vuex笔记
      • vue3学习笔记
      • pinia学习笔记
    • js相关

      • 原型和继承
      • jQuery笔记
    • react相关

      • Hooks笔记
      • React学习笔记
    • 后端

      • 关于服务器的一些概念
      • node.js笔记
      • nodejs后台起服务步骤
    • git相关

      • 给git设置代理
      • git笔记
    • 部署相关

      • webpack笔记
    • echarts

      • echarts笔记
  • 旧版迁移内容

    • 利用Hugo+gitHub搭建个人博客
    • 利用JavaSwing开发一款桌面塔防游戏
    • 配置达梦驱动包至本地Maven库
    • 热部署插件JRebel使用说明
    • Windows触摸板操作及谷歌浏览器快捷键整理
    • yapi部署笔记

Pinia 详细学习笔记

面向 Vue 3 的状态管理库,官方钦定的 Vuex 5.0 替代方案。

  • 1. Pinia 简介与动机
  • 2. 安装与初始化
  • 3. 定义 Store
    • 3.1 Option Store
    • 3.2 Setup Store
  • 4. State
    • 4.1 访问 state
    • 4.2 重置 state
    • 4.3 修改 state 的多种方式
  • 5. Getters
  • 6. Actions
  • 7. 在组件中使用 Store
    • 7.1 组合式 API (Composition API)
    • 7.2 选项式 API (Options API)
  • 8. Store 间相互引用
  • 9. 插件系统
  • 10. 服务端渲染(SSR)
  • 11. 开发工具支持
  • 12. 与 Vuex 的对比
  • 13. 最佳实践
  • 14. 常见问题 FAQ
  • 15. 总结

1. Pinia 简介与动机

Pinia 是 Vue 生态中的轻量级状态管理库,起初是 Vuex 5 的实验性实现,后因设计优秀被官方收编,现在已是 Vue 官方推荐的状态管理工具(甚至对 Vue 2 也提供支持)。

核心优势:

  • 完整的 TypeScript 支持:类型推导极其自然,无需额外的类型声明。
  • 模块化设计:不再有 modules 嵌套,每个 store 都是独立的,按需引入。
  • 删除 mutations:actions 直接支持同步/异步操作,代码更简洁。
  • 极轻量:压缩后仅约 1KB。
  • 支持 Composition API 和 Options API:写法灵活。
  • 开发者工具集成:完美支持 Vue Devtools,时间旅行、状态快照。
  • 插件扩展:可自定义插件实现日志、持久化等能力。
  • SSR 友好:内置服务端渲染场景的状态管理方案。

2. 安装与初始化

npm install pinia
# 或
yarn add pinia
# 或
pnpm add pinia

在 Vue 应用中注册:

// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
app.use(createPinia())
app.mount('#app')

若在使用 Vue 2,需要额外安装 @vue/composition-api 并使用 PiniaVuePlugin,此处仅讨论 Vue 3 场景。


3. 定义 Store

Pinia 支持两种语法定义 store:Option Store(类似 Vuex)和 Setup Store(类组合式 API)。两者完全等价,按喜好选用。

3.1 Option Store

与 Vuex 类似,通过选项对象定义 state、getters、actions。

// stores/counter.ts
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Counter',
  }),
  getters: {
    doubleCount: (state) => state.count * 2,
    // 使用 this 访问其他 getter(需明确返回值类型,否则 TS 推导出错)
    doublePlusOne(): number {
      return this.doubleCount + 1
    },
  },
  actions: {
    increment() {
      this.count++
    },
    async fetchAndSetCount(url: string) {
      const res = await fetch(url)
      const data = await res.json()
      this.count = data.value
    },
  },
})

3.2 Setup Store

使用类似 Vue 组件 setup() 的语法,可以利用组合式函数(composables)并更自由地组织代码。

// stores/counter.ts
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const name = ref('Counter')

  const doubleCount = computed(() => count.value * 2)

  function increment() {
    count.value++
  }

  async function fetchAndSetCount(url: string) {
    const res = await fetch(url)
    const data = await res.json()
    count.value = data.value
  }

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

两种写法的选择:

  • 熟悉 Vuex / Options API 推荐 Option Store。
  • 喜欢 Composition API、要复用逻辑推荐 Setup Store。
  • Setup Store 中所有 return 出的内容都会成为 store 的公共属性和方法,不能使用私有变量了吗?可以,不 return 的变量即为私有。

4. State

4.1 访问 state

import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()

// 直接读写
console.log(counter.count)
counter.count++

通过 storeToRefs 提取响应式引用(解构时保持响应性):

import { storeToRefs } from 'pinia'
const { count, name } = storeToRefs(counter)
// count 是 Ref<number>,name 是 Ref<string>

注意: 直接解构 store 会丢失响应性(如同解构 reactive),必须使用 storeToRefs。actions 可以直接解构,因为它们是普通函数。

4.2 重置 state

Option Store 下可直接调用 $reset():

counter.$reset() // 将 state 重置为初始值

但 Setup Store 默认不支持 $reset(),因为 Pinia 无法得知如何重建初始状态。需要自定义实现:

// 在 setup store 内手动提供 $reset
const initialState = { count: 0, name: 'Counter' } // 或用函数返回
const count = ref(initialState.count)
...
function $reset() {
  count.value = initialState.count
  name.value = initialState.name
}
return { count, name, $reset, ... }

4.3 修改 state 的多种方式

  1. 直接修改(推荐)
  2. 通过 $patch 批量更新:
counter.$patch({
  count: counter.count + 1,
  name: 'New Name',
})

// 或函数形式
counter.$patch((state) => {
  state.count++
  state.name = 'New Name'
})
  1. 替换整个 state(不推荐,可能破坏响应性):
counter.$state = { count: 10, name: 'Replaced' }

5. Getters

Getters 等价于 store 的计算属性。

Option Store:

getters: {
  doubleCount(state) { return state.count * 2 },
  // 使用 this 访问其他 getter,但需要明确返回类型(TS限制)
  doublePlusOne(): number { return this.doubleCount + 1 },
  // 返回函数实现可传参的 getter(但不缓存)
  countMultipliedBy: (state) => (multiplier: number) => state.count * multiplier,
}

Setup Store:

直接使用 computed:

const doubleCount = computed(() => count.value * 2)
// 可传参
const multiplied = computed(() => (m: number) => count.value * m)

注意事项:

  • Getters 默认是缓存的。
  • 若 getter 返回函数,则函数结果不缓存(因为每次调用返回新函数)。
  • 在 Option Store 中通过 this 访问其他 getter 时,由于 TS 无法推导 this 类型,必须明确标注返回值类型。

6. Actions

Actions 相当于组件里的方法,支持同步/异步操作。在 Option Store 中通过 this 访问 state 和其他 actions。

actions: {
  async fetchData() {
    this.loading = true
    try {
      const data = await api.get('/data')
      this.data = data
    } finally {
      this.loading = false
    }
  },
  // 调用其他 action
  async init() {
    await this.fetchData()
  }
}

在 Setup Store 中,actions 就是普通函数。

调用:

const store = useCounterStore()
store.increment()

Pinia 会自动将 actions 绑定到 store 实例,所以解构出来也可以直接调用,不会丢失 this:

const { increment } = store
increment() // 正常运作

7. 在组件中使用 Store

7.1 组合式 API (Composition API)

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

const counter = useCounterStore()
const { count, doubleCount } = storeToRefs(counter)
const { increment } = counter // action 可直接解构

function handleClick() {
  increment()
}
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double: {{ doubleCount }}</p>
    <button @click="handleClick">+1</button>
  </div>
</template>

7.2 选项式 API (Options API)

在没有 setup 的场景(如纯 Options API 组件),可以通过 mapStores、mapState、mapActions 辅助函数映射。

<script lang="ts">
import { mapStores, mapState, mapActions } from 'pinia'
import { useCounterStore } from '@/stores/counter'

export default {
  computed: {
    // 将整个 store 映射进来
    ...mapStores(useCounterStore),
    // 或映射特定 state/getter
    ...mapState(useCounterStore, ['count', 'doubleCount']),
    // 支持重命名
    ...mapState(useCounterStore, {
      myCount: 'count',
      double: (store) => store.doubleCount,
    }),
  },
  methods: {
    ...mapActions(useCounterStore, ['increment']),
    ...mapActions(useCounterStore, { plusOne: 'increment' }),
  },
}
</script>

注意:

  • mapStores 会生成 counterStore 这样的属性,通过 this.counterStore.count 访问。
  • 使用 mapState 需要传递 store 的定义函数,而不是实例。
  • Option API 下 store 需要在 setup() 内手动调用一次才能激活,或者在映射前确保 store 已被使用。通过 mapStores 会自动调用。

8. Store 间相互引用

一个 store 可以导入并使用另一个 store。

Setup Store 示例:

// stores/user.ts
export const useUserStore = defineStore('user', () => {
  const name = ref('Alice')
  return { name }
})

// stores/cart.ts
import { useUserStore } from './user'

export const useCartStore = defineStore('cart', () => {
  const user = useUserStore() // 直接在 setup 内调用
  const items = ref([])

  function addItem(item) {
    items.value.push({ ...item, addedBy: user.name })
  }

  return { items, addItem }
})

Option Store 示例:

import { useUserStore } from './user'

export const useCartStore = defineStore('cart', {
  state: () => ({ items: [] }),
  actions: {
    addItem(item) {
      const user = useUserStore() // 在 action 内部调用
      this.items.push({ ...item, addedBy: user.name })
    },
  },
})

注意:

  • 可以在任何地方调用 useXxxStore(),只要在 Pinia 注册之后。
  • 若在 setup 外部(如 action 内部)调用另一个 store,完全没问题,因为此时 Pinia 一定已挂载。
  • 避免在 store 的顶层直接调用另一个 store 时出现循环依赖,需要按需在内部使用。

9. 插件系统

Pinia 插件是一个函数,接收一个 context 对象,可以在 store 上增加属性、方法,或者拦截行为。

// plugins/myPlugin.ts
import { PiniaPluginContext } from 'pinia'

export function myPiniaPlugin(context: PiniaPluginContext) {
  // context: { app, pinia, store, options }
  const { store } = context

  // 添加全局属性
  store.$myGlobal = 'Hello'

  // 监听 action
  store.$onAction(({ name, store, args, after, onError }) => {
    console.log(`Action ${name} started`)
    after(() => console.log(`Action ${name} finished`))
    onError((error) => console.error(`Action ${name} failed`, error))
  })

  // 添加持久化逻辑(简易版)
  const savedState = localStorage.getItem(store.$id)
  if (savedState) {
    store.$patch(JSON.parse(savedState))
  }
  store.$subscribe((mutation, state) => {
    localStorage.setItem(store.$id, JSON.stringify(state))
  })
}

注册插件:

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

常用插件生态:

  • pinia-plugin-persistedstate:状态持久化。
  • 自行封装日志、权限、重置插件。

10. 服务端渲染(SSR)

Pinia 为 SSR 设计了安全的状态管理方案,防止跨请求状态污染。

核心原则:

  • 每个请求都创建全新的 Pinia 实例。
  • 避免在模块顶层直接使用 useStore(),应在 setup 或请求处理函数内调用。

典型流程(如 Nuxt 3 / 手动 SSR):

// server.ts
import { createPinia } from 'pinia'
import { createSSRApp } from 'vue'

export function createApp() {
  const app = createSSRApp(App)
  const pinia = createPinia()
  app.use(pinia)
  return { app, pinia }
}

// 请求处理
async function render(url, manifest) {
  const { app, pinia } = createApp()
  // 设置当前请求的 router 等
  // 渲染前可预先填充数据
  const store = useSomeStore(pinia) // 传入 pinia 实例确保在正确实例上操作
  await store.fetchData()
  // 序列化状态
  const state = JSON.stringify(pinia.state.value)
  // 渲染并将状态注入到 HTML 中
}

在客户端激活时,替换状态:

// entry-client.ts
const { app, pinia } = createApp()
if (window.__PINIA_STATE__) {
  pinia.state.value = JSON.parse(window.__PINIA_STATE__)
}
app.mount('#app')

Nuxt 3 自动处理了这些细节,你只需定义 store,并在 setup 中调用。


11. 开发工具支持

  • Vue Devtools:支持 Pinia 状态查看、时间旅行、action 追踪。
  • 时间旅行调试:需要开启 defineStore 的第三个参数中的 history 选项(仅开发模式):
    defineStore('id', { ... }, { history: true })
    
  • Pinia 内部支持 action 追踪,插件中可通过 $onAction 监听。

12. 与 Vuex 的对比

特性PiniaVuex 4
模块化扁平化,每个 store 独立树形嵌套 modules,命名空间
mutations移除,直接 actions 修改必须有 mutations 同步修改
TypeScript完美推导,零额外代码需要复杂类型声明,推导弱
体积~1KB~10KB+
API 风格Options + Setup Store仅有 Options API
开发者工具支持时间旅行支持时间旅行
社区生态快速增长,官方维护成熟但不再新功能开发
代码结构灵活,可自由组织较死板,state/getters/mutations/actions 分离

结论: 新项目无脑选 Pinia;老项目若已用 Vuex,可渐进迁移,两者可在同一项目共存。


13. 最佳实践

  1. 按功能领域拆分 store:如 useUserStore、useCartStore、useProductStore,保持单一职责。
  2. 避免过度集中:不要把所有状态丢进一个大 store,失去了模块化优势。
  3. Setup Store 优先:更接近 Composition API,方便逻辑抽取、复用、TS 推导最好。
  4. 使用 storeToRefs 解构:保持 state/getters 的响应性,actions 直接解构。
  5. action 建议返回 Promise:方便在组件中链式调用和等待。
    actions: {
      async fetchData() { ... }
    }
    // 组件内
    await store.fetchData()
    
  6. 考虑 SSR 安全性:不要在全局作用域直接调用 useStore(),始终在函数内调用(setup、action、请求处理函数)。
  7. 持久化用插件:pinia-plugin-persistedstate 配置简单,避免手写 $subscribe 逻辑。
  8. 复杂更新优先用 $patch:一次修改多个属性更清晰,且能触发一次响应式更新。
  9. 通过 TypeScript 增强健壮性:使用接口定义 state 类型(Option Store)或利用自动推导(Setup Store)。
  10. 测试友好:可以在单元测试中创建独立的 Pinia 实例并注入,避免状态污染。

14. 常见问题 FAQ

Q:能在 Pinia 中使用 Vuex 的 mapGetters 等辅助函数吗?
A:不能直接使用 Vuex 的,但 Pinia 提供了自己的 mapState、mapActions,用法相似。

Q:如何监听 state 变化?
A:通过 store.$subscribe(callback, options),类似于 Vue 的 watch,可指定 { detached: true } 使组件销毁后仍监听。

Q:action 是否必须是异步的?
A:不必须,可以是同步函数。Pinia 去掉了 mutation 就是为了让 action 统一处理同步和异步。

Q:为什么 $reset() 在 setup store 不可用?
A:Setup Store 的状态是通过 ref/reactive 构建的,没有最初的工厂函数来自动重建状态。你可以自己实现一个 $reset 方法并导出。

Q:能否在 Pinia store 中使用 watch 或 watchEffect?
A:可以,在 Setup Store 内直接使用 Vue 的 watch,但要注意在组件卸载时 store 不会自动销毁,因此可能导致内存泄漏。需要手动停止或仅在必要时使用。

Q:如何实现 store 间的循环依赖?
A:尽量避免。如果必须,可以在 action 内部延迟引用另一个 store(在运行时调用函数),而不是在顶层导入,这样打破循环。


15. 总结

Pinia 作为 Vue 3 的首选状态管理方案,凭借简洁的 API、出色的 TypeScript 体验和灵活的模块设计,极大地提升了开发效率。掌握其两种 store 定义方式、在组件中的正确使用姿势、插件机制与 SSR 策略,就能从容应对大多数复杂应用的状态管理需求。建议动手实践,从一个小项目开始全部使用 Pinia,很快便能感受到其丝般顺滑的开发体验。

最近更新: 2026/8/11 15:57
Contributors: shyarcher
Prev
vue3学习笔记