Vue3 官方推荐状态管理库Pinia

1 篇文章 0 订阅

介绍

Pinia 是 Vue 官方团队推荐代替Vuex的一款轻量级状态管理库,允许跨组件/页面共享状态
Pinia 旨在提供一种更简洁、更直观的方式来处理应用程序的状态。
Pinia 充分利用了 Vue 3 的 Composition API。

官网:
Pinia符合直觉的 Vue.js 状态管理库

Pinia的核心概念

  • store:是存储状态(共享数据)的地方。
    • 是一个保存状态和业务逻辑的实体。它承载着全局状态。
    • 每个组件都可以读取/写入。
    • 官方推荐使用 hooks 的命名方式,以 use 开头。例如:useCountStoreuseUserStoreuseCartStoreuseProductStore
  • state:是 store 中用于存储应用状态的部分。
    • 通俗来讲,state 是真正存储数据的地方,它就是存放在store里的数据。
    • 官方要求 state 写成函数形式,并且要return一个对象。
      示例:state() { return {} }
  • getters:从存储的状态中派生数据,类似于 Vue 中的计算属性(computed)。
    • 是一种依赖于 store 状态并产生计算值的函数。这些值将被缓存,直到依赖的状态改变。
  • actions:是用于改变状态的方法。

安装与配置 Pinia

  1. 通过npm或yarn安装Pinia:
npm install pinia
# 或者使用 yarn
yarn add pinia
  1. 在Vue应用文件中(通常是main.jsmain.ts),引入并使用Pinia:
// 引入 createApp 用于创建实例
import { createApp } from 'vue';
// 引入 App.vue 根组件
import App from './App.vue';
// 从 Pinia 库中引入 createPinia 函数,用于创建 Pinia 实例
import { createPinia } from 'pinia';

// 创建一个应用
const app = createApp(App)
// 创建 Pinia 实例
const pinia = createPinia();
// 将 Pinia 实例注册到 Vue 应用实例中,使得整个应用可以使用 Pinia 进行状态管理
app.use(pinia);

// 挂载整个应用到app容器中
app.mount('#app')

通过以上步骤,成功地在 Vue 项目中集成了 Pinia 状态管理库,为应用提供了集中式的状态管理功能,可以在组件中通过使用 Pinia 的 store 来管理和共享数据。

此时开发者工具中已经有了pinia选项:
在这里插入图片描述

Store

Store 是一个保存状态和业务逻辑的实体。它承载着全局状态。

定义Store

Pinia 使用 defineStore 定义Store。

import { defineStore } from 'pinia'
// 你可以任意命名 `defineStore()` 的返回值,但最好使用 store 的名字,同时以 `use` 开头且以 `Store` 结尾。
// 第一个参数是你的应用中 Store 的唯一 ID。
export const useCountStore = defineStore('count', {
  // 其他配置...
})

defineStore()

  1. 第一个参数(store 的 ID)
    • 这是一个字符串,用于唯一标识一个 store。 defineStore('count', {})中的count就是这个store的ID。
    • 必须传入, Pinia 将用它来连接 store 和 devtools。
  2. 第二个参数(配置对象)
    • 可接受两类值:Setup 函数或 Option 对象。
    • 这个对象包含了 store 的各种配置选项,主要有以下几个重要属性:stateactionsgetters
Option Store

与 Vue 的选项式 API 类似,可以传入一个带有 stateactionsgetters 属性的 Option 对象。

export const useCountStore = defineStore('count', {
  state: () => ({ count: 0 }),
  getters: {
    doubleCount: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++
    },
  },
})

可以认为 state 是 store 的数据 (data),getters 是 store 的计算属性 (computed),而 actions 则是方法 (methods)。

Setup Store

与 Vue 组合式 API 的 setup 函数 相似,可以传入一个函数,该函数定义了一些响应式属性和方法,并且 return 一个带有需要暴露出去的属性和方法的对象。

import { defineStore } from "pinia";
export const useCountStore = defineStore('count', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)
  function increment() {
    count.value++
  }
  
  // 把要在组件中使用到的属性、方法暴露出去
  return { count, doubleCount, increment }
})

在 Setup Store 中:

  • ref() 就是 state 属性
  • computed() 就是 getters
  • function() 就是 actions

注意,要让 pinia 正确识别 state,你必须在 setup store 中返回 state 的所有属性。这意味着,你不能在 store 中使用私有属性。不完整返回会影响 SSR ,开发工具和其他插件的正常运行。

使用 Store

