Vue3中Pinia

Vue 3 中 Pinia 完全指南

本文档从零开始讲解 Vue 3 的状态管理库 Pinia:什么是 Pinia、为什么需要它、如何安装与注册、如何定义 store(选项式与组合式两种写法)、如何在组件里使用、state/getters/actions 的用法,以及常见场景示例,适合新手系统学习。


一、什么是 Pinia?为什么需要它?

1.1 状态管理是什么?

页面上很多数据会被多个组件共用,例如:当前登录用户、购物车列表、主题(亮/暗)。
若只用 propsemit,就要一层层往下传、一层层往上发,非常麻烦。
状态管理就是:把这类“全局/共享”的数据放在一个统一的地方(store),谁需要谁就去读/改,不用层层传递。

1.2 Pinia 是什么?

Pinia 是 Vue 官方推荐的状态管理库(Vue 3 时代用来替代 Vuex)。
你可以把“共享状态”放在 store 里,在任意组件里引入 store,直接读写 state、调用 actions,非常直观。

1.3 Pinia 的特点(为什么选它)

  • API 简单:没有 mutations,直接改 state 或通过 actions 改。
  • 对 TypeScript 友好
  • 模块化:每个 store 独立定义,按需引入。
  • 与 Vue 3 组合式 API 契合:可以用 refcomputed 的写法定义 store。

下面从安装开始,到定义 store、在组件里使用,一步步说明。


二、安装与注册

2.1 安装

在项目根目录执行:

npm install pinia

或:

yarn add pinia
pnpm add pinia

2.2 在 main.js 里注册

创建 Pinia 实例,并用 app.use(pinia) 挂到 Vue 应用上,这样整个应用都能用 store。

// main.js
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')

之后就可以在项目里 defineStore 定义 store,并在组件里 useXxxStore() 使用。


三、定义第一个 Store

3.1 defineStore 与 store 的“名字”

defineStore 定义一个 store,需要传一个唯一 id(字符串)和配置(选项式或组合式)。
习惯上 id 和文件名一致,如 stores/counter.js 里定义 ‘counter’

3.2 选项式写法(state + getters + actions)

和 Vue 的选项式 API 类似:state 是数据,getters 是派生数据(像计算属性),actions 是方法(可异步)。

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

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  getters: {
    double: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++
    },
    add(n) {
      this.count += n
    }
  }
})
  • state 必须是函数,返回一个对象(和组件的 data 一样)。
  • getters 里用 state 做只读计算;若需要传参,可返回一个函数(见后文)。
  • actions 里用 this 访问 stategetters其他 actions

3.3 在组件里使用(选项式 store)

在组件里 import 并调用 useCounterStore(),得到 store 实例;然后可以:

  • store.count 读 state;
  • store.double 读 getter;
  • store.increment() 调 action。
<template>
  <div>
    <p>{{ store.count }}</p>
    <p>双倍:{{ store.double }}</p>
    <button @click="store.increment()">+1</button>
    <button @click="store.add(10)">+10</button>
  </div>
</template>

<script setup>
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
</script>
  • useCounterStore() 是 Pinia 根据 defineStore 自动生成的;每次调用返回同一个 store 实例(单例)。
  • 直接改 store.count 也可以触发更新(Pinia 不强制通过 action 改),但复杂逻辑建议放在 actions 里。

四、State(状态)

4.1 state 必须是函数

state 要写成函数,返回初始对象,这样每个 store 实例不会共享同一引用:

state: () => ({
  count: 0,
  list: []
})

4.2 直接改 state

在组件里可以直接改 store.xxx

const store = useCounterStore()
store.count++
store.list.push(1)

4.3 一次性改多个:$patch

$patch 传一个对象,一次性改多个字段:

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

或传一个函数,在函数里改 state

store.$patch((state) => {
  state.count++
  state.list.push(1)
})

4.4 恢复初始:$reset

$reset() 会把 state 恢复成 defineStorestate() 返回的初始值(仅选项式 store 支持):

store.$reset()

五、Getters(相当于计算属性)

5.1 基本写法

getters 接收 state 作为第一个参数,可以访问 this(即当前 store),返回派生数据;在组件里当只读用,不用括号(除非返回的是函数)。

getters: {
  double: (state) => state.count * 2,
  triple() {
    return this.count * 3
  }
}

5.2 使用其它 getter

在 getter 里可以用 this.其他Getter

getters: {
  double: (state) => state.count * 2,
  quadruple() {
    return this.double * 2
  }
}

5.3 带参数的 getter(返回函数)

需要“传参”时,让 getter 返回一个函数,在组件里调用这个函数并传参:

getters: {
  getItemById: (state) => (id) => {
    return state.list.find((item) => item.id === id)
  }
}

组件里:store.getItemById(1)


六、Actions(方法,可异步)

6.1 基本写法

actions 里用 this 访问 stategetters其他 actions;可以是 async,适合请求接口。

actions: {
  async fetchUser() {
    this.loading = true
    try {
      const res = await fetch('/api/user')
      this.user = await res.json()
    } finally {
      this.loading = false
    }
  }
}

6.2 调用其它 action

