关于前端:vue中如何优雅高效的使用pinia

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',
            })
        }
    })
}

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理