虽然定义了一个 store,但在使用 <script setup> 调用 useStore()(或者使用 setup() 函数) 之前,store 实例是不会被创建的:
在这里插入图片描述
Pinia中,没有名为 count 的store。

调用 useStore()后,Pinia 自动将store安装到vue应用中:

<script setup lang="ts">
import { useCountStore } from '@/store/count';

// 调用useCountStore函数得到一个countStore实例
// 一旦 store 被实例化,可以直接访问在 store 的 state、getters 和 actions 中定义的任何属性。
// 调用useCountStore后,Pinia 自动将store安装到vue应用中
const countStore = useCountStore()

console.log(countStore)         // 一个reactive对象
console.log(countStore.count)   // 0
</script>

通过工具vue devtools查看Pinia,名为count的store已经被安装到vue应用中:
在这里插入图片描述
通过工具vue devtools查看Count.vue:
在这里插入图片描述
Count.vue组件里:
countStore 是一个 reactive 定义的响应式对象。
sum是一个Ref(响应式引用)类型的数据。

通过实例countStore访问statecount属性:

// 直接访问, 不需要使用.value
const count1 = countStore.count;
// 通过访问 store 实例的 $state 属性来获取状态值
const count2 = constStore.$state.count

// 解构 constStore 
const { count } = constStore

每个 store 都被 reactive 包装过,所以可以自动解包任何它所包含的 Ref(ref()computed()…)。

  • 在 Vue 3 中,如果一个 reactive 对象包含了 ref 类型的数据,直接访问这个 ref 数据时不需要使用 .value
    • 这是因为 Vue 的响应式系统会自动处理这种嵌套的情况。当访问 reactive 对象中的 ref 数据时,Vue 会自动解包 ref 的值,就可以直接获取到 ref 所包裹的值,而无需显式地使用 .value
  • 当从 store 中解构状态时,如果直接解构赋值给变量,这些变量会失去响应性。
    • 直接解构出来的 count 属性失去响应性,值始终为 0。不会随着 store 中的状态变化而自动更新。

从 Store 解构

使用 storeToRefs() 解构store,解构后的属性保持响应性。它将为每一个响应式属性创建引用。

<script setup>
import { storeToRefs } from 'pinia';
import { useCountStore } from '@/store/count';
const countStore = useCountStore()
// `count` 和 `doubleCount` 是响应式的 ref
// 同时通过插件添加的属性也会被提取为 ref
// 并且会跳过所有的 action 或非响应式 (不是 ref 或 reactive) 的属性
const { count, doubleCount } = storeToRefs(countStore)
// 作为 action 的 increment 可以直接解构
const { increment } = countStore
</script>

执行console.log(storeToRefs(countStore)),看看控制台打印结果:
storeToRefs()只关注store里的数据,不关注store里的方法,不会对方法进行ref包装。
解构出来的属性都是放在stategetter 里面的数据。
在这里插入图片描述

为什么不使用toRefs()解构store呢?
toRefs()也可以解构store,但是它会把store的全部属性(数据、方法)变成ref类型。
执行console.log(toRefs(countStore)),看看控制台打印结果:
在这里插入图片描述

State

在大多数情况下,state 都 store 的核心。
在 Pinia 中,state 被定义为一个返回初始状态的函数。

import { defineStore } from 'pinia'

const useStore = defineStore('storeId', {
  // 为了完整类型推理,推荐使用箭头函数
  state: () => {
    return {
      // 所有这些属性都将自动推断出它们的类型
      count: 0,
      name: 'Eduardo',
      isAdmin: true,
      items: [],
      hasChanged: true,
    }
  },
})

如果在tsconfig.json中启用了 strict,或者启用了 noImplicitThis,Pinia 将自动推断变量的状态类型。

在某些情况下,需要使用类型断言:

const useStore = defineStore('storeId', {
  state: () => {
    return {
      // 用于初始化空列表
      userList: [] as UserInfo[],
      // 用于尚未加载的数据
      user: null as UserInfo | null,
    }
  },
})

