Vue3 状态管理 + Pinia
Vue3 状态管理 + Pinia
- 1. 什么是状态管理?
- 2. 多个组件共享一个共同状态导致的问题
- 3. 解决状态共享问题的方案
- 3.1 用响应式 API 做简单状态管理(这里用的是 reactive)
- 3.2 用组合式函数返回全局状态(使用直接看 3.2.2)
- 3.2.1 错误用法(在模板中使用组合式函数)
- 3.2.2 正确用法(在 script 中使用组合式函数)
- 4. SSR 相关细节
- 5. Pinia(状态管理库,可以认为是 Vuex5)
- 5.1 Pinia 简介
- 5.2 快速入门(下载、挂载、创建数据仓库、使用)
- 5.3 使用细节(核心概念)
- 5.3.1 定义 Store
- 5.3.1.1 命名规范(函数使用use...Store格式)
- 5.3.1.2 Option Store 或者 Setup Store(都行,哪个舒服用哪个)
- 5.3.1.2.1 Option Store(选项式仓库,更简单直观)
- 5.3.1.2.2 SetupStore(组合式仓库,更灵活强大)
- 5.3.1.3 使用 Store(state 和 getter 不能直接解构)
- 5.3.1.4 Store 的解构(action 可以直接解构,state 和 getter 需要借助 storeToRefs() 保持响应性)
- 5.3.2 State(状态,相当于 data)
- 5.3.2.1 定义 state
- 5.3.2.2 访问 state(最好都在 state 中定义初始字段)
- 5.3.2.3 重置 state(Option Store 有 $reset() 方法,Setup Store 需要自定义)
- 5.3.2.4 mapState 和 mapWritableState(组件中 Option Store 的 state 属性的映射方法)
- 5.3.2.3 变更 state($patch 方法)
- 5.3.2.4 替换 state(其实还是用 $patch 方法去变更)
- 5.3.2.5 订阅 State(使用 $subscribe() 方法侦听 state 及其变化,通常配合持久化存储)
- 5.3.3 Getter(state 的计算值,相当于 computed)
- 5.3.3.1 定义和使用 getter(state 是第一个参数)
- 5.3.3.2 访问其他 getter(通过this)
- 5.3.3.3 向 getter 传递参数(失去缓存效果,变成函数了)
- 5.3.3.4 访问其他 store 的 getter
- 5.3.4 Action(方法,相当于 method)
- 5.3.4.1 基本使用方式
- 5.3.4.2 异步 action(其实就是执行请求)
- 5.3.4.3 访问其他 store 的 action
- 5.3.4.4 mapActions(映射方法,起别名)
- 5.3.4.5 订阅 action
- 5.3.5 插件
- 5.3.5.1 快速入门
- 5.3.5.2 扩展 Store(3 种写法)
- 5.3.5.3 添加新的 state
- 5.3.5.4 重置插件中添加的 state
- 5.3.5.5 添加新的外部属性
- 5.3.5.6 在插件中调用 $subscribe(监听 store 及其 action 的变化)
- 5.3.5.7 添加新的选项(根据选项,判断是否对数据或者行为进行二次处理,比如防抖)
- 5.3.6 组件外的 Store
1. 什么是状态管理?
理论上来说,每一个组件实例都会管理自己数据的响应式状态。比如一个简单的计数器组件:
<script setup>
import { ref } from 'vue'// 状态
const count = ref(0)// 动作
function increment() {count.value++
}
</script><!-- 视图 -->
<template>{{ count }}</template>
每个组件实例,都是一个独立的单元,包括:
- 状态:驱动整个应用的数据源;
- 视图:对状态的一种声明式映射;
- 交互:状态根据用户在视图中的输入而作出相应变更的可能方式。
单向数据流的简单图示:

2. 多个组件共享一个共同状态导致的问题
当只有一个组件时,状态管理十分简单。但是,如果多个组件共享一个状态时,问题就变得复杂了起来。主要有两种情况:
(1)多个视图都依赖同一个状态;
(2)不同视图的交互,需要修改同一个状态。
第(1)种情况,我们可以将共享状态提升到共同的祖先上,然后通过 prop 进行数据传递。但是,如果层级嵌套过深,也会带来 prop 逐级透传问题。
虽然,可以使用 provide 和 inject 去解决这个问题,但是代码也会十分冗余,并且需要记忆数据(共享状态)来自于哪一个组件,这明显是不合理的。
第(2)种情况,我们经常会通过模板获得父/子实例,然后通过触发事件的方式去更改。但是这种方式,也明显会产生代码冗余,健壮性也极差。
3. 解决状态共享问题的方案
一个更简单直接的解决方案是抽取出组件间的共享状态,放在一个全局单例中来管理。这样我们的组件树就变成了一个大的“视图”,而任何位置上的组件都可以访问其中的状态或触发动作。
3.1 用响应式 API 做简单状态管理(这里用的是 reactive)
一个最为直接简单的方案,就是将组件中的共享状态抽取出来,放在一个全局单例中进行管理。
这样,整个app就相当于一个大的视图,而任何层级的组件都可以对这些共享的状态进行修改。
让我们来看一个例子。
store.js(用于存储全局共享的状态及其修改方法):
import { reactive } from 'vue'export const store = reactive({count: 0,increment() {this.count++}
})
A.vue:
<template><div><span>From A: {{ store.count }}</span><button @click="store.count++">+(直接修改状态)</button><button @click="store.increment">+(使用store上的方法修改状态)</button></div>
</template><script setup>
import { store } from '../store.js'
</script><style lang="scss" scoped>
button {margin-left: 10px;
}
</style>
B.vue:
<template><div><span>From B: {{ store.count }}</span><button @click="store.count++">+(直接修改状态)</button><button @click="store.increment">+(使用store上的方法修改状态)</button></div>
</template><script setup>
import { store } from '../store.js'
</script><style lang="scss" scoped>
button {margin-left: 10px;
}
</style>
App.vue:
<template><div><span>From A: {{ store.count }}</span><button @click="store.count++">+(直接修改状态)</button><button @click="store.increment">+(使用store上的方法修改状态)</button></div>
</template><script setup>
import { store } from '../store.js'
</script><style lang="scss" scoped>
button {margin-left: 10px;
}
</style>

