技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTVUE

Vue 3 中 Pinia 的使用总结

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/27-vue3中pinia的使用总结
本文目录13 个章节
  1. Pinia 的简介和优势
  2. Vue 3 环境安装
  3. Pinia 的安装
  4. 创建 Store 状态管理库
  5. 在 Vue 3 组件中读取 Store 数据
  6. 解构 Store 时使用 storeToRefs
  7. Pinia 修改状态数据的方式
  8. Store 之间相互调用
  9. Store 的重置、订阅和插件
  10. SSR 注意事项
  11. 小结
  12. 官方参考
  13. 原文出处

Vue 3 中 Pinia 的使用总结

本文原文写于 2022 年,主要介绍 Pinia 的基础用法。下面尽量保留原文的学习路径和示例,同时补充 Vue 3.5.x、Pinia 2/3 的版本边界、Setup Store、SSR、插件和当前状态管理建议。

文中的“Pinia 比 Vuex 简洁”“Pinia 是 Vuex 的后继方向”等属于作者当时的生态观点,不是 API 保证。Pinia 3 及更高版本面向 Vue 3;维护 Vue 2.7 的旧项目时应按兼容矩阵固定 Pinia 2,不要只执行不带版本约束的安装命令。

Pinia 的简介和优势

Pinia 是 Vue 生态中的状态管理库,用来在多个组件或页面之间共享状态。它由 Vuex 的维护者参与开发,吸收了 Composition API 的思想,也是 Vue 官方当前推荐的新项目状态管理方案之一。

相较于 Vuex 3/4,Pinia 常见的优势包括:

  1. API 更直接:没有强制的 mutations 层,state 可以直接修改,也可以通过 actions 集中处理业务逻辑。
  2. Store 更扁平:不需要 Vuex 风格的嵌套 modules 和 namespaced 字符串。
  3. TypeScript 推导更自然defineStore、state、getters 和 actions 都能获得较好的类型推导。
  4. Composition API 友好:既可以使用 Options Store,也可以使用 Setup Store。
  5. 开发工具和生态完善:支持 Devtools、插件、SSR、测试工具和热更新。
  6. 按 Store 拆分代码:每个 Store 可以放在独立文件中,构建工具也可以按模块组织代码。

Pinia 不等于“所有项目都必须使用的全局变量容器”。局部状态优先放在组件或 composable 中;只有多个组件真正共享的状态才需要放入 Store。

Vue 2.7 与 Vue 3 的版本说明

  • Vue 3.5.x:本文示例按 Pinia 3 的 Vue 3 API 编写。实际安装时请根据项目 lockfile 固定兼容版本。
  • Vue 2.7:旧项目可以使用 Pinia 2;Pinia 3 不支持 Vue 2。
  • Vue 2.6 及更早版本:还需要额外的 Composition API 兼容方案,不建议新项目采用。
  • Vue 2 已于 2023 年 12 月 31 日结束维护;Pinia 2 的 Vue 2 支持只适合作为迁移期或旧项目方案。

Vue 3 环境安装

如果需要新建 Vue 3 项目,可以使用 Vite:

pnpm create vite my-vue-app --template vue-ts
cd my-vue-app
pnpm install

原文中的命令截图保留如下:

原文 Vite 初始化命令截图(本地化)

原文安装依赖提示截图(本地化)

如果项目已经存在,直接安装 Pinia。Vue 3 项目示例:

pnpm add pinia

如果要明确使用 Pinia 3,可以在项目升级策略允许时写成:

pnpm add pinia@^3

Vue 2.7 项目应使用与项目 Vue 版本匹配的 Pinia 2:

pnpm add pinia@^2

不要只根据文章截图中的版本号判断当前版本;应以 package.json、lockfile 和官方 peer dependencies 为准。

原文 Pinia 安装命令截图(本地化)

可以在 package.json 或包管理器的依赖树中查看实际安装的版本。

Pinia 的安装

src/main.ts 中创建 Pinia 实例,并在挂载应用前安装:

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

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

app.use(pinia)
app.mount('#app')

createPinia() 返回的是当前应用的 Pinia 实例。一个页面中可以有多个 Vue app,但每个 app 都应安装自己对应的 Pinia 实例。

原文 Pinia 入口代码截图(本地化)

创建 Store 状态管理库

通常在 src/stores/ 下为每个业务域创建一个文件,例如 src/stores/counter.ts

