Pinia
起始于 2019 年 11 月左右的一次实验,其目的是设计一个拥有组合式 API 的 Vue 状态管理库。同时支持 Vue 2 和 Vue 3,并且不强制要求开发者使用组合式 API。
pinia官网链接地址。
Pinia
是 Vue 的专属状态管理库,它允许你跨组件或页面共享状态。对于单页应用来说,可以通过一行简单的 export const state = reactive({})
来共享一个全局状态。如果应用在服务器端渲染,这可能会使你的应用暴露出一些安全漏洞。
如果使用 Pinia
,即使在小型单页应用
中,你也可以获得如下功能:
Devtools
支持- 追踪
actions
、mutations
的时间线 - 在组件中展示它们所用到的
Store
- 让调试更容易的
Time travel
- 追踪
热更新
不必重载页面
即可修改 Store- 开发时可保持当前的 State
- 插件:可通过插件扩展
Pinia
功能 - 为 JS 开发者提供适当的 TypeScript 支持以及自动补全功能。
- 支持服务端渲染
基础示例
下面就是 pinia API 的基本用法。你可以先创建一个 Store:
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => {
return { count: 0 }
},
// 也可以这样定义
// state: () => ({ count: 0 })
actions: {
increment() {
this.count++
},
},
})
然后你就可以在一个组件中使用该 store
了:
// 需要使用的页面
import { useCounterStore } from '@/stores/counter'
export default {
setup() {
const counter = useCounterStore()
counter.count++
// 带有自动补全
counter.$patch({ count: counter.count + 1 })
// 或者使用 action 代替
counter.increment()
},
}
为实现更多高级用法,你甚至可以使用一个函数(与组件 setup() 类似)来定义一个 Store:
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
})
如果你还不熟悉 setup() 函数和组合式 API,别担心,Pinia 也提供了一组类似 Vuex 的 映射 state 的辅助函数。你可以用和之前一样的方式来定义 Store,然后通过 mapStores()、mapState() 或 mapActions() 访问
const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
const useUserStore = defineStore('user', {
// ...
})
export default {
computed: {
// 其他计算属性
// ...
// 允许访问 this.counterStore 和 this.userStore
...mapStores(useCounterStore, useUserStore)
// 允许读取 this.count 和 this.double
...mapState(useCounterStore, ['count', 'double']),
},
methods: {
// 允许读取 this.increment()
...mapActions(useCounterStore, ['increment']),
},
}
你将会在核心概念部分了解到更多关于每个映射辅助函数
的信息。
对比 Vuex
Pinia 起源于一次探索 Vuex 下一个迭代的实验,因此结合了 Vuex 5 核心团队讨论中的许多想法。最后,我们意识到 Pinia 已经实现了我们在 Vuex 5 中想要的大部分功能,所以决定将其作为新的推荐方案来代替 Vuex。
与 Vuex 相比,Pinia 不仅提供了一个更简单的 API,也提供了符合组合式 API 风格的 API,最重要的是,搭配 TypeScript 一起使用时有非常可靠的类型推断支持。
对比 Vuex 3.x/4.x
Vuex 3.x 只适配 Vue 2,而 Vuex 4.x 是适配 Vue 3 的。
Pinia API 与 Vuex(<=4) 也有很多不同,即:
mutation 已被弃用
。它们经常被认为是极其冗余
的。它们初衷是带来 devtools 的集成方案,但这已不再是一个问题了。- 无需要创建自定义的复杂包装器来支持 TypeScript,一切都可标注类型,API 的设计方式是尽可能地利用 TS 类型推理。
- 无过多的魔法字符串注入,只需要导入函数并调用它们,然后享受自动补全的乐趣就好。
- 无需要动态添加 Store,它们
默认都是动态的
,甚至你可能都不会注意到这点。注意,你仍然可以在任何时候手动使用一个 Store 来注册它,但因为它是自动的,所以你不需要担心它。 - 不再有嵌套结构的
模块
。你仍然可以通过导入和使用另一个 Store 来隐含地嵌套 stores 空间,虽然是 Pinia 从设计上提供的是一个扁平的结构,但仍然能够在 Store 之间进行交叉组合。你甚至可以让 Stores 有循环依赖关系
。 - 不再有
可命名的模块
。考虑到 Store 的扁平架构,Store 的命名取决于它们的定义方式,你甚至可以说所有 Store 都应该命名。
安装
用你喜欢的包管理器安装 pinia:
yarn add pinia
# 或者使用 npm
npm install pinia
TIP
如果你的应用使用的是 Vue 2,你还需要安装组合式 API 包:@vue/composition-api
。如果你使用的是 Nuxt,你应该参考这篇指南。
如果你正在使用 Vue CLI,你可以试试这个非官方插件。
创建一个 pinia 实例(根 store)并将其传递给应用:
import { createApp } from 'vue'
import { createPinia } from 'pinia' // 引入pinia
import App from './App.vue'
const app = createApp(App)
app.use(createPinia()) // 使用 pinia
app.mount('#app')
如果你使用的是 Vue 2,你还需要安装一个插件,并在应用的根部注入创建的 pinia:
import { createPinia, PiniaVuePlugin } from 'pinia'
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
el: '#app',
// 其他配置...
// ...
// 请注意,同一个`pinia'实例
// 可以在同一个页面的多个 Vue 应用中使用。
pinia,
})
因此不能与 Vuex 一起使用
这样才能提供 devtools 的支持。在 Vue 3 中,一些功能仍然不被支持,如 time traveling 和编辑,这是因为 vue-devtools 还没有相关的 API,但 devtools 也有很多针对 Vue 3 的专属功能,而且就开发者的体验来说,Vue 3 整体上要好得多。在 Vue 2 中,Pinia 使用的是 Vuex 的现有接口(因此不能与 Vuex 一起使用
)。
Store 是什么?
Store (如 Pinia)
是一个保存状态
和业务逻辑的实体
,它并不与你的组件树绑定
。换句话说,它承载着全局状态
。它有点像一个永远存在的组件,每个组件都可以读取和写入它。它有三个概念,state
、getter
和 action
,我们可以假设这些概念相当于组件中的 data
、 computed
和 methods
。
应该在什么时候使用 Store?
一个 Store 应该包含可以在整个应用中访问的数据。这包括在许多地方使用的数据,例如显示在导航栏中的用户信息
,以及需要通过页面保存的数据,例如一个非常复杂的多步骤表单
。
另一方面,你应该避免在 Store 中引入那些原本可以在组件中保存的本地数据
,例如,一个元素在页面中的可见性
。
并非所有的应用都需要访问全局状态
,但如果你的应用确实需要一个全局状态,那 Pinia 将使你的开发过程更轻松。
Pinia 核心概念
Store
定义 Store
在深入研究核心概念之前,我们得知道 Store 是用 defineStore()
定义的,它的第一个参数要求是一个独一无二
的名字:
import { defineStore } from 'pinia'
// 你可以对 `defineStore()` 的返回值进行任意命名,但最好使用 store 的名字,同时以 `use` 开头且以 `Store` 结尾。(比如 `useUserStore`,`useCartStore`,`useProductStore`)
// 第一个参数是你的应用中 Store 的唯一 ID。
export const useStore = defineStore('main', {
// 其他配置...
})
这个名字
,也被用作 id ,是必须传入的
, Pinia 将用它来连接 store 和 devtools。为了养成习惯性的用法,将返回的函数命名为 use... 是一个符合组合式函数风格
的约定。
defineStore() 的第二个参数可接受两类值
:Setup 函数
或 Option 对象
。
Option Store
与 Vue 的选项式 API 类似,我们也可以传入一个带有 state
、actions
与 getters
属性的 Option 对象
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
你可以认为 state 是 store 的数据 (data)
,getters 是 store 的计算属性 (computed)
,而 actions 则是方法 (methods)
。
为方便上手使用,Option Store
应尽可能直观简单
。
Setup Store
也存在另一种定义 store 的可用语法。与 Vue 组合式 API 的 setup 函数 相似,我们可以传入一个函数
,该函数定义了一些响应式属性
和方法
,并且返回一个带有我们想暴露出去的属性和方法的对象。
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
})
在 Setup Store
中:
ref()
就是state 属性
computed()
就是getters
function()
就是actions
Setup store 比 Option Store 带来了更多的灵活性,因为你可以在一个 store 内创建侦听器,并自由地使用任何组合式函数。不过,请记住,使用组合式函数会让 SSR 变得更加复杂。
应该选用哪种语法?
和在 Vue 中如何选择组合式 API 与选项式 API 一样,选择你觉得最舒服的那一个就好
。如果你还不确定,可以先试试 Option Store
。
使用 Store
虽然我们前面定义了一个 store
,但在 setup() 调用 useStore() 之前
,store 实例是不会被创建的:
import { useCounterStore } from '@/stores/counter'
export default {
setup() {
const store = useCounterStore()
return {
// 为了能在模板中使用它,你可以返回整个 Store 实例。
store,
}
},
}
你可以定义任意多的 store
,但为了让使用 pinia 的益处最大化
(比如允许构建工具自动进行代码分割以及 TypeScript 推断),你应该在不同的文件中去定义 store
。
如果你还不会使用 setup 组件,你也可以通过映射辅助函数
来使用 Pinia。
一旦 store 被实例化,你可以直接访问在 store 的 state、getters 和 actions 中定义的任何属性。我们将在后续章节继续了解这些细节,目前自动补全将帮助你使用相关属性。
请注意,store 是一个用 reactive 包装的对象,这意味着不需要在 getters 后面写 .value
,就像 setup 中的 props 一样,如果你写了,我们也不能解构它
:
export default defineComponent({
setup() {
const store = useCounterStore()
// ❌ 这将无法生效,因为它破坏了响应性
// 这与从 `props` 中解构是一样的。
const { name, doubleCount } = store
name // "eduardo"
doubleCount // 2
return {
// 始终是 "eduardo"
name,
// 始终是 2
doubleCount,
// 这个将是响应式的
doubleValue: computed(() => store.doubleCount),
}
},
})
为了从 store 中提取属性时保持其响应性,你需要使用 storeToRefs()
。它将为每一个响应式属性
创建引用。当你只使用 store 的状态而不调用任何 action 时,它会非常有用。请注意,你可以直接从 store 中解构 action,因为它们也被绑定到 store 上:
import { storeToRefs } from 'pinia'
export default defineComponent({
setup() {
const store = useCounterStore()
// `name` and `doubleCount` 都是响应式 refs
// 这也将为由插件添加的属性创建 refs
// 同时会跳过任何 action 或非响应式(非 ref/响应式)属性
const { name, doubleCount } = storeToRefs(store)
// 名为 increment 的 action 可以直接提取
const { increment } = store
return {
name,
doubleCount,
increment,
}
},
})
State
在大多数情况下,state 都是你的 store 的核心
。人们通常会先定义能代表他们 APP 的 state。在 Pinia 中,state 被定义为一个返回初始状态的函数
。这使得 Pinia 可以同时支持服务端
和客户端
。
import { defineStore } from 'pinia'
const useStore = defineStore('storeId', {
// 为了完整类型推理,推荐使用箭头函数
state: () => {
return {
// 所有这些属性都将自动推断出它们的类型
count: 0,
name: 'Eduardo',
isAdmin: true,
items: [],
hasChanged: true,
}
},
})
TIP
如果你使用的是 Vue 2,你在 state 中创建的数据与 Vue 实例中的 data 遵循同样的规则,即 state 对象必须是清晰的,当你想向其添加新属性时,你需要调用 Vue.set() 。参考:Vue#data。
访问 state
默认情况下,你可以通过 store
实例访问 state,直接对其进行读写
。
const store = useStore()
store.count++
重置 state
你可以通过调用 store 的 $reset() 方法将 state 重置为初始值。
const store = useStore()
store.$reset()
使用选项式 API 的用法
在下面的例子中,你可以假设相关 store 已经创建了:
/ 示例文件路径:
// ./src/stores/counter.js
import { defineStore } from 'pinia'
const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
})
如果你不能使用组合式 API,但你可以使用 computed
,methods
,...,那你可以使用 mapState()
辅助函数将 state 属性映射为只读的计算属性:
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.someGetter + this.count + this.double
},
}),
},
}
可修改的 state
如果你想修改这些 state 属性
(例如,如果你有一个表单),你可以使用 mapWritableState()
作为代替。但注意你不能像 mapState() 那样传递一个函数:
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',
}),
},
}
TIP
对于像数组
这样的集合,你并不一定需要使用 mapWritableState()
,mapState()
也允许你调用集合上的方法,除非你想用 cartItems = [] 替换整个数组。
变更 state
除了用 store.count++ 直接改变 store
,你还可以调用 $patch 方法
。它允许你用一个 state 的补丁对象在同一时间更改多个属性
:
store.$patch({
count: store.count + 1,
age: 120,
name: 'DIO',
})
不过,用这种语法的话,有些变更真的很难实现或者很耗时
:任何集合的修改(例如,向数组中添加、移除一个元素或是做 splice 操作)都需要你创建一个新的集合。因此,$patch 方法
也接受一个函数
来组合这种难以用补丁对象实现的变更。
store.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})
两种变更 store 方法的主要区别是
$patch()
允许你将多个变更归入 devtools 的同一个条目中。同时请注意,直接修改
state,$patch() 也会出现在 devtools 中,而且可以进行 time travel(在 Vue 3 中还没有)。
替换 state
你不能完全替换掉
store 的 state,因为那样会破坏其响应性
。但是,你可以 patch 它。
// 这实际上并没有替换`$state`
store.$state = { count: 24 }
// 在它内部调用 `$patch()`:
store.$patch({ count: 24 })
你也可以通过变更 pinia 实例的 state 来设置整个应用的初始 state。这常用于 SSR 中的激活过程。
pinia.state.value = {}
订阅 state
类似于 Vuex 的 subscribe
方法,你可以通过 store 的 $subscribe()
方法侦听 state 及其变化。比起普通的 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))
})
默认情况下,state subscription 会被绑定到添加它们的组件上(如果 store 在组件的 setup() 里面)。这意味着,当该组件被卸载时,它们将被自动删除。如果你想在组件卸载后依旧保留它们,请将 { detached: true } 作为第二个参数,以将 state subscription 从当前组件中分离:
export default {
setup() {
const someStore = useSomeStore()
// 在组件被卸载后,该订阅依旧会被保留。
someStore.$subscribe(callback, { detached: true })
// ...
},
}
TIP
你可以在 pinia 实例上侦听整个 state。
watch(
pinia.state,
(state) => {
// 每当状态发生变化时,将整个 state 持久化到本地存储。
localStorage.setItem('piniaState', JSON.stringify(state))
},
{ deep: true }
)
Getter
Getter 完全等同于 store 的 state 的计算值。可以通过 defineStore()
中的 getters 属性来定义它们。推荐使用箭头函数
,并且它将接收 state 作为第一个参数
:
export const useStore = defineStore('main', {
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
},
})
大多数时候,getter 仅依赖 state,不过,有时它们也可能会使用其他 getter。因此,即使在使用常规函数定义 getter 时,我们也可以通过 this 访问到整个 store 实例
,但(在 TypeScript 中)必须定义返回类型。这是为了避免 TypeScript 的已知缺陷,不过这不影响用箭头函数定义的 getter,也不会影响不使用 this 的 getter。
export const useStore = defineStore('main', {
state: () => ({
count: 0,
}),
getters: {
// 自动推断出返回类型是一个 number
doubleCount(state) {
return state.count * 2
},
// 返回类型**必须**明确设置
doublePlusOne(): number {
// 整个 store 的 自动补全和类型标注 ✨
return this.doubleCount + 1
},
},
})
然后你可以直接访问 store 实例上的 getter
了:
<template>
<p>Double count is {{ store.doubleCount }}</p>
</template>
<script>
export default {
setup() {
const store = useStore()
return { store }
},
}
</script>
访问其他 getter
与计算属性一样,你也可以组合多个 getter。通过 this,你可以访问到其他任何 getter
。即使你没有使用 TypeScript,你也可以用 JSDoc 来让你的 IDE 提示类型。
export const useStore = defineStore('main', {
state: () => ({
count: 0,
}),
getters: {
// 类型是自动推断出来的,因为我们没有使用 `this`
doubleCount: (state) => state.count * 2,
// 这里我们需要自己添加类型(在 JS 中使用 JSDoc)
// 可以用 this 来引用 getter
/**
* 返回 count 的值乘以 2 加 1
*
* @returns {number}
*/
doubleCountPlusOne() {
// 自动补全
return `this`.doubleCount + 1
},
},
})
向 getter 传递参数
Getter 只是幕后的计算
属性,所以不可以向它们传递任何参数
。不过,你可以从 getter 返回一个函数
,该函数可以接受任意参数:
export const useStore = defineStore('main', {
getters: {
getUserById: (state) => {
return (userId) => state.users.find((user) => user.id === userId)
},
},
})
并在组件中使用
<script>
export default {
setup() {
const store = useStore()
return { getUserById: store.getUserById }
},
}
</script>
<template>
<p>User 2: {{ getUserById(2) }}</p>
</template>
请注意,当你这样做时,getter 将不再被缓存,它们只是一个被你调用的函数
。不过,你可以在 getter 本身中缓存一些结果,虽然这种做法并不常见,但有证明表明它的性能会更好:
export const useStore = defineStore('main', {
getters: {
getActiveUserById(state) {
const activeUsers = state.users.filter((user) => user.active)
return (userId) => activeUsers.find((user) => user.id === userId)
},
},
})
访问其他 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
},
},
})
使用 setup()时的用法
作为 store 的一个属性,你可以直接访问任何 getter(与 state 属性完全一样):
export default {
setup() {
const store = useStore()
store.count = 3
store.doubleCount // 6
},
}
使用选项式 API 的用法
在下面的例子中,你可以假设相关的 store 已经创建了:
// 示例文件路径:
// ./src/stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
doubleCount(state) {
return state.count * 2
},
},
})
使用 setup()
虽然并不是每个开发者都会使用组合式 API,但 setup() 钩子依旧可以使 Pinia 在选项式 API 中更易用。并且不需要额外的映射辅助函数!
import { useCounterStore } from '../stores/counter'
export default {
setup() {
const counterStore = useCounterStore()
return { counterStore }
},
computed: {
quadrupleCounter() {
return this.counterStore.doubleCount * 2
},
},
}
不使用 setup()
你可以使用前一节的 state 中的 mapState()
函数来将其映射为 getters:
import { mapState } from 'pinia'
import { useCounterStore } from '../stores/counter'
export default {
computed: {
// 允许在组件中访问 this.doubleCount
// 与从 store.doubleCount 中读取的相同
...mapState(useCounterStore, ['doubleCount']),
// 与上述相同,但将其注册为 this.myOwnName
...mapState(useCounterStore, {
myOwnName: 'doubleCount',
// 你也可以写一个函数来获得对 store 的访问权
double: store => store.doubleCount,
}),
},
}
Action
Action 相当于组件中的 method
。它们可以通过 defineStore() 中的 actions 属性来定义
,并且它们也是定义业务逻辑的完美选择。
export const useStore = defineStore('main', {
state: () => ({
count: 0,
}),
actions: {
increment() {
this.count++
},
randomizeCounter() {
this.count = Math.round(100 * Math.random())
},
},
})
类似 getter,action 也可通过 this 访问整个 store 实例
,并支持完整的类型标注(以及自动补全)。不同的是,action 可以是异步的
,你可以在它们里面 await
调用任何 API,以及其他 action!下面是一个使用 Mande 的例子。请注意,你使用什么库并不重要,只要你得到的是一个Promise
,你甚至可以(在浏览器中)使用原生 fetch 函数
:
import { mande } from 'mande'
const api = mande('/api/users')
export const useUsers = defineStore('users', {
state: () => ({
userData: null,
// ...
}),
actions: {
async registerUser(login, password) {
try {
this.userData = await api.post({ login, password })
showTooltip(`Welcome back ${this.userData.name}!`)
} catch (error) {
showTooltip(error)
// 让表单组件显示错误
return error
}
},
},
})
你也完全可以自由地设置任何你想要的参数以及返回任何结果。当调用 action
时,一切类型也都是可以被自动推断出来的。
Action 可以像方法一样被调用:
export default defineComponent({
setup() {
const main = useMainStore()
// 作为 store 的一个方法调用该 action
main.randomizeCounter()
return {}
},
})
访问其他 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')
}
},
},
})
使用 setup()时的用法
你可以将任何 action 作为 store 的一个方法直接调用:
export default {
setup() {
const store = useStore()
store.randomizeCounter()
},
}
使用选项式 API 的用法
在下面的例子中,你可以假设相关的 store 已经创建了:
// 示例文件路径:
// ./src/stores/counter.js
import { defineStore } from 'pinia',
const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
actions: {
increment() {
this.count++
}
}
})
使用 setup()
虽然并不是每个开发者都会使用组合式 API,但 setup() 钩子依旧可以使 Pinia 在选项式 API 中更易用。并且不需要额外的映射辅助函数!
import { useCounterStore } from '../stores/counter'
export default {
setup() {
const counterStore = useCounterStore()
return { counterStore }
},
methods: {
incrementAndPrint() {
this.counterStore.increment()
console.log('New Count:', this.counterStore.count)
},
},
}
不使用 setup()
如果你不喜欢使用组合式 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: 'doubleCount' }),
},
}
订阅 action
你可以通过 store.$onAction()
来监听 action 和它们的结果。传递给它的回调函数会在 action 本身之前执行。after 表示在 promise 解决之后,允许你在 action 解决后执行一个一个回调函数。同样地,onError 允许你在 action 抛出错误或 reject 时执行一个回调函数。这些函数对于追踪运行时错误非常有用,类似于Vue docs 中的这个提示。
这里有一个例子,在运行 action 之前以及 action resolve/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 成功并完全运行后触发。
// 它等待着任何返回的 promise
after((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()
默认情况下,action 订阅器会被绑定到添加它们的组件上(如果 store 在组件的 setup() 内)。这意味着,当该组件被卸载时,它们将被自动删除。如果你想在组件卸载后依旧保留它们,请将 true 作为第二个参数传递给 action 订阅器,以便将其从当前组件中分离:
export default {
setup() {
const someStore = useSomeStore()
// 在组件被卸载后,这个订阅依旧会被保留。
someStore.$onAction(callback, true)
// ...
},
}
优雅解决pinia数据持久化问题
pinia和vuex都有一个通病,就是数据持久化需要手动修改
,插件本身不具备数据持久化的能力,当页面刷新或者应用更新后所有的状态数据均会丢失。
pinia是支持插件的,不知道各位小伙伴看过没有,这里引用的pinia数据持久化插件是pinia-plugin-persistedstate
。
安装:pinia-plugin-persistedstate
pnpm : pnpm i pinia-plugin-persistedstate
npm : npm i pinia-plugin-persistedstate
yarn : yarn add pinia-plugin-persistedstate
配置插件pinia-plugin-persistedstate
在src目录下新增store文件夹,新增index.js
// pinia数据持久化存储
import { createPinia } from "pinia"
import { createPersistedState } from 'pinia-plugin-persistedstate'
const store = createPinia()
store.use(createPersistedState({
serializer: { // 指定参数序列化器
serialize: JSON.stringify,
deserialize: JSON.parse,
}
}))
export default store
在src新增store文件夹下,新增main.ts
import { defineStore } from "pinia"
export const useMainStore = defineStore({
id: 'main',
state: () => ({
name: 'hello pinia'
}),
// persist: true, 开启数据持久化,所有store数据将被持久化到指定仓库
persist: { // 自定义数据持久化方式
// key: 'store-key', 指定key进行存储,此时非key的值不会持久化,刷新就会丢失
storage: window.localStorage, // 指定换成地址
// paths: ['nested.data'],// 指定需要持久化的state的路径名称
beforeRestore: context => {
console.log('Before' + context)
},
afterRestore: context => {
console.log('After'+ context)
},
},
getters: {
getName: (state) => {
return state.name
}
},
actions: {
SET_NAME (param:string) {
this.name = param
}
}
})
再将store下的index.ts引入到入口文件main.ts文件里
import { createApp } from 'vue'
import App from './App.vue'
// 注入路由
import router from './router'
// 注入store
import store from './store'
import 'element-plus/dist/index.css'
// 导入element-plus消息api
import { ElMessage } from "element-plus"
import { currentTiemStr } from "@/script/utils"
const app = createApp(App)
app.use(router)
app.use(store)
app.mount('#app')
// 全局常用api配置
// 消息提示APi
app.config.globalProperties.$ElMessage = ElMessage
// 获取时间戳方法
app.config.globalProperties.$CurrentTiemStr = currentTiemStr
使用方法还是同上述1是一样的,当变化name值后,你刷新数据,会发现便后的name值一直都是最新值,至此,pinia使用插件pinia-plugin-persistedstate
pina使用举例
定义 stores/index.js
// stores/index.js
// pinia数据持久化存储
import { createPinia } from "pinia"
import { createPersistedState } from 'pinia-plugin-persistedstate'
const store = createPinia()
store.use(createPersistedState({
serializer: { // 指定参数序列化器
serialize: JSON.stringify,
deserialize: JSON.parse,
}
}))
export default store
定义 stores/user.js
// stores/user.js
import { defineStore } from 'pinia'
export const useUserInfoStore = defineStore('userInfo', {
state: () => {
return {
username: 'Eduardo',
}
},
// persist: true, 开启数据持久化,所有store数据将被持久化到指定仓库
persist: { // 自定义数据持久化方式
// key: 'store-key', 指定key进行存储,此时非key的值不会持久化,刷新就会丢失
storage: window.localStorage, // 指定换成地址
// paths: ['nested.data'],// 指定需要持久化的state的路径名称
beforeRestore: context => {
console.log('Before' + context)
},
afterRestore: context => {
console.log('After' + context)
},
},
getters: {},
actions: {
logout() {
this.$patch({
name: '',
isAdmin: false,
})
// we could do other stuff like redirecting the user
},
/**
* Attempt to login a user
*/
addRegister(data) {
const userData = data
this.username = userData.username
},
},
})
组件内使用
引用 state值
// 引用
import { useUserInfoStore } from '@/stores/userInfo.js'
// 获取值
const getUserInfo = useUserInfoStore()
// 直接使用
const userInfo = reactive(getUserInfo);
修改 state值
// 引用 userInfo.js
import { useUserInfoStore } from '@/stores/userInfo.js'
const userInfo = useUserInfoStore()
const submitForm = async () => {
getApi({
path: 'users/register',
method: 'post',
params: {
'username': ruleForm.username,
'pwd': ruleForm.pass
}
}).then(res => {
console.log('res')
if (res.data.code == 200) {
const userData = res.data.data
// 添加数据
userInfo.addRegister(userData)
// 页面跳转
router.push('Admin')
} else {
ElMessage({
message: 'res.data.msg',
grouping: true,
type: 'error',
})
}
})
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。