组件 App 及其子组件A、B,都通过 store.js 共享一个响应式状态,并且都可以直接对其进行修改,或者使用store上定义的方法进行修改。
注意点:
(1)我们通常建议使用store上定义的方法,而非对全局共享状态直接进行修改,但是其实问题不大;
(2)官网说要加 (),比如store.increment(),但是我尝试不加,其实代码仍是正常运行,问题也不大。但是还是建议添加,便于区分状态和方法。
3.2 用组合式函数返回全局状态(使用直接看 3.2.2)
话不多说,先看个例子。
3.2.1 错误用法(在模板中使用组合式函数)
store.js:
import { ref } from 'vue'// 全局状态,创建在模块作用域下
const globalCount = ref(1)export function useCount() {// 局部状态,每个组件都会创建const localCount = ref(2)function incrementGlobalCount() {globalCount.value++}function incrementLocalCount() {console.log('incrementLocalCount', localCount.value)localCount.value++console.log('incrementLocalCount', localCount.value)}return {globalCount,localCount,incrementGlobalCount,incrementLocalCount}
}
A.vue:
<template><div><span>From A-globalCount: {{ useCount().globalCount }}</span><button @click="useCount().globalCount.value++">+(直接修改状态)</button><button @click="useCount().incrementGlobalCount">+(使用store上的方法修改状态)</button></div><div><span>From A-localCount: {{ useCount().localCount }}</span><button @click="useCount().localCount.value++">+(直接修改状态)</button><button @click="useCount().incrementLocalCount">+(使用store上的方法修改状态)</button></div>
</template><script setup>
import { useCount } from '../store.js'
</script><style lang="scss" scoped>
div{margin-bottom: 10px;
}
button {margin-left: 10px;
}
</style>
B.vue:
<template><div><span>From B-globalCount: {{ useCount().globalCount }}</span><button @click="useCount().globalCount.value++">+(直接修改状态)</button><button @click="useCount().incrementGlobalCount">+(使用store上的方法修改状态)</button></div><div><span>From B-localCount: {{ useCount().localCount }}</span><button @click="useCount().localCount.value++">+(直接修改状态)</button><button @click="useCount().incrementLocalCount">+(使用store上的方法修改状态)</button></div>
</template><script setup>
import { useCount } from '../store.js'
</script><style lang="scss" scoped>
div{margin-bottom: 10px;
}
button {margin-left: 10px;
}
</style>
App.vue:
<template><A></A><B></B><div><span>From App-globalCount: {{ useCount().globalCount }}</span><button @click="useCount().globalCount.value++">+(直接修改状态)</button><button @click="useCount().incrementGlobalCount">+(使用store上的方法修改状态)</button></div><div><span>From App-localCount: {{ useCount().localCount }}</span><button @click="useCount().localCount.value++">+(直接修改状态)</button><button @click="useCount().incrementLocalCount">+(使用store上的方法修改状态)</button></div>
</template><script setup>
import A from '@/components/A.vue';
import B from '@/components/B.vue';
import { useCount } from '@/store';
console.log('useCount().localCount === useCount().localCount:', useCount().localCount === useCount().localCount)
</script><style lang="scss" scoped>
div{margin-bottom: 10px;
}
button {margin-left: 10px;
}
</style>

每次使用 useCount(),都会创建一个新的变量 localCount,所以每次使用
useCount().localCount.value++
或者
useCount().incrementLocalCount
都无法对上一次新建的 localCount 产生任何影响。换句话数,这里产生的局部变量相当于一个常量。
3.2.2 正确用法(在 script 中使用组合式函数)
store.js:
import { ref } from 'vue'// 全局状态,创建在模块作用域下
const globalCount = ref(1)export function useCount() {// 局部状态,每个组件都会创建const localCount = ref(2)function incrementGlobalCount() {globalCount.value++}function incrementLocalCount() {localCount.value++}return {globalCount,localCount,incrementGlobalCount,incrementLocalCount}
}
A.vue:
<template><div><span>From A-globalCount: {{ globalCount }}</span><button @click="globalCount++">+(直接修改状态)</button><button @click="incrementGlobalCount">+(使用store上的方法修改状态)</button></div><div><span>From A-localCount: {{ localCount }}</span><button @click="localCount++">+(直接修改状态)</button><button @click="incrementLocalCount">+(使用store上的方法修改状态)</button></div>
</template><script setup>
import { useCount } from '../store.js'
const { globalCount, localCount, incrementGlobalCount, incrementLocalCount } = useCount()
</script><style lang="scss" scoped>
div{margin-bottom: 10px;
}
button {margin-left: 10px;
}
</style>
B.vue:
<template><div><span>From B-globalCount: {{ globalCount }}</span><button @click="globalCount++">+(直接修改状态)</button><button @click="incrementGlobalCount">+(使用store上的方法修改状态)</button></div><div><span>From B-localCount: {{ localCount }}</span><button @click="localCount++">+(直接修改状态)</button><button @click="incrementLocalCount">+(使用store上的方法修改状态)</button></div>
</template><script setup>
import { useCount } from '../store.js'
const { globalCount, localCount, incrementGlobalCount, incrementLocalCount } = useCount()
</script><style lang="scss" scoped>
div{margin-bottom: 10px;
}
button {margin-left: 10px;
}
</style>
App.vue:
<template><A></A><B></B><div><span>From App-globalCount: {{ globalCount }}</span><button @click="globalCount++">+(直接修改状态)</button><button @click="incrementGlobalCount">+(使用store上的方法修改状态)</button></div><div><span>From App-localCount: {{ localCount }}</span><button @click="localCount++">+(直接修改状态)</button><button @click="incrementLocalCount">+(使用store上的方法修改状态)</button></div>
</template><script setup>
import A from '@/components/A.vue';
import B from '@/components/B.vue';
import { useCount } from './store.js'
const { globalCount, localCount, incrementGlobalCount, incrementLocalCount } = useCount()
</script><style lang="scss" scoped>
div{margin-bottom: 10px;
}
button {margin-left: 10px;
}
</style>

4. SSR 相关细节
如果你正在构建一个需要利用 服务端渲染 (SSR) 的应用,由于 store 是跨多个请求共享的单例,上述模式可能会导致问题。这在 SSR 指引那一章节会讨论更多细节。
5. Pinia(状态管理库,可以认为是 Vuex5)
Pinia 官网地址:https://pinia.vuejs.org/