actions: {
  async fetchAndReset() {
    await this.fetchUser()
    this.someFlag = false
  }
}

七、组合式写法(setup 风格)的 Store

defineStore 的第二种写法:第二个参数传一个 setup 函数,函数里用 refcomputedfunction 定义 state、getters、actions,最后 return 出去。
和组件的 风格一致。

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

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const double = computed(() => count.value * 2)
  function increment() {
    count.value++
  }
  function add(n) {
    count.value += n
  }
  return { count, double, increment, add }
})
  • ref 相当于 state;computed 相当于 getters;function 相当于 actions。
  • 在组件里用法一样:useCounterStore()store.countstore.doublestore.increment()
  • setup 风格的 store 没有 $reset(没有“选项式 state 的初始值”概念),需要自己写一个 action 把各 ref 设回初值。

八、在组件里保持响应式:storeToRefs

若在组件里解构 store:

const store = useCounterStore()
const { count, double } = store

countdouble失去响应式(变成普通值/引用)。
要用 storeToRefs 解构 state 和 getters,解构出来的才是 ref,保持响应式;actions 直接解构即可(函数不需要响应式)。

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

const store = useCounterStore()
const { count, double } = storeToRefs(store)
const { increment } = store

模板里用 countdoubleincrement 即可。
不解构、一直用 store.countstore.double 也可以,不会丢响应式。


九、多个 Store 与在 Store 里用另一个 Store

9.1 定义多个 store

按功能拆成多个文件,例如 stores/user.jsstores/cart.js,各自 defineStore 不同 id。

9.2 在一个 store 里用另一个 store

action(或 setup 风格里的函数)里调用 useXxxStore() 即可:

// stores/cart.js
import { defineStore } from 'pinia'
import { useUserStore } from './user'

export const useCartStore = defineStore('cart', {
  state: () => ({ items: [] }),
  actions: {
    checkout() {
      const userStore = useUserStore()
      if (!userStore.isLoggedIn) {
        alert('请先登录')
        return
      }
      // ...
    }
  }
})

注意useUserStore() 要在 action 或 setup 函数执行时调用,不要在最顶层调用(否则可能 pinia 还未安装)。


十、完整示例

10.1 用户 Store(登录状态、用户信息)

// stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    user: null,
    token: ''
  }),
  getters: {
    isLoggedIn: (state) => !!state.token,
    userName: (state) => state.user?.name ?? ''
  },
  actions: {
    setToken(token) {
      this.token = token
    },
    setUser(user) {
      this.user = user
    },
    logout() {
      this.user = null
      this.token = ''
    }
  }
})

组件里:const userStore = useUserStore(),用 userStore.isLoggedInuserStore.logout() 等。

10.2 购物车 Store(列表、总价、增删)

// stores/cart.js
import { defineStore } from 'pinia'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: []
  }),
  getters: {
    total: (state) => state.items.reduce((sum, item) => sum + item.price * item.qty, 0),
    count: (state) => state.items.reduce((sum, item) => sum + item.qty, 0)
  },
  actions: {
    add(item) {
      const found = this.items.find((i) => i.id === item.id)
      if (found) found.qty++
      else this.items.push({ ...item, qty: 1 })
    },
    remove(id) {
      this.items = this.items.filter((i) => i.id !== id)
    }
  }
})

十一、持久化(可选)

若希望刷新页面后 state 不丢,可以用 pinia-plugin-persistedstate 等插件,把指定 store 的 state 存到 localStorage
安装后:

import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

defineStore 里加 persist: truepersist: { key: ‘xxx’, storage: localStorage } 等配置(具体看插件文档)。
新手可先不用,先把 store 的读写在内存里练熟。


十二、常见问题与注意点

12.1 解构 store 要用 storeToRefs

直接 const { count } = store 会丢响应式;对 state/gettersstoreToRefs(store) 再解构,actions 可直接解构。

12.2 在 store 里用其它 store 要在 action/setup 里调 useXxxStore

不要在 store 文件的最顶层调用 useUserStore()(可能 pinia 未挂载);在 actionsetup 函数里调用就没问题。

12.3 state 必须是函数

state: () => ({ … }),不要写成 state: { … },否则多个地方用同一个 store 时可能共享同一引用。


十三、Pinia 速查表

内容 说明
定义 store defineStore(‘id’, { state, getters, actions })defineStore(‘id’, () => { … })
使用 store const store = useXxxStore()
读 state store.xxx
改 state 直接 store.xxx = 值store.$patch({ … })
getters store.getterName,带参时 store.getterName(参数)
actions store.actionName()
解构保持响应式 storeToRefs(store) 解构 state/getters,actions 直接解构
恢复初始 state 选项式 store:store.$reset()

十四、学习建议

  1. 先会选项式一个 store:state + getters + actions,在组件里 useXxxStore() 读和调。
  2. 需要解构时用 storeToRefs;多个 store 时在 action 里 useOtherStore()
  3. 再试组合式写法(setup 风格),和 ref/computed/function 对应起来。
  4. 持久化、插件等用到再查文档即可。

把本文档里的 counterusercart 三个示例在项目里敲一遍,会掌握得更牢。祝你学习顺利。

发表评论