原文 defineStore 代码截图(本地化)

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

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Vue',
    items: [] as string[],
  }),
  getters: {
    doubleCount: state => state.count * 2,
    itemCount(): number {
      return this.items.length
    },
  },
  actions: {
    increment() {
      this.count++
    },
    async reload() {
      // 可以在这里请求接口,再更新 this.items
    },
  },
})

defineStore() 的第一个参数是 Store 的唯一 id,用于 Devtools、插件和 Pinia 内部识别。返回值通常命名为 useCounterStore,它是一个在组件中调用后才创建/取得 Store 实例的函数。

Options Store 的三个主要选项可以这样理解:

  • state:类似组件的 data,必须返回初始状态对象。需要使用的状态字段应在这里预先声明,即使初值是 undefined
  • getters:类似 computed,用于派生数据,不是 watch 监听器。
  • actions:类似 methods,用于封装同步或异步业务逻辑,可以通过 this 访问 Store。

原文 defineStore 示例截图(本地化)

Setup Store 写法

Pinia 也支持与 Composition API 类似的 Setup Store:

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

export const useUserStore = defineStore('user', () => {
  const token = ref<string | null>(null)
  const isLoggedIn = computed(() => token.value !== null)

  function setToken(value: string | null) {
    token.value = value
  }

  return {
    token,
    isLoggedIn,
    setToken,
  }
})

Setup Store 中:

  • 返回的 ref 会成为 state;
  • 返回的 computed 会成为 getter;
  • 返回的函数会成为 action;
  • 所有希望被 Pinia、Devtools、插件和 SSR 识别的状态都必须返回,不能隐藏一部分私有 state。

Setup Store 更灵活,可以在 Store 中使用 composable 或同步创建 watcher;但 SSR 场景也需要更仔细地处理浏览器 API和请求级状态。

在 Vue 3 组件中读取 Store 数据

<script setup> 中调用 Store 函数得到实例:

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

const counterStore = useCounterStore()
</script>

<template>
  <p>count:{{ counterStore.count }}</p>
  <p>double:{{ counterStore.doubleCount }}</p>
  <button @click="counterStore.increment()">增加</button>
</template>

Store 实例本身被 Vue 的 reactive() 包装,因此模板和脚本中直接访问 counterStore.count 不需要 .value

原文的多个“读取 Store”截图已本地化:

原文读取 Store 数据截图(本地化)

原文组件使用 Store 截图(本地化)

原文模板读取 Store 截图(本地化)

解构 Store 时使用 storeToRefs

直接解构 Store 的 state 或 getter 会丢失响应式连接:

const store = useCounterStore()

// 不推荐:取出的 count 不会随着 store.count 更新
const { count, doubleCount } = store

正确做法是使用 storeToRefs()

import { storeToRefs } from 'pinia'

const store = useCounterStore()
const { count, doubleCount } = storeToRefs(store)

// action 可以直接解构,因为 Pinia 会绑定它的 this
const { increment } = store

storeToRefs() 会为 state、getter 以及插件加入的响应式属性创建 refs;普通方法和非响应式属性不会被转换。

原文直接解构 Store 截图(本地化)

原文解构后响应式失效截图(本地化)

原文 storeToRefs 引入截图(本地化)

原文 storeToRefs 使用截图(本地化)

Pinia 修改状态数据的方式

1. 直接修改

Pinia 允许直接修改已声明的 state:

const store = useCounterStore()
store.count++
store.name = 'Pinia'

这不是绕过响应式系统的修改,Vue 和 Pinia Devtools 仍然可以追踪它。不能随意增加没有在 state() 中声明的字段。

原文直接修改状态截图(本地化)

2. 使用 $patch 对象

一次修改多个字段时,可以使用对象形式:

store.$patch({
  count: 10,
  name: 'Pinia',
})

3. 使用 $patch 函数

数组、对象等复杂修改可以使用函数形式:

store.$patch(state => {
  state.items.push('new item')
  state.count++
})

$patch() 的主要价值是把一组修改作为一个 Devtools 记录,便于追踪和时间旅行;官方没有保证它“一定更快”,不要把 $patch 当成无条件的性能优化。

原文 $patch 对象形式截图(本地化)

原文 $patch 函数形式截图(本地化)

4. 在 action 中集中处理复杂逻辑

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
  actions: {
    async incrementAfterRequest() {
      await Promise.resolve()
      this.count++
    },
  },
})

组件只负责调用 action:

const store = useCounterStore()
await store.incrementAfterRequest()

Action 可以接收任意参数、返回普通值或 Promise,也可以调用其他 Store:

const authStore = useAuthStore()
const settingsStore = useSettingsStore()