5.1 Pinia 简介
(1)Pinia 是 Vue 目前最新也是推荐的状态管理库,用于替代之前的 Vuex。
Pinia (发音为 /piːnjʌ/,类似英文中的 “peenya”) 是最接近有效包名 piña (西班牙语中的 pineapple,即“菠萝”) 的词。Pinia 和 菠萝一样,和很多小块组成。在 Pinia 中,每个 Store 都是单独存在,一同进行状态管理。
Pinia 是由 Vue.js 团队成员研发的。起始于 2019年11月 左右的一次实验,其目的是设计一个拥有组合式 API 的 Vue 状态管理库。设计之初,就倾向于同时支持 Vue 2 和 Vue 3,并且不强制要求开发者使用组合式 API。在探索的过程中,Pinia 实现了 Vuex5 提案的大部分内容,于是就直接取而代之了。
目前 Vue 官方已经宣布 Pinia 就是新一代的 Vuex,但是为了尊重作者本人,名字保持不变,仍然叫做 Pinia。
(2)与 Vuex 相比,Pinia 不仅提供了一个更简单的 API,也提供了符合组合式 API 风格的 API,最重要的是,搭配 TypeScript 一起使用时有非常可靠的类型推断支持。
对比之前的 Vuex,Pinia 的特点区别如下:
-
弃用 mutations(现在只剩state、getters、actions)。它们经常被认为是极其冗余的。它们初衷是带来 devtools 的集成方案,但这已不再是一个问题了。
-
actions 中支持同步和异步方法修改 state 状态;
-
与 TypeScript 一起使用具有可靠的类型推断支持;
-
不再有模块嵌套,只有 Store 的概念,Store 之间可以相互调用;
-
支持插件扩展,可以非常方便实现本地存储等功能;
-
更加轻量,压缩后体积只有 2kb 左右。
5.2 快速入门(下载、挂载、创建数据仓库、使用)
(1)下载 Pinia:
npm i pinia
(2)在 app 中挂载 Pinia。main.js:
import { createApp} from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'const app = createApp(App)
// 创建一个 pinia 的实例
const pinia = createPinia()app.use(router)
app.use(pinia)app.mount('#app')
(3)创建数据仓库。src 文件夹下创建一个 stores 文件夹(数据仓库目录),下面存放多个数据仓库,每个仓库就是一个 js文件。
比如,我们创建一个stores/counter.js:
import { defineStore } from "pinia";export const useCounterStore = defineStore('counter', {// 定义数据状态,可以当作 data state: () => {return {count: 0}},// 定义了计算属性,可以当作 computed getters: {double: (state) => state.count * 2,},// 定义了修改数据状态的两个方法,可以当作 methods actions: {increment() {this.count++},decrement() {this.count--}}
})
(4)在组件中,使用数据仓库的状态和方法。
App.vue:
<template><div class="counter"><h1>计数器:{{ conterStore.count }}</h1><button @click="conterStore.increment">增加</button><button @click="conterStore.decrement">减少</button><h1>计数器的两倍:{{ conterStore.double }}</h1></div>
</template><script setup>
import { useCounterStore } from './stores/counter.js'
// 获取数据仓库
const conterStore = useCounterStore()
</script><style scoped>
.counter {display: flex;flex-direction: column;align-items: center;gap: 10px;
}
button {padding: 10px 20px;font-size: 16px;cursor: pointer;
}
</style>

