🍍大菠萝(Pinia)符合真香定律

5,262 阅读6分钟

最近在开发Vue3项目时,遇到了多层跨域数据传输的问题。起初,我用propsemit实现了爷孙组件之间的数据传递,效果不错。子组件通过defineEmit定义方法,父组件通过emit调用这些方法,而defineProps则负责接收参数,一切井然有序。

但问题出现在兄弟组件之间的数据传递上。兄弟组件的数据由父组件管理,两者不会直接修改数据,每次使用都从父组件独立获取属性或方法。然而,这些兄弟组件并非“堂兄弟”,而是“表兄弟”,数据传输变得复杂。

有人可能会想到用injectprovide实现跨层级传递,但随着项目规模扩大,数据来源不明确,维护起来会很麻烦。

这时,Vuex似乎是个解决方案,但今天的主角是更轻量级的Pinia,也就是“大菠萝”🍍

大菠萝Pinia是啥?

Pinia 是 Vue 的存储库,它允许您跨组件/页面共享状态。

这是Pinia官网对它的定义。Vuex用起来还是有点麻烦,React里的Context倒是还不错。 而Pinia不仅能在Vue3中使用,还兼容Vue2,简直是开发者的福音。 下面是 Pinia官网对自身的评价,看起来它对自己的定位非常清晰。

image.png

Pinia 常规操作

  • 安装

通过常用的包管理器进行安装:

yarn add pinia
// 或者
npm install pinia

安装完成后,我们需要将Pinia挂载到Vue应用中。也就是说,我们需要创建一个根存储并传递给应用。我们需要修改main.js,引入Pinia提供的createPinia方法:

import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'

const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.mount('#app')

创建Pinia实例,并在根组件中use一下,这样整个应用就可以使用Pinia来管理状态了。

Store

Store是Pinia的核心概念,它是一个中央数据仓库,专门用于状态管理。这里存放的数据是整个应用可以访问的全局数据,而组件内部的数据不应该放入Store。

使用Pinia提供的defineStore方法创建一个全局仓库。defineStore接收两个参数:

  • name:一个字符串,必传项,该Store的唯一ID。
  • options:一个对象,Store的配置项,比如配置Store内的数据、修改数据的方法等等。

defineStore配置store属性时有两种写法,一种是Option对象形式,一种是Setup函数形式。

对象形式(Option store)

Store有三个核心概念:stategettersactions,我们可以将它们类比为组件中的“数据”、“计算属性”和“方法”。(直观,推荐使用)

// 中央管理store 
import { defineStore } from 'pinia'
import { ref } from 'vue'

// 第一个参数:唯一ID;第二个:一个对象,其他配置项
export const useCounterStore = defineStore('counter', {
  // 推荐使用完整类型推断的箭头函数
  state: () => {
    return {
      count: ref(0),
      isLogin: ref(false),
    }
  },
  actions: {
    add() {
      this.count++
    }
  }
})

函数形式(setup store)

与 Vue 组合式 API 的 setup 函数 相似,我们可以传入一个函数,该函数定义了一些响应式属性和方法,并且返回一个带有我们想暴露出去的属性和方法的对象。

在 Setup Store 中:

  • ref() 就是 state 属性
  • computed() 就是 getters
  • function() 就是 actions

可见,函数式相比对象式更灵活,还可以创建监听器

import { defineStore } from 'pinia';
import { ref } from 'vue';

export const useCounterStore = defineStore('counter', () => {
  // 定义状态
  const count = ref(0);
  const isLogin = ref(false);

  // 定义 actions
  function add() {
    count.value++;
  }

  // 返回状态和 actions
  return {
    count,
    isLogin,
    add,
  };
});

使用Store

下面都说Option对象形式创建的store

可以在任意组件中引入定义的Store来进行使用。比如在App.js中:

<template>
  <div>
    <p>数量: {{ counterStore.count }}</p>
    <p>是否登入: {{ counterStore.isLogin }}</p>
    <button @click="counterStore.add">增加Add</button>
  </div>
</template>

<script setup>
import { useCounterStore } from './stores/counter';

const counterStore = useCounterStore();
</script>

实例化后,就可以使用Store里面的stategettersactions中定义的任何属性了。

可能有人会说每次都用实例counterStore.去点方法/属性,有点麻烦。这时候大菠萝pinia又给舔了一嘴——使用解构直接

解构与直接访问

一般来说,我们实例化Store后,直接进行读取和写入操作即可。

// CompA.js
<template>
  <div>
    {{ count }}
    <button @click="add">B点我{{ count }}</button>
    <br>
    <button @click="counterStore.add">A点我{{ counterStore.count }}</button>
  </div>
</template>

<script>
import { useCounterStore } from '../store/counter'
const counterStore = useCounterStore()
const { count, add } = useCounterStore();
</script>

这里有个问题,如果直接解构,会发现数据失去了响应式。

这时候,我们可以使用toRef一个一个解构,或者使用toRefs一次性解构所有属性或方法。Pinia还有一个自带的解构状态storeToRefs,但是不包括方法。如果你使用的是 Vue 3 的 <script setup> 语法,可以直接解构和使用 Pinia store

// 使用 toRefs 一次性解构所有属性
const { count, add } = toRefs(counterStore);


// 使用 toRef 逐个解构
const count = toRef(counterStore, 'count');
const add = toRef(counterStore, 'add');

// 使用 storeToRefs 解构状态
const { count } = storeToRefs(counterStore);

// setup语法糖下,直接解构方法
const { add } = counterStore;

State

其他常用的操作还有:$reset$patch$state$subscribe等,这些方法可以帮助我们更好地管理状态。

  • $reset重置为初始值
import { useCounterStore } from './store/counter';
const counterStore = useCounterStore();

counterStore.count = 10;
// reset重置状态为初始值
counterStore.$reset(); // count 恢复为初始值 0
  • 2.  $patch:批量更新状态

$patch 可以一次性更新多个状态,支持对象或函数形式。

// 对象形式
counterStore.$patch({
  count: 5,
  isLogin: true,
});

// 函数形式
counterStore.$patch((state) => {
  state.count += 1;
  state.isLogin = false;
});
  • 3.  $state:替换整个状态

$state 用于替换整个 state 对象。

counterStore.$state = {
  count: 100,
  isLogin: true,
};
  • 4.  $subscribe:监听状态变化

$subscribe 可以监听 state 的变化,适合做一些副作用操作。

counterStore.$subscribe((mutation, state) => {
  console.log('状态变化了:', state.count);
});

// 修改状态
counterStore.count = 20; // 控制台输出: "状态变化了: 20"

Actions

直接通过 this 访问 stategetters 和其他 action,不需要像vuex每次都去使用 context。同时可以直接修改状态,不需要像vuex通过 mutation

Pinia 的 action 天然支持异步操作,比如 async/await 或 Promise,无需额外配置。

actions: {
  async fetchData() {
    const data = await api.getData();
    this.data = data; // 直接修改状态
  }
}

除了异步,直接修改状态,调用其他action和访问getters这4项操作外,Pinia 的 action 还有以下能力:

  • 组合操作:可以在一个 action 中执行多个操作,比如先调用 API,再修改状态,最后调用其他 action
actions: {
  async login(credentials) {
    const user = await api.login(credentials); // 异步操作
    this.user = user; // 直接修改状态
    this.setLoginStatus(true); // 调用其他 action
  },
  setLoginStatus(status) {
    this.isLoggedIn = status; // 直接修改状态
  }
}

数据持久化

store中的数据,其实是在内存中的,刷新页面后就丢失了,如果想保留这些数据,就要用到数据持久化了。

推荐使用 pinia-plugin-persistedstate

  • 安装插件
yarn add pinia-plugin-persistedstate
  • 将插件添加到pinia
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
  • 使用持久化功能

在定义 Store 时,通过 persist 选项启用持久化:

import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'John',
    age: 30
  }),
  actions: {
    increment() {
      this.count++;
    }
  },
  persist: true // 启用持久化
});
  • 高级配置

你还可以自定义持久化的配置,例如指定存储的 key、存储方式(localStoragesessionStorage)以及需要持久化的路径:

export const useUserStore = defineStore('user', {
  state: () => ({
    name: '',
    age: 0
  }),
  persist: {
    key: 'user-store', // 存储的 key
    storage: localStorage, // 使用 localStorage
    paths: ['name'] // 只持久化 `name` 属性
  }
});
方法二:手动实现数据持久化

如果不使用插件,也可以通过手动监听状态变化并存储到 localStoragesessionStorage 中。

import { defineStore } from 'pinia';

export const useStore = defineStore('store', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment() {
      this.count++;
      this.saveToStorage(); // 每次修改后保存到存储
    }
  },
  saveToStorage() {
    localStorage.setItem('store', JSON.stringify(this.$state));
  }
});

// 初始化时从存储加载数据
const store = useStore();
const storedState = localStorage.getItem('store');
if (storedState) {
  store.$patch(JSON.parse(storedState));
}

小结

Pinia 的设计让状态管理更加直观和灵活,特别适合现代 Vue 开发!

还是不得不说:Vuex太重,Pinia(大菠萝)轻巧好用,真香!

参考内容:

pinia中文文档

🍍(Pinia)不酸,保甜