interface UserInfo {
  name: string
  age: number
}
  • userList: [] as UserInfo[]

    • userList: []:这部分将userList初始化为一个空数组。在应用启动时,这个属性没有任何值,所以初始化为一个空数组可以确保有一个明确的初始状态。
    • as UserInfo[]:类型断言,明确指定userList的类型为UserInfo[],即一个由UserInfo类型元素组成的数组。
    • 在使用userList时,TypeScript 可以进行类型检查,确保只向数组中添加符合UserInfo类型的元素,减少类型错误的发生。
  • user: null as UserInfo | null

    • user: null:将user初始化为null。这表示在应用启动时,还没有特定的用户信息被加载或设置,所以初始值为null
    • as UserInfo | null:类型断言,明确指定user的类型为UserInfo | null。这意味着user可以是一个符合UserInfo类型的对象,也可以是null
    • TypeScript 可以在编译时进行类型检查,确保对user的操作符合其类型定义。例如,如果尝试将一个不兼容的类型赋值给user,TypeScript 会报错,从而避免运行时错误。

可以用一个接口定义 state,并添加 state() 的返回值的类型:

interface State {
  userList: UserInfo[]
  user: UserInfo | null
}

const useStore = defineStore('storeId', {
  state: (): State => {
    return {
      userList: [],
      user: null,
    }
  },
})

interface UserInfo {
  name: string
  age: number
}

在组件中访问State

  1. 默认情况下,通过 store 实例访问 state,直接对其进行读写。
<script setup lang="ts">
import { useCountStore } from '@/store/count';
// 一旦 store 被实例化,可以直接访问在 store 的 state、getters 和 actions 中定义的任何属性。
const countStore = useCountStore()
countStore.count ++ 
</script>
  1. 在 Vue3 的选项式API中,可以使用 mapState() 辅助函数将 state 属性映射为只读的计算属性
<script>
import { mapState } from 'pinia';
import { useCountStore } from '@/store/count';
export default {
  computed: {
   // 使用数组形式
   // 可以访问组件中的 this.count
   // 与从 store.count 中读取的数据相同
   ...mapState(useCountStore, ['count']),
   // 通过对象形式传入映射配置
   ...mapState(useCountStore, {
     // 给属性 count 取别名为 myOwnCount
     myOwnCount: 'count',
     // 定义了一个名为 double 的计算属性,是一个函数,接受 store 作为参数
     doubleCount: store => store.count * 2,
     // 它可以访问 `this`,但它没有标注类型...
     magicValue(store) {
       return store.someGetter + this.count + this.doubleCount
     },
   }),
  },
  methods: {
    incrementCount() {
      this.$store.dispatch('count/increment');
    },
  },
};
</script>

在这个示例中:

  • import { mapState } from 'pinia';:从 Pinia 库中引入 mapState 辅助函数。这个函数用于将 Pinia store 的状态映射为 Vue 组件的计算属性,使得在组件中可以方便地访问 store 的状态。
  • 通过mapState(useCountStore, ['count'])将 Pinia store 中的 count 状态映射为组件的计算属性。这样在组件中可以直接使用 count 来访问 store 中的状态,并且这个属性是只读的。
  • methods中定义了一个incrementCount方法,通过this.$store.dispatch('count/increment')来调用 store 中的increment action,实现对状态的修改。
  • myOwnCount: 'count':将 store 中的 count 状态映射为组件的计算属性 myOwnCount,这样可以使用 this.myOwnCount 来访问与 store.count 相同的值,但使用了自定义的属性名。
  • magicValue(store) { return store.someGetter + this.count + this.doubleCount}:定义了一个名为 magicValue 的计算属性,它是一个函数,接受 store 作为参数。
    • 在这个函数中,可以访问 store.someGetter(假设 store 中有这个 getter)、组件中的 this.countthis.doubleCount,并返回它们的组合结果。
    • 注意,这里的 magicValue 函数没有明确的类型标注,可能会在某些情况下导致类型不明确的问题。
  1. 在 Vue3 的选项式API中,可以使用 mapWritableState() 辅助函数将 state 属性映射为可写的计算属性,可以在组件中直接读取/修改 store 的状态。
<script>
import { mapWritableState } from 'pinia';
import { useCountStore } from '@/store/count';
export default {
  computed: {
   // 使用数组形式
   // 可以访问组件中的 this.count,并允许设置它。
   // 例如:直接操作 this.count++ 修改count的值
   // 与从 store.count 中读取的数据相同
   ...mapWritableState(useCountStore, ['count']),
   // 通过对象形式传入映射配置
   ...mapWritableState (useCountStore, {
     // 给属性 count 取别名为 myOwnCount
     myOwnCount: 'count',
   }),
  },
  methods: {
    incrementCount() {
      this.count++;
    },
  },
};
</script>