5.3 使用细节(核心概念)
5.3.1 定义 Store
5.3.1.1 命名规范(函数使用use…Store格式)
store 由 defineStore 进行定义。
import { defineStore } from 'pinia'// `defineStore()` 的返回值的命名是自由的
// 但最好含有 store 的名字,且以 `use` 开头,以 `Store` 结尾。
// (比如 `useUserStore`,`useCartStore`,`useProductStore`)
// 第一个参数是你的应用中 Store 的唯一 ID。
export const useAlertsStore = defineStore('alerts', {// 其他配置...
})
- 第一个参数是当前 store 的ID(要求独一无二);
- 习惯将返回的函数命名为 use…Store,是一个符合组合式函数的命名约定;
5.3.1.2 Option Store 或者 Setup Store(都行,哪个舒服用哪个)
5.3.1.2.1 Option Store(选项式仓库,更简单直观)
defineStore() 的第二个参数可接受两类值:Setup 函数或 Option 对象。
让我们先看看 Option 对象的写法:
export const useCounterStore = defineStore('counter', {state: () => ({ count: 0, name: 'Eduardo' }),getters: {doubleCount: (state) => state.count * 2,},actions: {increment() {this.count++},},
})
你可以认为 state 是 store 的数据 (data),getters 是 store 的计算属性 (computed),而 actions 则是方法 (methods)。
为方便上手使用,Option Store 应尽可能直观简单。
5.3.1.2.2 SetupStore(组合式仓库,更灵活强大)
与 Vue 组合式 API 的 setup 函数 相似,我们可以传入一个函数,该函数定义了一些响应式属性和方法,并且返回一个带有我们想暴露出去的属性和方法的对象。
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 Store 中:
- ref() 就是 state 属性
- computed() 就是 getters
- function() 就是 actions
注意:
(1)要在 setup store 返回所有的 state 属性,以及对应的 getters 和 actions;
(2)store 中不能有私有属性,否则可能会影响开发工具、SSR和其他插件的正常运行;
(3)Setup store 内可以创建侦听器。
(4)可以使用任何的组合式函数,不过这也可能让 SSR 变得更复杂。
(5)可以访问全局属性(比如使用 useRoute 或者 inject 获取),但别在 return 中返回(因为组件可以直接访问)。
import { inject } from 'vue'
import { useRoute } from 'vue-router'
import { defineStore } from 'pinia'export const useSearchFilters = defineStore('search-filters', () => {const route = useRoute()// 这里假定 `app.provide('appProvided', 'value')` 已经调用过const appProvided = inject('appProvided')// ...return {// ...}
})
5.3.1.3 使用 Store(state 和 getter 不能直接解构)
(1)使用方式:
<script setup>
import { useCounterStore } from '@/stores/counter'
// 在组件内部的任何地方均可以访问变量 `store` ✨这步才创建 store 的实例
const store = useCounterStore()
</script>
(2)不能对 store 直接进行解构 state 和getter。
store 是一个用 reactive 包装的对象,这意味着不需要在 getters 后面写 .value。就像 setup 中的 props 一样,我们不能对它进行解构:
<script setup>
import { useCounterStore } from '@/stores/counter'
import { computed } from 'vue'const store = useCounterStore()
// ❌ 下面这部分代码不会生效,因为它的响应式被破坏了
// 与 reactive 相同: https://vuejs.org/guide/essentials/reactivity-fundamentals.html#limitations-of-reactive
const { name, doubleCount } = store
name // 将会一直是 "Eduardo" //
doubleCount // 将会一直是 0 //
setTimeout(() => {store.increment()
}, 1000)
// ✅ 而这一部分代码就会维持响应式
// 💡 在这里你也可以直接使用 `store.doubleCount`
const doubleValue = computed(() => store.doubleCount)
</script>
5.3.1.4 Store 的解构(action 可以直接解构,state 和 getter 需要借助 storeToRefs() 保持响应性)
(1)为了从 store 中提取属性时保持其响应性,你需要使用 storeToRefs()。
(2)可以直接从 store 中解构 action,因为它们也被绑定到 store 上。
<script setup>
import { storeToRefs } from 'pinia'
const store = useCounterStore()
// `name` 和 `doubleCount` 都是响应式引用
// 下面的代码同样会提取那些来自插件的属性的响应式引用
// 但是会跳过所有的 action 或者非响应式(非 ref 或者 非 reactive)的属性
const { name, doubleCount } = storeToRefs(store)
// 名为 increment 的 action 可以被解构
const { increment } = store
</script>
5.3.2 State(状态,相当于 data)
5.3.2.1 定义 state
Pinia 中,state 被定义为一个返回初始状态的函数。比如:
import { defineStore } from 'pinia'const useStore = defineStore('storeId', {// 为了完整类型推理,推荐使用箭头函数state: () => {return {// 所有这些属性都将自动推断出它们的类型count: 0,name: 'Eduardo',isAdmin: true,items: [],hasChanged: true,}},
})
5.3.2.2 访问 state(最好都在 state 中定义初始字段)
(1)默认情况下,可以直接对 state 进行读写。比如:
const store = useStore()store.count++
(2)没有包含在 state() 中的初始状态的属性,在外部无法被添加和使用。比如:
store.secondCount = 2
但是经过实验,好像并非如此。让我们来看一个完整案例。
仓库 counter.js:
import { defineStore } from "pinia";export const useCounterStore = defineStore('counter', {// 定义数据状态,可以当作 data state: () => {return {count: 0}},// 定义了计算属性,可以当作 computed getters: {double: (state) => state.count * 2,},// 定义了修改数据状态的两个方法,可以当作 methods actions: {increment() {this.count++},decrement() {this.count--}}
})
A.vue(添加 counterStore.secondCount):
<template><div class="A"><div>From A</div><div><span>count:{{ counterStore.count }}</span><button @click="counterStore.count++">+</button></div><div><span>stateSecondCount:{{ counterStore.secondCount }}</span><button @click="counterStore.secondCount++">+</button></div></div>
</template><script setup>
import { useCounterStore } from '@/stores/counter.js'
// 获取数据仓库
const counterStore = useCounterStore()
counterStore.secondCount = 2
console.log('counterStore', counterStore)
</script><style lang="scss" scoped>
.A{border: 1px solid red;padding: 10px;margin: 10px;button{margin-left: 10px;}
}
</style>
B.vue:
<template><div class="B"><div>From B</div><div><span>count:{{ counterStore.count }}</span><button @click="counterStore.count++">+</button></div><div><span>stateSecondCount:{{ counterStore.secondCount }}</span><button @click="counterStore.secondCount++">+</button></div></div>
</template><script setup>
import { useCounterStore } from '@/stores/counter.js'
// 获取数据仓库
const counterStore = useCounterStore()
</script><style lang="scss" scoped>
.B{border: 1px solid red;padding: 10px;margin: 10px;button{margin-left: 10px;}
}
</style>
App.vue:
<template><div><A /><B /></div>
</template><script setup>
import A from '@/components/A.vue'
import B from '@/components/B.vue';</script><style lang="scss" scoped></style>

官网原文如下:
注意,新的属性如果没有在 state() 中被定义,则不能被添加。它必须包含初始状态。例如:如果 secondCount 没有在 state() 中定义,我们无法执行 store.secondCount = 2。
所以正确的解读,应该新的属性无法被 添加到 state() 中,但是可以添加到仓库中,并且享有响应性。
不过,即使代码可行,我们也不建议这么做,因为这会让仓库的属性定义变得难以追踪和理解。
5.3.2.3 重置 state(Option Store 有 $reset() 方法,Setup Store 需要自定义)
(1)使用选项式 API 时,你可以通过调用 store 的 $reset() 方法将 state 重置为初始值。
关键代码:
counterStore.$reset()
完整实例如下。
counter.js:
import { defineStore } from "pinia";export const useCounterStore = defineStore('counter', {// 定义数据状态,可以当作 data state: () => {return {count: 0}},// 定义了计算属性,可以当作 computed getters: {double: (state) => state.count * 2,},// 定义了修改数据状态的两个方法,可以当作 methods actions: {increment() {this.count++},decrement() {this.count--}}
})
App.vue:
<template><div><span>count:{{ counterStore.count }}</span><button @click="counterStore.increment()">+</button><button @click="counterStore.$reset()">reset</button></div>
</template><script setup>
import { useCounterStore } from '@/stores/counter';
const counterStore = useCounterStore();
</script><style lang="scss" scoped>
button{margin-left: 10px;
}
</style>


(2)组合式 API 需要自定义 $reset() 方法。
counter.js(没有自定义会报错):
import { defineStore } from "pinia";
import { ref } from "vue";export const useCounterStore = defineStore('counter', () =>{const count = ref(0)function increment() {count.value++}return { count, increment }
})

