当前位置: 首页 > news >正文

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)PiniaVue 目前最新也是推荐的状态管理库,用于替代之前的 Vuex

Pinia (发音为 /piːnjʌ/,类似英文中的 “peenya”) 是最接近有效包名 piña (西班牙语中的 pineapple,即“菠萝”) 的词。Pinia 和 菠萝一样,和很多小块组成。在 Pinia 中,每个 Store 都是单独存在,一同进行状态管理

Pinia 是由 Vue.js 团队成员研发的。起始于 2019年11月 左右的一次实验,其目的是设计一个拥有组合式 API 的 Vue 状态管理库。设计之初,就倾向于同时支持 Vue 2Vue 3,并且不强制要求开发者使用组合式 API。在探索的过程中,Pinia 实现了 Vuex5 提案的大部分内容,于是就直接取而代之了

目前 Vue 官方已经宣布 Pinia 就是新一代的 Vuex,但是为了尊重作者本人,名字保持不变,仍然叫做 Pinia。

(2)与 Vuex 相比,Pinia 不仅提供了一个更简单的 API,也提供了符合组合式 API 风格的 API,最重要的是,搭配 TypeScript 一起使用时有非常可靠的类型推断支持。

对比之前的 Vuex,Pinia 的特点区别如下:

  1. 弃用 mutations(现在只剩state、getters、actions)。它们经常被认为是极其冗余的。它们初衷是带来 devtools 的集成方案,但这已不再是一个问题了。

  2. actions 中支持同步和异步方法修改 state 状态;

  3. 与 TypeScript 一起使用具有可靠的类型推断支持;

  4. 不再有模块嵌套,只有 Store 的概念,Store 之间可以相互调用;

  5. 支持插件扩展,可以非常方便实现本地存储等功能;

  6. 更加轻量,压缩后体积只有 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,必须在 storestore.$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.$subscribestore.$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 练习)》

http://www.dtcms.com/a/582286.html

相关文章:

  • 2025重新出发!中小物流仓配一体化平台的技术选型建设手记
  • 记录 RTPEngine Dockerfile 的调试过程
  • 阜新网站seo做软装的网站
  • 微信公众号微网站开发中国装修网官方网站
  • 学做预算网站wordpress编辑器
  • 盲盒抽赏小程序爬塔玩法分析:技术实现 + 留存破局,打造长效抽赏生态
  • 基于springboot的学院班级回忆录的设计与实现
  • 重庆专业网站建设公司响应式框架
  • day10(11.7)——leetcode面试经典150
  • 特定网站开发个人主页网站设计
  • flink 1.20 物化表(Materialized Tables)
  • html网站建设流程图只做一页的网站多少钱
  • 百度上能收到的企业名称网站怎么做wordpress theme api
  • asp.net网站开发案例教程做图片模板
  • 大庆门户网站做外贸接私单的网站
  • 企业项目级医院随访系统源码,患者随访管理系统,技术框架:Java+Spring boot,Vue,Ant-Design+MySQL5
  • 上蔡网站建设WordPress导出静态网页
  • 成都网站制作怎么样设计企业网站内容
  • 互联网做网站地推WordPress全局屏蔽谷歌
  • RV1126 NO.40:OPENCV图形计算面积、弧长API讲解
  • 威海网站建设哪家好哪有个人免费云服务器
  • 【Redis】02 基本数据类型
  • Rust内存问题检测
  • 娄底市网站建设课程资源网站开发
  • 写 CSDN 文章的体会
  • Vibe Coding - 免费使用claude code 、gpt-5、grok-code-fast-1进行氛围编程
  • 【NOI】C++一维数组之数组计数法
  • flash xml网站wordpress 表单数据
  • 事业单位建立网站wordpress后台慢
  • 轴控功能块常用调用方法