本文为稀土技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

前言

Pinia ,发音为/pinj/,来源于西班牙语pia 。意思为菠萝,表明与菠萝相同,由很多小块组成。在 Pinia 中,每个 Store 都是独自存在,一同进行状况办理。

Pinia 是由 Vue.js 团队成员开发,开始是为了探索 Vuex 下一次迭代会是什么样子。过程中,Pinia 完成了 Vuex5 提案的大部分内容,所以就取而代之了。

与 Vuex 比较,Pinia 提供了更简略的 API,更少的规范,以及 Composition-API 风格的 API 。更重要的是,与 TypeScript 一同运用具有牢靠的类型揣度支撑。

Pinia 与 Vuex 3.x/4.x 的不同

  • mutations不复存在。只要 state 、getters 、actions。
  • actions 中支撑同步和异步办法修正 state 状况。
  • 与 TypeScript 一同运用具有牢靠的类型揣度支撑。
  • 不再有模块嵌套,只要 Store 的概念,Store 之间能够相互调用。
  • 支撑插件扩展,能够非常便利完成本地存储等功用。
  • 愈加轻量,紧缩后体积只要 2kb 左右。

既然 Pinia 这么香,那么还等什么,一同用起来吧!

根本用法

装置

npm install pinia

在 main.js 中 引入 Pinia

// src/main.js
import { createPinia } from 'pinia'
const pinia = createPinia()
app.use(pinia)

界说一个 Store

src/stores 目录下创立 counter.js 文件,运用 defineStore() 界说一个 Store 。defineStore() 第一个参数是 storeId ,第二个参数是一个选项目标:

// src/stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    doubleCount: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++
    }
  }
})

咱们也能够运用更高档的办法,第二个参数传入一个函数来界说 Store :

// src/stores/counter.js
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)
  function increment() {
    count.value++
  }
  return { count, doubleCount, increment }
})

在组件中运用

在组件中导入方才界说的函数,并执行一下这个函数,就能够获取到 store 了:

<script setup>
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
// 以下三种方式都会被 devtools 跟踪
counterStore.count++
counterStore.$patch({ count: counterStore.count + 1 })
counterStore.increment()
</script>
<template>
  <div>{{ counterStore.count }}</div>
  <div>{{ counterStore.doubleCount }}</div>
</template>

这便是根本用法,下面咱们来介绍一下每个选项的功用,及插件的运用办法。

State

解构 store

store是一个用 reactive包裹的目标,如果直接解构会失掉呼应性。咱们能够运用 storeToRefs() 对其进行解构:

<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
const { count, doubleCount } = storeToRefs(counterStore)
</script>
<template>
  <div>{{ count }}</div>
  <div>{{ doubleCount }}</div>
</template>

修正 store

除了能够直接用 store.count++ 来修正 store,咱们还能够调用 $patch 办法进行修正。$patch 性能更高,并且能够同时修正多个状况。

<script setup>
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
counterStore.$patch({
  count: counterStore.count + 1,
  name: 'Abalam',
})
</script>

可是,这种办法修正集合(比如从数组中增加、删去、插入元素)都需要创立一个新的集合,代价太高。因而,$patch 办法也承受一个函数来批量修正:

cartStore.$patch((state) => {
  state.items.push({ name: 'shoes', quantity: 1 })
  state.hasChanged = true
})

监听 store

咱们能够经过 $subscribe() 办法能够监听 store 状况的改变,类似于 Vuex 的 subscribe 办法。与 watch() 比较,运用 $subscribe() 的优点是,store 多个状况发生改变之后,回调函数只会执行一次。

<script setup>
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
counterStore.$subscribe((mutation, state) => {
  // 每逢状况发生改变时,将 state 持久化到本地存储
  localStorage.setItem('counter', JSON.stringify(state))
})
</script>

也能够监听 pinia 实例上一切 store 的改变

// src/main.js
import { watch } from 'vue'
import { createPinia } from 'pinia'
const pinia = createPinia()
watch(
  pinia.state,
  (state) => {
    // 每逢状况发生改变时,将一切 state 持久化到本地存储
    localStorage.setItem('piniaState', JSON.stringify(state))
  },
  { deep: true }
)

Getters

拜访 store 实例

大多数情况下,getter 只会依靠 state 状况。但有时分,它会运用到其他的 getter ,这时分咱们能够经过this拜访到当时 store 实例。

// src/stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    doubleCount(state) {
      return state.count * 2
    },
    doublePlusOne() {
      return this.doubleCount + 1
    }
  }
})

拜访其他 Store 的 getter

要运用其他 Store 的 getter,能够直接在getter内部运用:

// src/stores/counter.js
import { defineStore } from 'pinia'
import { useOtherStore } from './otherStore'
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 1
  }),
  getters: {
    composeGetter(state) {
      const otherStore = useOtherStore()
      return state.count + otherStore.count
    }
  }
})

将参数传递给 getter

getter本质上是一个computed ,无法向它传递任何参数。可是,咱们能够让它回来一个函数以承受参数:

// src/stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
  state: () => ({
    users: [{ id: 1, name: 'Tom'}, {id: 2, name: 'Jack'}]
  }),
  getters: {
    getUserById: (state) => {
      return (userId) => state.users.find((user) => user.id === userId)
    }
  }
})

在组件中运用:

<script setup>
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const { getUserById } = storeToRefs(userStore)
</script>
<template>
  <p>User: {{ getUserById(2) }}</p>
</template>

留意:如果这样运用,getter 不会缓存,它只会当作一个一般函数运用。一般不引荐这种用法,由于在组件中界说一个函数,能够完成同样的功用。

Actions

拜访 store 实例

与 getters 相同,actions 能够经过 this 拜访当 store 的实例。不同的是,actions 能够是异步的。

// src/stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
  state: () => ({ userData: null }),
  actions: {
    async registerUser(login, password) {
      try {
        this.userData = await api.post({ login, password })
      } catch (error) {
        return error
      }
    }
  }
})

拜访其他 Store 的 action

要运用其他 Store 的 action,也能够直接在action内部运用:

// src/stores/setting.js
import { defineStore } from 'pinia'
import { useAuthStore } from './authStore'
export const useSettingStore = defineStore('setting', {
  state: () => ({ preferences: null }),
  actions: {
    async fetchUserPreferences(preferences) {
      const authStore = useAuthStore()
      if (authStore.isAuthenticated()) {
        this.preferences = await fetchPreferences()
      } else {
        throw new Error('User must be authenticated!')
      }
    }
  }
})

以上便是 Pinia 的具体用法,是不是比 Vuex 简略多了。除此之外,插件也是 Pinia 的一个亮点,个人觉得非常有用,下面咱们就来重点介绍一下。

Plugins

由所以底层 API,Pania Store 彻底支撑扩展。以下是能够扩展的功用列表:

  • 向 Store 增加新状况
  • 界说 Store 时增加新选项
  • 为 Store 增加新办法
  • 包装现有办法
  • 更改乃至撤销操作
  • 完成本地存储等副作用
  • 仅适用于特定 Store

运用办法

Pinia 插件是一个函数,承受一个可选参数 contextcontext 包括四个特点:app 实例、pinia 实例、当时 store 和选项目标。函数也能够回来一个目标,目标的特点和办法会别离增加到 state 和 actions 中。

export function myPiniaPlugin(context) {
  context.app // 运用 createApp() 创立的 app 实例(仅限 Vue 3)
  context.pinia // 运用 createPinia() 创立的 pinia
  context.store // 插件正在扩展的 store
  context.options // 传入 defineStore() 的选项目标(第二个参数)
  // ...
  return {
    hello: 'world', // 为 state 增加一个 hello 状况
    changeHello() { // 为 actions 增加一个 changeHello 办法
      this.hello = 'pinia'
    }
  }
}

然后运用pinia.use()将此函数传递给pinia 就能够了:

// src/main.js
import { createPinia } from 'pinia'
const pinia = createPinia()
pinia.use(myPiniaPlugin)

向 Store 增加新状况

能够简略地经过回来一个目标来为每个 store 增加状况:

pinia.use(() => ({ hello: 'world' }))

也能够直接在store上设置特点来增加状况,为了使它能够在 devtools 中运用,还需要对 store.$state 进行设置:

import { ref, toRef } from 'vue'
pinia.use(({ store }) => {
  const hello = ref('word')
  store.$state.hello = hello
  store.hello = toRef(store.$state, 'hello')
})

也能够在 use 办法外面界说一个状况,共享全局的refcomputed

import { ref } from 'vue'
const globalSecret = ref('secret')
pinia.use(({ store }) => {
  // `secret` 在一切 store 之间共享
  store.$state.secret = globalSecret
  store.secret = globalSecret
})

界说 Store 时增加新选项

能够在界说 store 时增加新的选项,以便在插件中运用它们。例如,能够增加一个debounce选项,答应对一切操作进行去颤动:

// src/stores/search.js
import { defineStore } from 'pinia'
export const useSearchStore = defineStore('search', {
  actions: {
    searchContacts() {
      // ...
    },
    searchContent() {
      // ...
    }
  },
  debounce: {
    // 操作 searchContacts 防抖 300ms
    searchContacts: 300,
    // 操作 searchContent 防抖 500ms
    searchContent: 500
  }
})

然后运用插件读取该选项,包装并替换原始操作:

// src/main.js
import { createPinia } from 'pinia'
import { debounce } from 'lodash'
const pinia = createPinia()
pinia.use(({ options, store }) => {
  if (options.debounce) {
    // 咱们正在用新的 action 覆盖原有的 action
    return Object.keys(options.debounce).reduce((debouncedActions, action) => {
      debouncedActions[action] = debounce(
        store[action],
        options.debounce[action]
      )
      return debouncedActions
    }, {})
  }
})

这样在组件中运用 actions 的办法就能够去颤动了,是不是很便利!

完成本地存储

信任大家运用 Vuex 都有这样的困惑,F5 改写一下数据全没了。在咱们眼里这很正常,但在测试同学眼里这便是一个 bug 。Vuex 中完成本地存储比较费事,需要把状况一个一个存储到本地,取数据时也要进行处理。而运用 Pinia ,一个插件就能够搞定。

这次咱们就不自己写了,直接装置开源插件。

npm i pinia-plugin-persist

然后引入插件,并将此插件传递给pinia

// src/main.js
import { createPinia } from 'pinia'
import piniaPluginPersist from 'pinia-plugin-persist'
const pinia = createPinia()
pinia.use(piniaPluginPersist)

接着在界说 store 时敞开 persist 即可:

// src/stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 1 }),
  // 敞开数据缓存
  persist: {
    enabled: true
  }
})

这样,无论你用什么姿势改写,数据都不会丢失啦!

默许情况下,会以 storeId 作为 key 值,把 state 中的一切状况存储在 sessionStorage 中。咱们也能够经过 strategies 进行修正:

// 敞开数据缓存
persist: {
  enabled: truestrategies: [
    {
      key: 'myCounter', // 存储的 key 值,默以为 storeId
      storage: localStorage, // 存储的方位,默以为 sessionStorage
      paths: ['name', 'age'], // 需要存储的 state 状况,默许存储一切的状况
    }
  ]
}

ok,今日的共享便是这些。不知道你对 Pinia 是不是有了更进一步的了解,欢迎评论区留言评论。

小结

Pinia 整体来说比 Vuex 愈加简略、轻量,但功用却愈加强壮,也许这便是它取代 Vuex 的原因吧。此外,Pinia 还能够在 Vue2 中结合 map 函数运用,有爱好的同学能够研究一下。

欢迎重视专栏 Vue3 特训营 ,后边我会持续共享更多 Vue3 相关的内容。如果对你有所协助,记住点个赞呦!

参阅文档:Pinia 中文文档