counter.js(自定义$reset() 方法,正常运行):
import { defineStore } from "pinia";
import { ref } from "vue";export const useCounterStore = defineStore('counter', () =>{const count = ref(0)function increment() {count.value++}function $reset() {count.value = 0}return { count, increment, $reset }
})
5.3.2.4 mapState 和 mapWritableState(组件中 Option Store 的 state 属性的映射方法)
(1)mapState:在组件中将 Option Store 中的 state 映射为只读的计算属性。比如:
counter.js:
import { defineStore } from "pinia";export const useCounterStore = defineStore('counter', {// 定义数据状态,可以当作 data state: () => {return {count: 1}},// 定义了计算属性,可以当作 computed getters: {double: (state) => state.count * 2,},// 定义了修改数据状态的两个方法,可以当作 methods actions: {increment() {this.count++},decrement() {this.count--}}
})
App.vue:
<template><div>useCounterStore: {{ count }}</div><div>myOwnName: {{ myOwnName }}</div><div>double: {{ double }}</div><div>magicValue: {{ magicValue }}</div>
</template><script>
import { mapState } from 'pinia'
import { useCounterStore } from '@/stores/counter';export default {computed: {// 可以访问组件中的 this.count// 与从 store.count 中读取的数据相同...mapState(useCounterStore, ['count']),// 与上述相同,但将其注册为 this.myOwnName...mapState(useCounterStore, {myOwnName: 'count',// 你也可以写一个函数来获得对 store 的访问权double: store => store.count * 2,// 它可以访问 `this`,但它没有标注类型...magicValue(store) {return store.double + this.count + this.double},}),},mounted() {console.log(this.count)this.count++this.double++this.magicValue++}
}
</script><style lang="scss" scoped>
button{margin-left: 10px;
}
</style>

(2)mapWritableState:在组件中将 Option Store 中的 state 映射为可写属性。但注意你不能像 mapState() 那样传递一个函数。
App.vue:
<template><div>useCounterStore: {{ count }}</div><div>myOwnName: {{ myOwnName }}</div>
</template><script>
import { mapWritableState } from 'pinia'
import { useCounterStore } from '@/stores/counter';export default {computed: {// 可以访问组件中的 this.count,并允许设置它。// this.count++// 与从 store.count 中读取的数据相同...mapWritableState(useCounterStore, ['count']),// 与上述相同,但将其注册为 this.myOwnName...mapWritableState(useCounterStore, {myOwnName: 'count',}),},mounted() {console.log(this.count)setTimeout(() => {this.count++}, 3000)setTimeout(() => {this.myOwnName++}, 6000)}
}
</script><style lang="scss" scoped>
button{margin-left: 10px;
}
</style>

3s后

6s 后

5.3.2.3 变更 state($patch 方法)
除了用 store.count++ 直接改变 store,你还可以调用 $patch 方法。
(1)它允许你用一个 state 的补丁对象在同一时间更改多个属性:
store.$patch({count: store.count + 1,age: 120,name: 'DIO',
})
完整示例如下。
counter.js:
import { defineStore } from "pinia";export const useCounterStore = defineStore('counter', {// 定义数据状态,可以当作 data state: () => {return {count: 1,age: 29,name: 'Sheldon',hobbies: ['football', 'basketball'],sex: 'male'}},// 定义了计算属性,可以当作 computed getters: {double: (state) => state.count * 2,},// 定义了修改数据状态的两个方法,可以当作 methods actions: {increment() {this.count++},decrement() {this.count--}}
})
App.vue:
<template><div><div>count:{{ counterStore.count }}</div><div>age:{{ counterStore.age }}</div><div>name:{{ counterStore.name }}</div><div>hobbies:{{ counterStore.hobbies }}</div><div>sex:{{ counterStore.sex }}</div></div>
</template><script setup>
import { useCounterStore } from './stores/counter'
const counterStore = useCounterStore()
setTimeout(() => {counterStore.$patch({count: counterStore.count + 1,age: 30,name: 'Mike',})
}, 3000)</script><style lang="scss" scoped></style>

3s 后

(2)$patch 方法也接受一个函数来组合这种难以用补丁对象实现的变更(这种用于修改集合特别方便):
counterStore.$patch((state) => {state.hobbies.push('swimming')})
App.vue:
<template><div><div>count:{{ counterStore.count }}</div><div>age:{{ counterStore.age }}</div><div>name:{{ counterStore.name }}</div><div>hobbies:{{ counterStore.hobbies }}</div><div>sex:{{ counterStore.sex }}</div></div>
</template><script setup>
import { useCounterStore } from './stores/counter'
const counterStore = useCounterStore()
setTimeout(() => {counterStore.$patch((state) => {state.hobbies.push('swimming')})
}, 3000)</script><style lang="scss" scoped></style>

5.3.2.4 替换 state(其实还是用 $patch 方法去变更)
你不能完全替换掉 store 的 state,因为那样会破坏其响应性。但是,你可以 patch 它。
// 这实际上并没有替换`$state`
store.$state = { count: 24 }
// 在它内部调用 `$patch()`:
store.$patch({ count: 24 })
5.3.2.5 订阅 State(使用 $subscribe() 方法侦听 state 及其变化,通常配合持久化存储)
(1)比起普通的 watch(),使用 $subscribe() 的好处是 subscriptions 在 patch 后只触发一次。
关键代码
cartStore.$subscribe((mutation, state) => {// import { MutationType } from 'pinia'// mutation.type // 'direct' | 'patch object' | 'patch function'// 和 cartStore.$id 一样// mutation.storeId // 'cart'// 只有 mutation.type === 'patch object'的情况下才可用// mutation.payload // 传递给 cartStore.$patch() 的补丁对象。// 每当状态发生变化时,将整个 state 持久化到本地存储。localStorage.setItem('cart', JSON.stringify(state))
})
App.vue:
<template><div><div>count:{{ counterStore.count }}</div><div>age:{{ counterStore.age }}</div><div>name:{{ counterStore.name }}</div><div>hobbies:{{ counterStore.hobbies }}</div><div>sex:{{ counterStore.sex }}</div></div>
</template><script setup>
import { useCounterStore } from './stores/counter'
const counterStore = useCounterStore()
setTimeout(() => {counterStore.$patch({count: counterStore.count + 1,age: 30,name: 'Mike',})// counterStore.$patch((state) => {// state.hobbies.push('swimming')// })
}, 3000)counterStore.$subscribe((mutation, state) => {// 每当状态发生变化时,将整个 state 持久化到本地存储localStorage.setItem('counterStore', JSON.stringify(state))console.log('mutation', mutation)console.log('state', state)
}, { flush: 'sync' })</script><style lang="scss" scoped></style>


可以看到,每次更改 state 中的属性,都会触发侦听,从而更新本地localStorage 中的信息,从而做到持久化存储。
(2)另外,在底层实现上,$subscribe() 使用了 Vue 的 watch() 函数。你可以传入与 watch() 相同的选项。
(3)甚至,你可以在 pinia 实例上使用 watch() 函数侦听整个 state。
watch(pinia.state,(state) => {// 每当状态发生变化时,将整个 state 持久化到本地存储。localStorage.setItem('piniaState', JSON.stringify(state))},{ deep: true }
)
(4)取消订阅。
默认情况下,组件卸载时会自动取消订阅。如果想保留订阅,可以使用 { detached: true }。比如:
<script setup>
const someStore = useSomeStore()
// 此订阅器即便在组件卸载之后仍会被保留
someStore.$subscribe(callback, { detached: true })
</script>
5.3.3 Getter(state 的计算值,相当于 computed)
5.3.3.1 定义和使用 getter(state 是第一个参数)
在 defineStore 中,通过 getters 定义一个计算属性,推荐使用箭头函数,并且它将接收 state 作为第一个参数。
下面是一个完整实例。
counter.js:
import { defineStore } from "pinia";export const useCounterStore = defineStore('counter', {state: () => ({count: 1,}),getters: {// 定义一个计算属性,推荐使用箭头函数,并且它将接收 state 作为第一个参数doubleCount: (state) => state.count * 2,},
})
App.vue:
<template><div><div>doubleCount:{{ counterStore.doubleCount }}</div></div>
</template><script setup>
import { useCounterStore } from './stores/counter'
const counterStore = useCounterStore()</script><style lang="scss" scoped></style>

5.3.3.2 访问其他 getter(通过this)
与计算属性一样,你也可以组合多个 getter。通过 this,你可以访问到其他任何 getter。
counter.js:
import { defineStore } from "pinia";export const useCounterStore = defineStore('counter', {state: () => ({count: 1,}),getters: {doubleCount: (state) => state.count * 2,// 通过this访问其他getterdoubleCountPlusOne() {return this.doubleCount + 1},},
})
App.vue:
import { defineStore } from "pinia";export const useCounterStore = defineStore('counter', {state: () => ({count: 1,}),getters: {doubleCount: (state) => state.count * 2,// 通过this访问其他getterdoubleCountPlusOne() {return this.doubleCount + 1},},
})

5.3.3.3 向 getter 传递参数(失去缓存效果,变成函数了)
(1)Getter 只是幕后的计算属性,所以不可以向它们传递任何参数。不过,你可以从 getter 返回一个函数,该函数可以接受任意参数:
export const useUserListStore = defineStore('userList', {getters: {getUserById: (state) => {return (userId) => state.users.find((user) => user.id === userId)},},
})
完整示例如下。
userList.js:
import { defineStore } from "pinia";export const useUserListStore = defineStore('userList', {state: () => {return {users: [{ id: 1, name: 'Sheldon', active: true },{ id: 2, name: 'Jane', active: false },{ id: 3, name: 'Smith', active: true },{ id: 4, name: 'Mike', active: false },]}},getters: {getUserById: (state) => {// getter 返回一个函数,可以接收参数。但是会失去缓存效果return (userId) => state.users.find((user) => user.id === userId)},},
})
App.vue:
<template><p>User 2: {{ getUserById(2) }}</p>
</template><script setup>
import { useUserListStore } from '@/stores/userList'
import { storeToRefs } from 'pinia'
const useListStore = useUserListStore()
const { getUserById } = storeToRefs(useListStore)</script><style lang="scss" scoped></style>

(2)虽然 getter 将不再被缓存。它们只是一个被你调用的函数。不过,你可以在 getter 本身中缓存一些结果,虽然这种做法并不常见,但有证明表明它的性能会更好:
export const useUserListStore = defineStore('userList', {getters: {getActiveUserById(state) {const activeUsers = state.users.filter((user) => user.active)return (userId) => activeUsers.find((user) => user.id === userId)},},
})
完整实例如下。
userList.js:
import { defineStore } from "pinia";export const useCounterStore = defineStore('counter', {state: () => ({count: 1,}),getters: {doubleCount: (state) => state.count * 2,// 通过this访问其他getterdoubleCountPlusOne() {return this.doubleCount + 1},},
})
App.vue:
<template><p>User 2: {{ getUserById(2) }}</p><p>User 2: {{ getActiveUserById(2) }}</p>
</template><script setup>
import { useUserListStore } from '@/stores/userList'
import { storeToRefs } from 'pinia'
const useListStore = useUserListStore()
const { getUserById, getActiveUserById } = storeToRefs(useListStore)</script><style lang="scss" scoped></style>

5.3.3.4 访问其他 store 的 getter
想要使用另一个 store 的 getter 的话,那就直接在 getter 内使用就好:
import { useOtherStore } from './other-store'export const useStore = defineStore('main', {state: () => ({// ...}),getters: {otherGetter(state) {const otherStore = useOtherStore()return state.localData + otherStore.data},},
})
完整示例如下。
main.js:
import { useOtherStore } from './other'
import { defineStore } from "pinia";export const useMainStore = defineStore('main', {state: () => ({localData: 5,}),getters: {otherGetter(state) {const otherStore = useOtherStore()// otherStore.data 是其他Store的getterreturn state.localData + otherStore.data},},
})
other.js:
import { defineStore } from "pinia";export const useOtherStore = defineStore('other', {state: () => ({count: 10,}),getters: {data: (state) => {return state.count * 2}}
})
App.vue:
<template><p>{{ mainStore.otherGetter }}</p>
</template><script setup>
import { useMainStore } from '@/stores/main';
const mainStore = useMainStore();
</script><style lang="scss" scoped></style>

5.3.4 Action(方法,相当于 method)
5.3.4.1 基本使用方式
通过 actions 进行定义,在组件中可以直接通过类似 store.increment() 方式去调用。
export const useCounterStore = defineStore('main', {state: () => ({count: 0,}),actions: {increment() {this.count++},randomizeCounter() {this.count = Math.round(100 * Math.random())},},
})
5.3.4.2 异步 action(其实就是执行请求)
action 可以是异步的,你可以在它们里面 await 调用任何 API,以及其他 action。
让我们看一个完整的异步 action 示例(后端服务和 api 部分,可以参照之前的《Vue 用户管理系统(路由相关练习一)》)。
users.js:
import { getUserListApi } from '../api/userApi.js'
import { defineStore } from "pinia";export const useUsersStore = defineStore('users', {state: () => ({userData: null,// ...}),actions: {// 获取用户列表数据async getUserList() {try {const data = await getUserListApi()this.userData = data} catch (error) {// 让表单组件显示错误console.error(error)return error}},},
})
App.vue:
<template><p>users:{{ usersStore.userData }}</p><div><button @click="usersStore.getUserList()">获取用户列表</button></div>
</template><script setup>
import { useUsersStore } from '@/stores/users';
const usersStore = useUsersStore();
</script><style lang="scss" scoped></style>


5.3.4.3 访问其他 store 的 action
想要使用另一个 store 的话,那你直接在 action 中调用就好了:
import { useAuthStore } from './auth-store'export const useSettingsStore = defineStore('settings', {state: () => ({preferences: null,// ...}),actions: {async fetchUserPreferences() {const auth = useAuthStore()if (auth.isAuthenticated) {this.preferences = await fetchPreferences()} else {throw new Error('User must be authenticated')}},},
})
5.3.4.4 mapActions(映射方法,起别名)
如果你不喜欢使用组合式 API,你也可以使用 mapActions() 辅助函数将 action 属性映射为你组件中的方法。
import { mapActions } from 'pinia'
import { useCounterStore } from '../stores/counter'export default {methods: {// 访问组件内的 this.increment()// 与从 store.increment() 调用相同...mapActions(useCounterStore, ['increment'])// 与上述相同,但将其注册为this.myOwnName()...mapActions(useCounterStore, { myOwnName: 'increment' }),},
}
5.3.4.5 订阅 action
(1)可以通过 store.$onAction() 来监听 action 和它们的结果;
(2)传递给它的回调函数会在 action 本身之前执行;
(3)after 表示在 promise 解决之后,允许你在 action 解决后执行一个回调函数;
(4)onError 允许你在 action 抛出错误或 reject 时执行一个回调函数。
const unsubscribe = someStore.$onAction(({name, // action 名称store, // store 实例,类似 `someStore`args, // 传递给 action 的参数数组after, // 在 action 返回或解决后的钩子onError, // action 抛出或拒绝的钩子}) => {// 为这个特定的 action 调用提供一个共享变量const startTime = Date.now()// 这将在执行 "store "的 action 之前触发。console.log(`Start "${name}" with params [${args.join(', ')}].`)// 这将在 action 成功并完全运行后触发。// 它等待着任何返回的 promiseafter((result) => {console.log(`Finished "${name}" after ${Date.now() - startTime}ms.\nResult: ${result}.`)})// 如果 action 抛出或返回一个拒绝的 promise,这将触发onError((error) => {console.warn(`Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`)})}
)// 手动删除监听器
unsubscribe()
(5)默认情况下,action 订阅器会被绑定到添加它们的组件上(如果 store 在组件的 setup() 内)。这意味着,当该组件被卸载时,它们将被自动删除;
(6)如果你想在组件卸载后依旧保留它们,请将 true 作为第二个参数传递给 action 订阅器,以便将其从当前组件中分离:
<script setup>
const someStore = useSomeStore()
// 此订阅器即便在组件卸载之后仍会被保留
someStore.$onAction(callback, true)
</script>
5.3.5 插件
Pinia store 可扩展的内容:
- 为 store 添加新的属性
- 定义 store 时增加新的选项
- 为 store 增加新的方法
- 包装现有的方法
- 改变甚至取消 action
- 实现副作用,如本地存储
- 仅应用插件于特定 store
5.3.5.1 快速入门
插件是通过 pinia.use() 添加到 pinia 实例的。最简单的例子是通过返回一个对象将一个静态属性添加到所有 store。
关键代码:
import { createPinia } from 'pinia'// 创建的每个 store 中都会添加一个名为 `secret` 的属性。
// 在安装此插件后,插件可以保存在不同的文件中
function SecretPiniaPlugin() {return { secret: 'the cake is a lie' }
}const pinia = createPinia()
// 将该插件交给 Pinia
pinia.use(SecretPiniaPlugin)// 在另一个文件中
const store = useStore()
store.secret // 'the cake is a lie'
完整示例如下。
mian.js:
import { createApp} from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'const app = createApp(App)
// 创建一个 pinia 的实例
const pinia = createPinia()// 创建的每个 store 中都会添加一个名为 `secret` 的属性。
// 在安装此插件后,插件可以保存在不同的文件中
function SecretPiniaPlugin(context) {console.log('context', context)return { secret: 'the cake is a lie' }
}pinia.use(SecretPiniaPlugin)app.use(router)
app.use(pinia)app.mount('#app')
counter.js:
import { defineStore } from "pinia";export const useCounterStore = defineStore('counter', {state: () => {return {count: 0}},
})
user.js:
import { defineStore } from "pinia";export const useUsersStore = defineStore('users', {state: () => ({userData: [],}),})
App.vue:
<template><p>counterStore.secret:{{ counterStore.secret }}</p><p>userStore.secret:{{ userStore.secret }}</p>
</template><script setup>
import { useCounterStore } from '@/stores/counter';
import { useUsersStore } from '@/stores/users';
const counterStore = useCounterStore();
console.log('counterStore', counterStore)
const userStore = useUsersStore();
console.log('userStore', userStore)
</script><style lang="scss" scoped></style>

通过观察输出结果及其顺序,可以发现:
(1)插件的函数是在创建对应的仓库实例时才会执行应用;
(2)通过插件插入的 store 属性,会应用到所有的 store;
(3)插件函数的第一个参数 context 是一个对象,主要有4个属性,分别为:
- app —— 用
createApp()创建的当前应用(仅 Vue 3) - options —— 该插件想扩展的 store
- pinia —— 用
createPinia()创建的 pinia - store —— 定义传给
defineStore()的 store 的可选对象
5.3.5.2 扩展 Store(3 种写法)
其实 5.3.5.1 快速入门 就是一个扩展 Store 的例子。除了使用普通函数进行扩展,我们还可以使用其他语法:
(1)普通函数写法:
function SecretPiniaPlugin(context) {console.log('context', context)return { secret: 'the cake is a lie' }
}pinia.use(SecretPiniaPlugin)
(2)箭头函数写法:
pinia.use(() => ({ secret: 'the cake is a lie' }))
(3)直接在参数 context.store 中进行添加:
pinia.use(({ store }) => { store.secret = 'the cake is a lie'
})
任何由插件返回的属性都会被 devtools 自动追踪,所以如果你想在 devtools 中调试 secret 属性,为了使 devtools 能追踪到 secret,请确保在 dev 模式下将其添加到 store._customProperties 中:
// 上文示例
pinia.use(({ store }) => {store.secret = 'the cake is a lie' // 确保你的构建工具能处理这个问题,webpack 和 vite 在默认情况下应该能处理。if (process.env.NODE_ENV === 'development') {// 添加你在 store 中设置的键值store._customProperties.add('hello')}
})
每个 store 都被 reactive包装过,所以可以自动解包任何它所包含的 Ref(ref()、computed()等:
const sharedRef = ref('shared')
pinia.use(({ store }) => {// 每个 store 都有单独的 `hello` 属性store.hello = ref('secret')// 它会被自动解包store.hello // 'secret'// 所有的 store 都在共享 `shared` 属性的值store.shared = sharedRefstore.shared // 'shared'
})
5.3.5.3 添加新的 state
如果要使用插件为 store 添加新的 state,必须在 store 和 store.$state 两个地方同时添加它。比如:
import { toRef, ref } from 'vue'pinia.use(({ store }) => {// 为了正确地处理 SSR,我们需要确保我们没有重写任何一个// 现有的值if (!store.$state.hasOwnProperty('hasError')) {// 在插件中定义 hasError,因此每个 store 都有各自的// hasError 状态const hasError = ref(false)// 在 `$state` 上设置变量,允许它在 SSR 期间被序列化。store.$state.hasError = hasError}// 我们需要将 ref 从 state 转移到 store// 这样的话,两种方式:store.hasError 和 store.$state.hasError 都可以访问// 并且共享的是同一个变量// 查看 https://cn.vuejs.org/api/reactivity-utilities.html#torefstore.hasError = toRef(store.$state, 'hasError')// 在这种情况下,最好不要返回 `hasError`// 因为它将被显示在 devtools 的 `state` 部分// 如果我们返回它,devtools 将显示两次。
})
完整示例如下:
main.js:
import { createApp, toRef, ref} from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'const app = createApp(App)
// 创建一个 pinia 的实例
const pinia = createPinia()pinia.use(({ store }) => { if (!store.$state.hasOwnProperty('hasError')) {const hasError = ref(false)store.$state.hasError = hasError}store.hasError = toRef(store.$state, 'hasError')
})app.use(router)
app.use(pinia)app.mount('#app')
counter.js 和 user.js 不做改动。
App.vue:
<template><p>counterStore.hasError:{{ counterStore.hasError }}</p><p>userStore.hasError:{{ userStore.hasError }}</p>
</template><script setup>
import { useCounterStore } from '@/stores/counter';
import { useUsersStore } from '@/stores/users';
const counterStore = useCounterStore();
console.log('counterStore', counterStore)
const userStore = useUsersStore();
console.log('userStore', userStore)
</script><style lang="scss" scoped></style>

5.3.5.4 重置插件中添加的 state
关键代码:
import { toRef, ref } from 'vue'pinia.use(({ store }) => {if (!store.$state.hasOwnProperty('hasError')) {const hasError = ref(false)store.$state.hasError = hasError}store.hasError = toRef(store.$state, 'hasError')// 确认将上下文 (`this`) 设置为 storeconst originalReset = store.$reset.bind(store)// 覆写其 $reset 函数return {$reset() {originalReset()store.hasError = false},}
})
App.vue:
<template><p>counterStore.hasError:{{ counterStore.hasError }}</p><p>userStore.hasError:{{ userStore.hasError }}</p>
</template><script setup>
import { useCounterStore } from '@/stores/counter';
import { useUsersStore } from '@/stores/users';
const counterStore = useCounterStore();
counterStore.hasError = true
setTimeout(() => {// 重置为初始 statecounterStore.$reset()
}, 3000)
console.log('counterStore', counterStore)
const userStore = useUsersStore();
console.log('userStore', userStore)
</script><style lang="scss" scoped></style>
5.3.5.5 添加新的外部属性
当添加外部属性、第三方库的类实例或非响应式的简单值时,你应该先用 markRaw() 来包装一下它,再将它传给 pinia。下面是一个在每个 store 中添加路由器的例子:
import { markRaw } from 'vue'
// 根据你的路由器的位置来调整
import { router } from './router'pinia.use(({ store }) => {store.router = markRaw(router)
})
5.3.5.6 在插件中调用 $subscribe(监听 store 及其 action 的变化)
你也可以在插件中使用 store.$subscribe 和 store.$onAction 。
pinia.use(({ store }) => {store.$subscribe(() => {// 响应 store 变化})store.$onAction(() => {// 响应 store actions})
})
5.3.5.7 添加新的选项(根据选项,判断是否对数据或者行为进行二次处理,比如防抖)
在定义 store 时,可以创建新的选项,以便在插件中使用它们。
例如,你可以创建一个 debounce 选项,允许你让任何 action 实现防抖。完整示例如下。
search.js(添加选项):
import { defineStore } from "pinia";export const useSearchStore = defineStore('search', {actions: {searchContacts() {console.log('searchContacts action is called')},},// 这将在后面被一个插件读取debounce: {// 让 action searchContacts 防抖 300mssearchContacts: 300,},
})
main.js(判断是否有 debounce 选项,并进行处理):
import { createApp, toRef, ref} from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
// 使用任意防抖库,这里使用了 lodash,可以先使用 npm i lodash 先下载工具库
import { debounce } from 'lodash'const app = createApp(App)
// 创建一个 pinia 的实例
const pinia = createPinia()pinia.use(({ options, store }) => {if (options.debounce) {// 我们正在用新的 action 来覆盖这些 actionreturn Object.keys(options.debounce).reduce((debouncedActions, action) => {debouncedActions[action] = debounce(store[action],options.debounce[action])return debouncedActions}, {})}
})app.use(router)
app.use(pinia)app.mount('#app')
App.vue:
<template><div><button @click="searchStore.searchContacts()">测试点击防抖</button></div>
</template><script setup>
import { useSearchStore } from '@/stores/search';
const searchStore = useSearchStore();
</script><style lang="scss" scoped></style>

5.3.6 组件外的 Store
在单页面应用中,useStore() 的调用,需要在 pinia 实例被激活之后。下面是两个例子。
(1)main.js 中使用 Store:
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)// ✅ 成功,因为 pinia 实例现在激活了
const userStore = useUserStore()
(2)在路由守卫中:
import { createRouter } from 'vue-router'
const router = createRouter({// ...
})// ❌ 由于引入顺序的问题,这将失败
const store = useStore()router.beforeEach((to, from, next) => {// 我们想要在这里使用 storeif (store.isLoggedIn) next()else next('/login')
})router.beforeEach((to) => {// ✅ 这样做是可行的,因为路由器是在其被安装之后开始导航的,// 而此时 Pinia 也已经被安装。const store = useStore()if (to.meta.requiresAuth && !store.isLoggedIn) return '/login'
})
注意,因为本文主要讲的是在 Vue3 的web端场景下的使用,因此关于 SSR 应用并未过多记录。如果需要,请前往官网 https://pinia.vuejs.org/zh/ssr/ 进行了解。
上一章 《Vue3 KeepAlive(缓存组件实例)》
下一章 《Vue3 任务管理器(Pinia 练习)》
