Vue3:使用 Pinia 集中式状态(数据)管理

有时,我们希望几个组件共享某一个数据,如果通过 Vue3 的参数传递将会很混乱,集中式状态(数据)管理插件就是用于解决这一问题的。在 Vue2 中,推荐使用的是 vuex;而在 Vue3,官方推荐使用 Pinia 用作集中式状态管理插件。

环境准备

npm install pinia

安装 Pinia

只需简单三步,即可在您的项目中使用 Pinia.

main.ts:

import {createApp} from "vue";
import App from "@/App.vue";
// 1. 引入 pinia
import {createPinia} from "pinia";

const app = createApp(App);

// 2. 创建一个 Pinia 实例
const pinia = createPinia();

// 3. 安装 pinia
app.use(pinia);

app.mount("#app")

使用 Pinia

store 是一个保存状态业务逻辑的实体,每个组件都可以读取写入它。其包含三个概念:stategetteraction,分别对应组件中的 datacomputedmethods

编码规范要求我们,在存储数据时,将脚本文件放在 src/store/ 下,并使用 useXxxStore 命名暴露的 store

存储数据(选项式)

src/store/Counter.ts

import {defineStore} from "pinia";

export const useCounterStore = defineStore('counter', {
    // 数据
    state() {
        return {
            sum: 6
        }
    },
    // 操作
    actions: {
        add(val: number) {
            // this 是当前 store 实例
            this.sum += val;
        },
        sub(val: number) {
            this.sum -= val;
        }
    },
    // 计算属性
    getters: {
        doubleSum(): number {
            return this.sum * 2
        },
        /*使用箭头函数*/
        halfSum: state => state.sum / 2
    }
});

存储数据(组合式)

src/store/Counter.ts

import {defineStore} from "pinia";
import {computed, ref} from "vue";

export const useCounterStore = defineStore('counter', () => {
    // 数据
    const sum = ref(6);

    // 操作
    function add(val: number) {
        sum.value += val;
    }
    function sub(val: number) {
        sum.value -= val;
    }

    // 计算方法
    const doubleSum = computed(() => sum.value * 2);
    const halfSum = computed(() => sum.value / 2);

    // 最终需要暴露出去
    return {
        sum,
        add,
        sub,
        doubleSum,
        halfSum
    }
});

读取数据

src/components/Counter.vue(或任何其他组件):

<script setup lang="ts">
import {ref} from "vue";

// 引入定义的store
import {useCounterStore} from "@/store/Conuter";
const counterStore = useCounterStore();

let n = ref(1);
</script>

<template>
  <div class="counter">
    <!--  counterStore.sum 本身是响应式的,可以读写  -->
    <h2>当前求和为:{{ counterStore.sum }}</h2>
    <!--  v-model 的作用是,当select的值发生变化时,会自动更新n的值,v-model.number 是把n转成number类型  -->
    <select v-model.number="n">
      <option value="1">1</option>
      <option value="2">2</option>
      <option value="3">3</option>
    </select>
    <!--  可以直接使用counterStore的add和sub方法  -->
    <button @click="counterStore.add(n)">加</button>
    <button @click="counterStore.sub(n)">减</button>
  </div>
</template>

<style scoped>
.counter {
  margin: 20px;
  background-color: skyblue;
  padding: 10px;
  border-radius: 10px;
  box-shadow: 0 0 10px;
}

select, button {
  margin: 0 5px;
  height: 25px;
}
</style>

但在使用数据时,总是使用 counterStore. 前缀太冗长,是否可以将数据本身直接解构出来?

当然可以!但是,使用 toRefs 保留解构数据的响应式是一种不被推荐的做法!因为 toRefs 会将所有属性都添加响应式,这会影响性能,好在 Pinia 提供了更快的解决方案:使用 storeToRefs(obj)

// 只会给store中的数据进行响应式代理
let {sum} = storeToRefs(counterStore);

storeToRefs 只会给存储的数据添加响应式。

订阅数据的变化

使用 $subscribe(callback) 可以实现监听 store 里面数据的变化,并执行响应的回调函数。

例如可以根据这个函数结合 localStorage 实现 sum 的存储:

// 监听数据变化,并保存到localStorage中
counterStore.$subscribe((mutation, state) => {
  console.log('数据更新了');
  localStorage.setItem('sum', state.sum.toString());
})

其中,mutation 记录了变化的相关信息,state 就是监听的 store 的数据。

src/storage/Counter.ts 中:

export const useCounterStore = defineStore('counter', {
    // 数据
    state() {
        return {
            // 从本地缓存中初始化
            sum: parseInt(localStorage.getItem('sum') || '0')
        }
    },

或者组合式

// 从本地缓存中初始化
const sum = ref(parseInt(localStorage.getItem('sum') || '0'));