Pinia 基础
Pinia 是 Vue 官方推荐的状态管理工具。
它的作用是把多个组件都需要访问的数据集中管理起来,让状态、派生状态和修改逻辑有一个清晰的位置。
如果只是一个组件内部自己使用的数据,直接放在组件里就可以。但如果数据需要在多个页面、多个组件之间共享,Pinia 就会变得很有用。
这篇笔记主要整理:
- 为什么需要 Pinia。
- Store 是什么。
- 如何安装和注册 Pinia。
- 如何定义和使用 Store。
state、getters、actions分别做什么。- 为什么解构 Store 时要使用
storeToRefs。
为什么要使用 Pinia
Section titled “为什么要使用 Pinia”在 Vue 项目中,组件之间共享数据是很常见的需求。
比如:
- 导航栏要展示当前登录用户信息。
- 多个页面都要读取购物车数量。
- 权限信息需要在路由、按钮、页面中复用。
- 多步骤表单切换页面后仍然要保留数据。
如果这些数据只靠父子组件传递,会越来越麻烦。组件层级一深,数据传递路径就会变长,维护成本也会变高。
Pinia 的价值就是把这些“跨组件共享的数据”放到统一的 Store 中。
它还能带来一些开发体验上的好处:
- 支持 Vue Devtools 调试。
- 可以查看状态变化和 action 调用。
- 支持热更新,开发时修改 Store 不需要丢失已有状态。
- 对 TypeScript 支持比较友好。
- 支持插件扩展。
- 支持服务端渲染场景。
简单说,Pinia 不是为了替代组件本地状态,而是用来管理真正需要共享的应用状态。
什么是 Store
Section titled “什么是 Store”Store 可以理解为一个独立于组件树之外的状态容器。
它有点像一个一直存在的组件,任何需要它的组件都可以读取它,也可以通过它提供的方法修改状态。
Pinia 中 Store 主要包含三个概念:
| Pinia 概念 | 组件中的类比 | 作用 |
|---|---|---|
state | data | 存放状态数据 |
getters | computed | 根据状态计算派生数据 |
actions | methods | 修改状态,处理同步或异步逻辑 |
比如一个计数器 Store:
count是状态。doubleCount是根据count计算出来的派生值。increment是修改count的方法。
什么时候应该使用 Store
Section titled “什么时候应该使用 Store”适合放进 Pinia 的数据通常有一个特点:多个地方都要用,或者需要跨页面保留。
适合放进 Store:
- 用户信息。
- 登录状态。
- 权限信息。
- 购物车数据。
- 多页面共享的筛选条件。
- 复杂多步骤表单数据。
不太适合放进 Store:
- 某个弹窗是否打开。
- 某个按钮是否 loading。
- 某个局部输入框的临时值。
- 只在当前组件内部使用的数据。
判断标准可以简单一点:如果这个数据只属于当前组件,就放在当前组件里;如果多个组件都需要它,再考虑放进 Pinia。
安装 Pinia
Section titled “安装 Pinia”使用 npm:
npm install pinia使用 yarn:
yarn add pinia使用 pnpm:
pnpm add pinia注册 Pinia
Section titled “注册 Pinia”安装完成后,需要在入口文件中创建 Pinia 实例,并通过 app.use() 注册到 Vue 应用中。
import { createApp } from 'vue'import { createPinia } from 'pinia'import App from './App.vue'
const app = createApp(App)
app.use(createPinia())app.mount('#app')注册完成后,组件中就可以使用 Store 了。
定义 Store
Section titled “定义 Store”Pinia 使用 defineStore() 定义 Store。
defineStore() 的第一个参数是 Store 的唯一 ID,第二个参数可以是对象,也可以是一个函数。
Option Store 写法
Section titled “Option Store 写法”Option Store 和 Vue Options API 的风格比较接近。
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', { state: () => ({ count: 0, }), getters: { doubleCount: (state) => state.count * 2, }, actions: { increment() { this.count++ }, },})这种写法很直观:
state返回状态对象。getters定义计算属性。actions定义修改状态的方法。
在 actions 中可以通过 this 访问当前 Store。
Setup Store 写法
Section titled “Setup Store 写法”Setup Store 更接近 Vue Composition API。
import { computed, ref } from 'vue'import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => { const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() { count.value++ }
return { count, doubleCount, increment, }})在 Setup Store 中:
ref()定义的数据会变成state。computed()定义的数据会变成getters。- 普通函数会变成
actions。
如果项目主要使用 <script setup> 和 Composition API,Setup Store 会更自然。
需要注意:Store 中希望暴露给外部使用的状态、计算属性和方法,都要在最后 return 出去。
在组件中使用 Store
Section titled “在组件中使用 Store”定义好 Store 后,在组件中导入并执行对应的 use...Store() 函数即可。
<script setup lang="ts">import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()</script>
<template> <button @click="counterStore.increment"> {{ counterStore.count }} </button>
<p>双倍计数:{{ counterStore.doubleCount }}</p></template>这里有两个细节:
useCounterStore()执行后会得到 Store 实例。- 在模板中读取 Store 状态时,不需要写
.value。
Pinia 会帮你处理响应式访问。
一个更完整的示例
Section titled “一个更完整的示例”下面把计数、计算属性、异步请求放在同一个 Store 中。
import { computed, ref } from 'vue'import { defineStore } from 'pinia'import axios from 'axios'
type User = { id: number name: string role: string}
const API_URL = 'http://127.0.0.1:9090/list'
export const useUserStore = defineStore('user', () => { const count = ref(0) const users = ref<User[]>([]) const loading = ref(false)
const doubleCount = computed(() => count.value * 2) const userCount = computed(() => users.value.length)
function increment() { count.value++ }
async function getUsers() { loading.value = true
try { const response = await axios.get<User[]>(API_URL) users.value = response.data } finally { loading.value = false } }
return { count, users, loading, doubleCount, userCount, increment, getUsers, }})这个 Store 中:
count、users、loading是状态。doubleCount、userCount是 getter。increment、getUsers是 action。getUsers是异步 action,可以请求接口并更新状态。
在组件中调用异步 action
Section titled “在组件中调用异步 action”<script setup lang="ts">import { onMounted } from 'vue'import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
onMounted(() => { userStore.getUsers()})</script>
<template> <button @click="userStore.increment"> 计数:{{ userStore.count }} </button>
<p>双倍计数:{{ userStore.doubleCount }}</p> <p>用户数量:{{ userStore.userCount }}</p>
<p v-if="userStore.loading">加载中...</p>
<table v-else> <thead> <tr> <th>ID</th> <th>姓名</th> <th>角色</th> </tr> </thead> <tbody> <tr v-for="item in userStore.users" :key="item.id"> <td>{{ item.id }}</td> <td>{{ item.name }}</td> <td>{{ item.role }}</td> </tr> </tbody> </table></template>组件只关心“什么时候调用 action”和“如何展示状态”,真正的数据请求和状态修改放在 Store 中。
这样组件会更干净,业务逻辑也更容易复用。
actions 可以同步也可以异步
Section titled “actions 可以同步也可以异步”Pinia 不需要像 Vuex 那样区分 mutation 和 action。
在 Pinia 中,actions 既可以写同步逻辑,也可以写异步逻辑。
function increment() { count.value++}
async function fetchList() { const response = await axios.get('/api/list') list.value = response.data}这也是 Pinia 比 Vuex 更轻的一点:概念更少,写法更直接。
getters 如何实现
Section titled “getters 如何实现”在 Setup Store 中,getter 通常使用 computed() 实现。
const count = ref(0)
const doubleCount = computed(() => count.value * 2)在组件中使用时:
<template> <p>{{ counterStore.doubleCount }}</p></template>模板里不需要写 doubleCount.value。
storeToRefs 的作用
Section titled “storeToRefs 的作用”使用 Store 时,一个常见问题是解构导致响应式丢失。
下面这种写法不推荐:
<script setup lang="ts">import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
const { count, doubleCount } = counterStore</script>原因是 Store 本身是响应式对象,直接解构普通属性会破坏响应式连接。
如果确实想解构状态和 getter,应该使用 storeToRefs()。
<script setup lang="ts">import { storeToRefs } from 'pinia'import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
const { count, doubleCount } = storeToRefs(counterStore)const { increment } = counterStore</script>
<template> <button @click="increment"> {{ count }} </button>
<p>{{ doubleCount }}</p></template>这里的规则很重要:
- 状态和 getter 使用
storeToRefs()解构。 - action 可以直接从 Store 中解构。
因为 action 本身已经和 Store 绑定,不需要转换成 ref。
直接使用 Store 也可以
Section titled “直接使用 Store 也可以”并不是所有场景都必须解构。
如果模板里使用的状态不多,直接使用 Store 反而更清晰:
<script setup lang="ts">import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()</script>
<template> <button @click="counterStore.increment"> {{ counterStore.count }} </button></template>这也是我更推荐的入门写法。
等页面里用到的字段变多,再考虑用 storeToRefs() 做解构。
Pinia 和组件状态的边界
Section titled “Pinia 和组件状态的边界”Pinia 很方便,但不要把所有状态都塞进 Store。
例如一个列表页里:
- 当前用户信息:适合放 Store。
- 当前页面的搜索关键词:看是否需要跨页面保留。
- 一个筛选弹窗是否打开:通常放组件本地。
- 表格当前展开哪一行:通常放组件本地。
Store 应该服务于共享状态,而不是把组件内部的临时交互全部搬出去。
如果 Store 变成所有状态的杂货箱,后期维护会很难。
Pinia 是用来做什么的
Section titled “Pinia 是用来做什么的”Pinia 是 Vue 的集中状态管理工具,常用于跨组件、跨页面共享状态。
可以把它理解为新一代 Vuex,但写法更轻,类型推导更好,也不再需要手写 mutation。
Pinia 还需要 mutation 吗
Section titled “Pinia 还需要 mutation 吗”不需要。
Pinia 中通过 actions 修改状态。actions 同时支持同步和异步逻辑。
Pinia 如何实现 getter
Section titled “Pinia 如何实现 getter”如果使用 Setup Store,通常使用 computed()。
如果使用 Option Store,则写在 getters 选项中。
Pinia 的 Store 解构后如何保持响应式
Section titled “Pinia 的 Store 解构后如何保持响应式”状态和 getter 使用 storeToRefs()。
action 可以直接解构。
const { count, doubleCount } = storeToRefs(counterStore)const { increment } = counterStoreStore 文件应该怎么命名
Section titled “Store 文件应该怎么命名”一般放在 src/stores 目录下,并按业务模块拆分。
例如:
src/ stores/ user.ts cart.ts counter.ts命名上通常使用 useXxxStore:
export const useUserStore = defineStore('user', () => {})export const useCartStore = defineStore('cart', () => {})这样和 Vue 组合式函数的命名习惯保持一致。
Pinia 的核心并不复杂。
入门时先记住四句话就够了:
defineStore()用来定义 Store。ref()是状态,computed()是 getter,函数是 action。- 组件中执行
useXxxStore()得到 Store 实例。 - 解构状态和 getter 时使用
storeToRefs()。
真正写项目时,最重要的是判断哪些数据值得进入 Store。只要这个边界判断清楚,Pinia 会让 Vue 项目的状态管理轻很多。