Vue3 官方推荐状态管理库Pinia
介绍
Pinia 是 Vue 官方团队推荐代替Vuex的一款轻量级状态管理库,允许跨组件/页面共享状态。
Pinia 旨在提供一种更简洁、更直观的方式来处理应用程序的状态。
Pinia 充分利用了 Vue 3 的 Composition API。
官网:
Pinia符合直觉的 Vue.js 状态管理库
Pinia的核心概念
store
:是存储状态(共享数据)的地方。- 是一个保存状态和业务逻辑的实体。它承载着全局状态。
- 每个组件都可以读取/写入。
- 官方推荐使用 hooks 的命名方式,以
use
开头。例如:useCountStore
、useUserStore
,useCartStore
,useProductStore
。
state
:是 store 中用于存储应用状态的部分。- 通俗来讲,
state
是真正存储数据的地方,它就是存放在store里的数据。 - 官方要求
state
写成函数形式,并且要return
一个对象。
示例:state() { return {} }
- 通俗来讲,
getters
:从存储的状态中派生数据,类似于 Vue 中的计算属性(computed
)。- 是一种依赖于 store 状态并产生计算值的函数。这些值将被缓存,直到依赖的状态改变。
actions
:是用于改变状态的方法。
安装与配置 Pinia
- 通过npm或yarn安装Pinia:
npm install pinia
# 或者使用 yarn
yarn add pinia
- 在Vue应用文件中(通常是
main.js
或main.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()
- 第一个参数(store 的 ID)
- 这是一个字符串,用于唯一标识一个 store。
defineStore('count', {})
中的count
就是这个store的ID。 - 必须传入, Pinia 将用它来连接 store 和 devtools。
- 这是一个字符串,用于唯一标识一个 store。
- 第二个参数(配置对象)
- 可接受两类值:Setup 函数或 Option 对象。
- 这个对象包含了 store 的各种配置选项,主要有以下几个重要属性:
state
、actions
、getters
。
Option Store
与 Vue 的选项式 API 类似,可以传入一个带有 state
、actions
与 getters
属性的 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
访问state
的count
属性:
// 直接访问, 不需要使用.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
。
- 这是因为 Vue 的响应式系统会自动处理这种嵌套的情况。当访问
- 当从 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
包装。
解构出来的属性都是放在state
、getter
里面的数据。
为什么不使用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
- 默认情况下,通过 store 实例访问 state,直接对其进行读写。
<script setup lang="ts">
import { useCountStore } from '@/store/count';
// 一旦 store 被实例化,可以直接访问在 store 的 state、getters 和 actions 中定义的任何属性。
const countStore = useCountStore()
countStore.count ++
</script>
- 在 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.count
和this.doubleCount
,并返回它们的组合结果。 - 注意,这里的
magicValue
函数没有明确的类型标注,可能会在某些情况下导致类型不明确的问题。
- 在这个函数中,可以访问
- 在 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 的多个状态属性计算出一个新的属性值。
- 使用
storeToRefs
函数,确保解构出来的 state 属性保持响应性。
注意
新的属性如果没有在 state()
中被定义,则不能被添加。它必须包含初始状态。
例如:如果 secondCount
没有在 state()
中定义,无法执行 useCountStore .secondCount= 2
。
- 明确的初始状态:通过要求所有可访问的属性在
state()
中定义并包含初始状态,Pinia 确保了应用在任何时候都有一个明确的、已知的状态起点。 - 可控的状态变更:只允许修改在
state()
中定义的属性,可以防止意外地引入新的状态变量,从而降低了由于错误的状态修改而导致的错误风险。 - 正确的响应式更新:Pinia 的响应式系统依赖于对已知状态属性的跟踪。只有在
state()
中定义的属性才能被 Pinia 的响应式系统正确地跟踪和更新,确保了状态变化能够及时反映在界面上。
修改State
- 直接修改
<script setup lang="ts">
import { useCountStore } from '@/store/count';
const countStore = useCountStore()
countStore.count ++
</script>
- 使用
$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!'
})
同时更新了count
、name
、value
这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;
});
- 变更
pinia
实例的state
来设置整个应用的初始state
import { createPinia } from 'pinia';
const pinia = createPinia();
// 直接修改pinia.state.value来设置应用的初始状态
pinia.state.value = {
someStoreKey: {
someProperty: initialValue,
},
};
重置State
- 如果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()
函数来创建一个新的状态对象,并用它替换当前状态。
- 如果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
- 通过 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()
方法来手动取消订阅。
- 可以在组件中使用
watch
监视state
watch(countStore.$state, (newValue, oldValue) => {
console.log(newValue, oldValue)
})
Getter
在 Pinia 中,Getter 用于从 store 的状态中派生新的数据或者对状态进行计算。
Getter 完全等同于 store 的 state 的计算值。
可以通过 defineStore()
中的 getters
属性来定义。
- 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 使用箭头函数,箭头函数没有this
,this
的值是undefined
。
- 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
访问其他 getter
和 state
属性。
此时,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
- 默认情况下,通过 store 实例访问 getter
- 在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 可以执行异步操作:
- 使用
async
和await
:可以在 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
- 默认情况下,将 action 作为 store 的方法进行调用
- 在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
- 可以直接通过在一个插件中返回包含特定属性的对象来为每个 store 都添加上特定属性:
// 使用返回对象的方法, hello 能被 devtools 自动追踪到
pinia.use(() => ({ hello: 'world' }))
通过vue devtools 查看Pinia:
在名为count
的store
中,hello
属性已经自动被添加到store._customProperties
中。devtools会自动追踪hello
属性。
- 可以直接在 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官网的插件