mapState() 的区别

  • mapWritableState() 不能像 mapState() 那样传递一个函数来进行复杂的状态映射或计算。
  • mapState() 中,可以通过传递一个函数来实现更灵活的状态映射,例如根据 store 的多个状态属性计算出一个新的属性值。
  1. 使用storeToRefs函数,确保解构出来的 state 属性保持响应性。

注意
新的属性如果没有在 state() 中被定义,则不能被添加。它必须包含初始状态。
例如:如果 secondCount 没有在 state() 中定义,无法执行 useCountStore .secondCount= 2

  • 明确的初始状态:通过要求所有可访问的属性在 state() 中定义并包含初始状态,Pinia 确保了应用在任何时候都有一个明确的、已知的状态起点。
  • 可控的状态变更:只允许修改在 state() 中定义的属性,可以防止意外地引入新的状态变量,从而降低了由于错误的状态修改而导致的错误风险。
  • 正确的响应式更新:Pinia 的响应式系统依赖于对已知状态属性的跟踪。只有在 state() 中定义的属性才能被 Pinia 的响应式系统正确地跟踪和更新,确保了状态变化能够及时反映在界面上。

修改State

  1. 直接修改
<script setup lang="ts">
import { useCountStore } from '@/store/count';
const countStore = useCountStore()
countStore.count ++ 
</script>
  1. 使用$patch$patch是一个用于批量更新 store 状态的方法。
    $patch接受一个对象作为参数,该对象的属性将被用来更新 store 的状态。
<script setup lang="ts">
import { useCountStore } from '@/store/count';
const countStore = useCountStore()
countStore.$patch({ count: 5 })
</script>

使用$patch方法将 store 中的count状态更新为5

$patch 的优点是批量更新,可以一次性更新多个状态属性,而不需要分别调用多个 actions 或直接修改状态属性。

countStore.$patch({ 
  count: 5,
  name: 'John',
  value: 'new Value!'
})

同时更新了countnamevalue这3个状态属性。

替换 state

在 Pinia 中,直接完全替换 store 的 state 会破坏其响应性。
因为 Pinia 的响应式系统是基于对特定状态对象的跟踪和变化检测来实现的。如果完全替换了这个对象,响应式系统将无法正确地检测到变化,从而导致相关的组件不能自动更新。

<script setup lang="ts">
import { useCountStore } from '@/store/count';
const countStore = useCountStore()
// 直接替换 state 对象
countStore.$state = { count: 10 }
</script>

在这个例子中,直接将整个 state 对象替换为一个新的对象,这样会导致组件中使用 countStore.count 的地方不会自动更新,因为响应式系统无法检测到这种替换操作。

不管是Vue2 还是 Vue3,直接替换 state 都会破坏响应性。

可以使用 $patch 方法:利用$patch 方法批量更新的特性,可以全部更新store 的 state,也可以只进行部分更新,而不会破坏响应性。它接受一个对象或一个函数作为参数,用于描述要进行的更新操作。

// 接受一个对象
countStore.$patch({ count: 5 });
// 或者使用函数形式
countStore.$patch(state => {
  state.count = state.count + 1;
});
  1. 变更 pinia 实例的 state 来设置整个应用的初始 state
import { createPinia } from 'pinia';
const pinia = createPinia();

// 直接修改pinia.state.value来设置应用的初始状态
pinia.state.value = {
  someStoreKey: {
    someProperty: initialValue,
  },
};

重置State

  1. 如果Pinia Store 是使用 Option Store(选项式 API) ,调用 store 的 $reset() 方法将 state 重置为初始值。
<script setup lang="ts">
import { useCountStore } from '@/store/count';
const countStore = useCountStore()
// 打印countStore,可以看到$reset 属性
console.log(countStore)

const resetCountStore = () => {
  countStore.$reset()
}
</script>

$reset() 内部,会调用 state() 函数来创建一个新的状态对象,并用它替换当前状态。

  1. 如果Pinia Store 是使用Setup Stores ,需要创建 store 自己的 $reset() 方法:
export const useCountStore = defineStore('count', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)
  function increment() {
    count.value++
  }
  function $reset() {
    count.value = 0
  }
  
  // 把要在组件中使用到的属性、方法暴露出去
  return { count, doubleCount, increment }
})

如果 state 的属性 既有使用ref()定义的,也有reactive()定义的,可以在 $reset() 使用 isRef()isReactive() 检查类型:

  • isRef()检查某个值是否为 ref。
  • isReactive()检查一个对象是否是由 reactive()shallowReactive() 创建的代理。

订阅 state

  1. 通过 store 的 $subscribe() 方法侦听 state 及其变化。
  • $subscribe接受一个回调函数作为参数,这个回调函数会在 store 的状态发生变化时被调用。
  • $subscribe方法的返回值是一个函数,用于取消订阅。
  • 使用 $subscribe() 的好处是 subscriptions 在 patch 后只触发一次。
const countStore = useCountStore()
countStore.$subscribe((mutation, state) => {
  // import { MutationType } from 'pinia'
  // mutation.type // 'direct' | 'patch object' | 'patch function'
  // 和 countStore.$id 一样
  // mutation.storeId // 'count'
  // 只有 mutation.type === 'patch object'的情况下才可用
  // mutation.payload // 传递给 countStore.$patch() 的补丁对象。
  
  console.log(mutation, state)
  if (mutation.type === 'direct') {
    console.log(`直接修改, mutation.type 是 'direct'`);
  } else if (mutation.type === 'patch object') {
    console.log(`使用对象形式的 $patch修改状态,mutation.type 是 'patch object'`);
    console.log('补丁对象:', mutation.payload);
  } else if (mutation.type === 'patch function') {
    console.log(`使用函数形式的 $patch修改状态,mutation.type 是 'patch function'`);
  }
})

$subscribe回调函数参数:

  • mutation:包含了关于状态变化的信息,比如变化的类型和路径。
    • mutation.type:以获取状态变化的类型。值有:
      • 'direct':直接修改状态
      • 'patch object':使用对象形式的 $patch 修改状态
      • 'patch function':使用函数形式的 $patch 修改状态
    • mutation.storeId:获取当前 store 的唯一标识符。比如示例中的 'count'
    • mutation.payload:仅在 mutation.type === 'patch object' 的情况下可用,它是传递给 countStore.$patch() 的补丁对象,包含了用于更新状态的属性和值。
  • state:当前的 store 状态。

例如,当使用以下方式修改 store 的状态时:

// 直接修改, mutation.type 是 'direct'
countStore.count++;

// 使用对象形式的 $patch修改状态,mutation.type 是 'patch object'
countStore.$patch({ count: countStore.count + 1 });

// 使用函数形式的 $patch修改状态,mutation.type 是 'patch function'
countStore.$patch(state => {
  state.count= 15;
});

$subscribe 回调函数会被触发,根据状态变化的类型记录相应的信息。
在这里插入图片描述


在 Pinia 中,如果在组件的 setup() 方法中使用 $subscribe 来订阅 store 的状态变化,默认情况下,这个订阅会被绑定到添加它们的组件上。只要组件存在,订阅就会一直有效。当该组件被卸载时,订阅也会自动被删除。

如果想在组件卸载后依旧保留状态订阅,可以将 { detached: true } 作为第二个参数传递给 $subscribe 方法,以将状态订阅从当前组件中分离:

<script setup>
const countStore = useCountStore()
// 此订阅器即便在组件卸载之后仍会被保留
const unsubscribe = countStore.$subscribe(callback, { detached: true })

// 手动取消订阅
unsubscribe()
</script>

当组件被卸载时,订阅不会被自动删除。可以在适当的时候调用 unsubscribe() 方法来手动取消订阅。

  1. 可以在组件中使用watch监视 state
watch(countStore.$state, (newValue, oldValue) => {
  console.log(newValue, oldValue)
})

Getter

在 Pinia 中,Getter 用于从 store 的状态中派生新的数据或者对状态进行计算。
Getter 完全等同于 store 的 state 的计算值。
可以通过 defineStore() 中的 getters 属性来定义。

  1. getter使用箭头函数
import { defineStore } from "pinia";

export const useCountStore = defineStore('count', {
  state() {
    return {
      count: 0,
    }
  },
  getters: {
    // 推荐使用箭头函数,箭头函数没有this
    // state作为 getter 的第一个参数
    doubleCount: (state) => state.count * 2,
  }
})

在这个例子中,定义了一个名为 doubleCount 的 Getter,它返回 count 状态属性的两倍。

注意:getter 使用箭头函数,箭头函数没有thisthis的值是undefined

  1. getter使用常规函数
    如果要在getter 中使用this来访问 store 实例,那要使用使用常规函数定义 getter(在TypeScript中,必须明确getter的返回类型):
import { defineStore } from "pinia";
export const useCountStore = defineStore('count', {
  state() {
    return {
      count: 0,
    }
  },
  getters: {
    doubleCount():number {
      return this.count * 2
    }
  }
})

在组件中使用:

<template>
  <div>
    <p>Count: {{ countStore.count }}</p>
    <p>Double Count: {{ countStore.doubleCount }}</p>
  </div>
</template>

<script setup lang="ts">
import { useCountStore } from '@/store/count';
const countStore = useCountStore()
</script>

getters 具有缓存功能,只要它们依赖的状态没有发生变化,多次调用同一个 getter 将返回缓存的值,而不会重复执行计算逻辑。

访问其它getter

通常情况下,getter 主要依赖于 store 的 state 来派生新的数据。
有时候,getter 也可以使用其他 Getter 来进行更复杂的计算。

import { defineStore } from "pinia";
export const useCountStore = defineStore('count', {
  state() {
    return {
      count: 0,
    }
  },
  getters: {
    // 自动推断出返回类型是一个 number
    doubleCount(state) {
      return state.count * 2
    },
    // 自动推断出返回类型是一个 number
    tripleCount(state) {
      return state.count * 3
    },
    // 返回类型**必须**明确设置
    combinedCount(): number {
      // 整个 store 的 自动补全和类型标注 
      return this.doubleCount + this.tripleCount
    },
  }
})

在这个例子中,combinedCount getter 使用普通函数的形式定义,因此,在函数内部可以正确地使用 this 访问其他 getterstate 属性。
此时,this指向store实例 useCountStore

在组件中使用:

<template>
  <div>
    <p>Count: {{ countStore.count }}</p>
    <p>Double Count: {{ countStore.doubleCount }}</p>
    <p>Triple Count: {{ countStore.tripleCount }}</p>
    <p>Combined Count: {{ countStore.combinedCount }}</p>
  </div>
</template>

<script setup lang="ts">
import { useCountStore } from '@/store/count';
const countStore = useCountStore()
</script>

渲染结果:
在这里插入图片描述

向 getter 传递参数(让getter返回一个函数)

Getter 只是幕后的计算属性,所以不可以向它们传递任何参数。
可以从 getter 返回一个函数,该函数可以接受任意参数
当 getter 返回一个函数,getter 将不再被缓存。

import { defineStore } from "pinia";
export const useCountStore = defineStore('count', {
  state() {
    return {
      count: 0,
    }
  },
  getters: {
    doubleCount: (state) =>  state.count * 2,
    tripleCount: (state) =>  state.count * 3,
    getDoubleSum: (state) => {
      const getSum = function(flag: boolean) {
        if(flag) {
          const store = useCountStore();
          return (store.doubleCount + store.tripleCount) * 2;
        }
      }
      return getSum;
    }
  },
})

在组件中使用:

<template>
  <div>
    <p>Count: {{ countStore.count }}</p>
    <p>Double Count: {{ countStore.doubleCount }}</p>
    <p>Triple Count: {{ countStore.tripleCount }}</p>
    <p>Double Sum: {{ countStore.getDoubleSum(true) }}</p>
  </div>
</template>

<script setup lang="ts">
import { useCountStore } from '@/store/count';
const countStore = useCountStore()
</script>

访问其他 store 的 getter

想要使用另一个 store 的 getter 的话,那就直接在 getter 内使用就好:

import { useOtherStore } from '@/store/other-store'

export const useStore = defineStore('main', {
  state() {
    return {
      count: 0,
    }
  },
  getters: {
    otherGetter(state) {
      const otherStore = useOtherStore()
      return state.count + otherStore.data
    },
  },
})

在组件中访问getter

  1. 默认情况下,通过 store 实例访问 getter
  2. 在Vue3的选项式API中,可以使用 mapState() 辅助函数函数来将其映射为 getters:
<script>
import { mapState } from 'pinia';
import { useCountStore } from '@/store/count';
export default {
  computed: {
    // 使用数组形式
    // 允许在组件中访问 this.doubleCount
    // 与从 store.doubleCount 中读取的相同
    ...mapState(useCounterStore, ['doubleCount']),
    // 使用对象形式
    ...mapState(useCounterStore, {
      myDoubleCount: 'doubleCount',
      // 你也可以写一个函数来获得对 store 的访问权
      double: (store) => store.doubleCount,
    }),
  }
};
</script>

Action

Action 相当于组件中的 method。它们可以通过 defineStore() 中的 actions 属性来定义,并且它们也是定义业务逻辑的完美选择

import { defineStore } from "pinia";
export const useCountStore = defineStore('count', {
  state() {
    return {
      count: 0,
    }
  },
  actions: {
    increment() {
      this.count++
    }
  }
})

action 可以通过 this 访问整个 store 实例,并支持完整的类型标注(以及自动补全)

在组件中使用:

<template>
  <!-- 即使在模板中也可以 -->
  <button @click="countStore.increment()">Randomize</button>
</template>
<script setup>
import { useCountStore } from '@/store/count';
const countStore = useCountStore()
// 将 action 作为 store 的方法进行调用
countStore.increment()
</script>

Action 可以执行异步操作:

  • 使用 asyncawait:可以在 Action 的定义中使用 async 关键字来标记该函数为异步函数,然后在函数内部使用 await 来等待异步操作的完成。
actions: {
  async fetchDogImage() {
    try {
      const response = await fetch('https://dog.ceo/api/breeds/image/random');
      console.log('response', response)
      const data = await response.json();
      console.log(data)
      this.dogImage = data.message
    }catch (err) {
      console.log(err)
    }
  },
},

在组件中使用:

<template>
  <img v-if="countStore.dogImage" :src="countStore.dogImage" alt="">
</template>
<script setup>
import { useCountStore } from '@/store/count';
const countStore = useCountStore()
// 将 action 作为 store 的方法进行调用
countStore.fetchDogImage()
</script>
  • 返回 Promise:异步 Action 也可以返回一个 Promise,在调用 Action 的地方进行进一步的处理。
actions: {
  async fetchDogImage() {
     return await fetch('https://dog.ceo/api/breeds/image/random');
  },
},

访问其他 store 的 action

想要使用另一个 store 的话,直接在 action 中调用:

import { useOtherStore } from '@/store/other-store'
export const useStore = defineStore('main', {
  state() {
    return {
      count: 0,
    }
  },
  actions: {
    increment() {
      const otherStore = useOtherStore()
      this.count = this.count + otherStore.data
    }
  }
})

在组件中访问Action

  1. 默认情况下,将 action 作为 store 的方法进行调用
  2. 在Vue3的选项式API中,使用 mapActions() 辅助函数将 action 属性映射为组件中的方法:
import { mapActions } from 'pinia'
import { useCountStore } from '@/store/count';
export default {
  methods: {
    // 访问组件内的 this.increment()
    // 与从 store.increment() 调用相同
    ...mapActions(useCountStore, ['increment'])
    // 与上述相同,但将其注册为this.myOwnIncrement()
    ...mapActions(useCounterStore, { myOwnIncrement: 'increment' }),
  },
}

在组件中给 increment Action 取一个别名myOwnIncrement,可以通过this.myOwnIncrement()调用 increment Action。

订阅 action

通过someStore.$onAction(...)来为特定的 Pinia store(这里假设名为someStore)的 actions 进行监听传入的回调函数接收一个包含多个属性的对象,这些属性提供了关于正在执行的 action 的信息:

  • name:action 的名称。
  • store:当前的 store 实例,类似于外部的someStore
  • args:传递给 action 的参数数组。
  • after回调函数
    • 执行时机:在 action 的 promise 解决之后执行,即当 action 成功完成时。
    • 用途:允许在 action 解决后执行一个回调函数。例如,可以在这个回调函数中进行一些后续处理,如更新 UI、发送通知等。
  • onError回调函数
    • 执行时机:当 action 抛出错误或 promise 被 reject 时执行。
    • 用途:允许你在 action 出现错误时执行一个回调函数。例如,你可以在这个回调函数中记录错误日志、显示错误消息给用户、进行错误处理等。

someStore.$onAction(...)的返回值 是一个函数,用于取消订阅。
传递给 someStore.$onAction()的回调函数会在 action 本身之前执行。这意味着可以在 action 执行之前进行一些预处理或设置一些状态。

示例:

<template>
  <div>
    <p>Count: {{ countStore.count }}</p>
    <img v-if="countStore.dogImage" :src="countStore.dogImage" alt="">
    <button @click="countStore.increment">Increment</button>
    <button @click="countStore.fetchDogImage">换图</button>
  </div>
</template>

<script setup lang="ts">
import { useCountStore } from '@/store/count';
const countStore = useCountStore()

// 订阅 actions
const unsubscribe = countStore.$onAction(
  ({
    name, // action 名称
    store, // store 实例,类似 `someStore`
    args, // 传递给 action 的参数数组
    after, // 在 action 返回或解决后的钩子
    onError, // action 抛出或拒绝的钩子
  }) => {
  
  // 这将在执行 store 的 action 之前触发。
  // 此时,已经调用 store 的action
  console.log(`Action "${name}" is triggered.`);
  
  // 这将在 action 成功并完全运行后触发。
  // 它等待着任何返回的 promise
  after((result) => {
    console.log(`Action "${name}" completed with result:`, result);
  });
  // 在 action 抛出或返回一个拒绝的 promise 时触发
  onError((error) => {
    console.error(`Action "${name}" failed with error:`, error);
  });
});
countStore.fetchDogImage()


// 手动删除监听器
unsubscribe()
</script>

插件

由于有了底层 API 的支持,Pinia store 现在完全支持扩展。以下是Pinia store 支持扩展的内容:

  • 为 store 添加新的属性
  • 定义 store 时增加新的选项
  • 为 store 增加新的方法
  • 包装现有的方法
  • 改变甚至取消 action
  • 实现副作用,如本地存储
  • 仅应用插件于特定 store

Pinia 插件是一个函数,可以选择性地返回要添加到 store 的属性。
Pinia 插件接收一个可选参数,即 context对象,这个对象包含了在 Pinia 应用中不同阶段的各种信息,允许插件在不同的上下文中进行操作和扩展。

  • context.pinia:这是使用createPinia()创建的 Pinia 实例。通过这个属性,插件可以访问 Pinia 的全局状态和方法,例如获取其他存储或应用插件的全局配置。
  • context.app(仅在 Vue 3 中可用):这是使用createApp()创建的当前应用实例。这个属性允许插件与 Vue 应用进行交互,例如访问应用的全局配置、注册全局组件或插件等。
  • context.store:这是当前插件想要扩展的存储实例。插件可以通过这个属性访问和修改存储的状态、动作(actions)和获取器(getters)等。
  • context.options:这是定义传给defineStore()的存储的可选对象。这个属性包含了存储的初始配置,可以在插件中进行修改或扩展,以改变存储的行为。

创建 Pinia 实例后,可以使用 pinia.use() 把插件添加到 Pinia 中。
在应用中创建的每个 store 都会应用这个插件。

创建一个用于日志记录的Pinia 插件:

export function myLoggingPlugin(context) {
  const { store } = context;

  const originalActions = store.$actions;

  Object.keys(originalActions).forEach((actionName) => {
    const originalAction = originalActions[actionName];

    store.$actions[actionName] = async function (...args) {
      console.log(`Before executing action "${actionName}" with args: ${args}`);
      const result = await originalAction.apply(this, args);
      console.log(`After executing action "${actionName}". Result: ${result}`);
      return result;
    };
  });
}

在这个示例中,插件在每个动作执行前后打印日志。

然后通过 pinia.use() 在 Pinia 中应用这个插件:

import { createPinia } from 'pinia';

const pinia = createPinia();
pinia.use(myLoggingPlugin);

只有在 Pinia 实例被应用后新创建的 store 才会应用Pinia插件。

扩展 Store

  1. 可以直接通过在一个插件中返回包含特定属性的对象来为每个 store 都添加上特定属性:
// 使用返回对象的方法, hello 能被 devtools 自动追踪到
pinia.use(() => ({ hello: 'world' }))

通过vue devtools 查看Pinia:
在名为countstore 中,hello属性已经自动被添加到store._customProperties中。devtools会自动追踪hello属性。
在这里插入图片描述

  1. 可以直接在 store 上设置属性,这种方式设置的属性不会被devtools自动追踪
pinia.use(({ store }) => {
  store.hello = 'world'
})

通过vue devtools 查看Pinia:
hello属性没有被添加到store._customProperties中,不会被devtools自动追踪。
在这里插入图片描述

如果想在 devtools 中调试 hello 属性,为了使 devtools 能追踪到 hello,确保在 dev 模式下将其添加到 store._customProperties 中:

pinia.use(({ store }) => {
  store.hello = 'world'
  // 确保你的构建工具能处理这个问题,webpack 和 vite 在默认情况下应该能处理。
  if (process.env.NODE_ENV === 'development') {
    // 添加你在 store 中设置的键值
    store._customProperties.add('hello')
  }
})

通过vue devtools 查看Pinia:
hello属性已经被添加到store._customProperties中。
在这里插入图片描述如果process报错:找不到名称“process”。这个错误提示表明在 TypeScript 项目中,缺少对 process 对象的类型定义。
解决方案:在tsconfig.json"compilerOptions" 属性中配置 "types": ["node"]

{
  "compilerOptions": {
    //...其他配置
    "types": ["node"]
  }
}

插件的更多功能请查看Pinia官网的插件

  • 23
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值