Pinia 起始于 2019 年 11 月左右的一次实验,其目的是设计一个拥有组合式 API 的 Vue 状态管理库。同时支持 Vue 2 和 Vue 3,并且不强制要求开发者使用组合式 API。

pinia官网链接地址。

Pinia 是 Vue 的专属状态管理库,它允许你跨组件或页面共享状态。对于单页应用来说,可以通过一行简单的 export const state = reactive({}) 来共享一个全局状态。如果应用在服务器端渲染,这可能会使你的应用暴露出一些安全漏洞。

如果使用 Pinia,即使在小型单页应用中,你也可以获得如下功能:

  • Devtools 支持

    • 追踪 actionsmutations 的时间线
    • 在组件中展示它们所用到的 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) 是一个保存状态业务逻辑的实体,它并不与你的组件树绑定。换句话说,它承载着全局状态。它有点像一个永远存在的组件,每个组件都可以读取和写入它。它有三个概念,stategetteraction,我们可以假设这些概念相当于组件中的 datacomputedmethods

应该在什么时候使用 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 类似,我们也可以传入一个带有 stateactionsgetters 属性的 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,但你可以使用 computedmethods,...,那你可以使用 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',
            })
        }
    })
}

前端老兵
15 声望1 粉丝

你们说这是哪里?