if (authStore.isLoggedIn) {
  await settingsStore.load()
}

使用普通函数 action 时不要改成箭头函数,因为 Options Store 的 action 需要通过 this 访问 Store。

原文 action 定义截图(本地化)

原文组件调用 action 截图(本地化)

5. 使用 Getters

Getter 类似 computed:

export const useUserStore = defineStore('user', {
  state: () => ({
    phone: '13812345678',
  }),
  getters: {
    hiddenPhone: state => {
      return state.phone.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2')
    },
  },
})

Getter 可以在模板中直接访问:

<script setup>
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()
</script>

<template>
  <p>{{ userStore.hiddenPhone }}</p>
</template>

Getter 的结果具有 computed 的缓存语义;如果 getter 返回一个函数来接收参数,则这个函数调用本身不会自动缓存每个参数的结果。

原文 getter 定义截图(本地化)

原文 getter 使用截图(本地化)

6. action 和 getter 中的 this

Options Store 的 action 可以使用 this 访问整个 Store:

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [] as { price: number }[],
  }),
  getters: {
    total(): number {
      return this.items.reduce((sum, item) => sum + item.price, 0)
    },
  },
  actions: {
    clear() {
      this.items = []
    },
  },
})

如果 TypeScript getter 通过 this 访问其他 getter,通常需要显式声明返回类型。原文截图中的 String 应改为小写的 string,避免把 JavaScript 包装对象类型带入业务代码。

原文 getter 中使用 this 截图(本地化)

Store 之间相互调用

在一个 Store 中直接调用另一个 Store:

// stores/settings.ts
import { defineStore } from 'pinia'
import { useAuthStore } from './auth'

export const useSettingsStore = defineStore('settings', {
  state: () => ({
    theme: 'light',
  }),
  actions: {
    async loadForCurrentUser() {
      const auth = useAuthStore()
      if (!auth.isLoggedIn) return
      // 根据当前用户加载设置
    },
  },
})

要注意循环依赖:如果两个 Store 在模块顶层就互相调用,而不是在 action/getter 执行时调用,可能造成初始化顺序问题。

原文 Store 互相调用截图(本地化)

Store 的重置、订阅和插件

$reset

Options Store 自带 $reset()

const store = useCounterStore()
store.$reset()

Setup Store 需要自己实现:

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

$subscribe$onAction

store.$subscribe((mutation, state) => {
  console.log(mutation.type, state)
})

const unsubscribe = store.$onAction(({ name, after, onError }) => {
  console.log('开始 action:', name)
  after(result => console.log('完成:', result))
  onError(error => console.error(error))
})

unsubscribe()

在组件 setup() 中同步注册的订阅通常会随组件卸载自动清理;需要脱离组件生命周期时,应使用官方提供的 detached 选项或手动取消。

Pinia 插件

插件通过 Pinia 实例安装,而不是通过 app.use() 直接传插件:

const pinia = createPinia()

pinia.use(({ store, app, options }) => {
  // 可以给 Store 增加方法或属性
  return {
    createdAt: new Date(),
  }
})

插件应在 Store 创建前注册。注入 router、第三方实例等非响应式对象时,可以使用 markRaw(),避免不必要的代理。

SSR 注意事项

Pinia 支持 SSR,但服务端必须避免多个请求共享同一个可变 Store:

  • SSR 应为每个请求创建对应的 app 和 Pinia 实例。
  • 在组件外使用 Store(例如 router 守卫)时,显式传入当前 pinia 实例:useStore(pinia)
  • 服务端 state 注入 HTML 前要安全序列化,防止 XSS;客户端应在首次使用 Store 前完成 hydration。
  • localStoragewindowdocument 只存在于浏览器环境。不要在 action 的模块顶层或 SSR 路径无条件访问 localStorage

小结

Pinia 的核心使用流程是:

  1. 创建 app 和 Pinia,并在 app 上安装。
  2. 使用唯一 id 定义 Store。
  3. 在 state 中声明初始状态。
  4. 用 getters 派生数据,用 actions 封装业务逻辑。
  5. 组件中保留 Store 实例,或用 storeToRefs 解构响应式 state/getter。
  6. 需要持久化、插件或 SSR 时,按官方 API 处理生命周期和安全边界。

官方参考

原文出处

原文参考地址:https://i.cnblogs.com/posts/edit

原链接是博客后台编辑地址,不是稳定的公开文章地址;本文保留该出处信息,但当前 API 以 Pinia 和 Vue 官方文档为准。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS