Pinia 学习

  • Pinia 最初是在 2019 年 11 月左右重新设计使用Composition API。从那时起,最初的原则仍然相同,但 Pinia 对 Vue 2 和 Vue 3 都有效,并且不需要您使用组合 API。

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

一、介绍

1.Pinia与Vuex的比较

  • Pinia 最初是为了探索 Vuex 的下一次迭代会是什么样子,结合了 Vuex 5 核心团队讨论中的许多想法。最终,我们意识到 Pinia 已经实现了我们在 Vuex 5 中想要的大部分内容;
  • Vuex 相比,Pinia 提供了一个更简单的 API,具有更少的规范,提供了 Composition-API 风格的 API;
  • 最重要的是,在与 TypeScript 一起使用时具有可靠的类型推断支持

1.1 不同点:

  1. mutations 不再存在。他们经常被认为是 非常冗长。他们最初带来了 devtools 集成,但这不再是问题;
  2. 更友好的 TypeScripts 支持,Vuex之前使用TS是很麻烦的;
  3. 不再有 modules 的嵌套结构。您仍然可以通过在另一个 Store 中导入和 使用 来隐式嵌套 Store,但 Pinia 通过设计提供平面结构,同时仍然支持 Store 之间的交叉组合方式。 您甚至可以拥有 Store 的循环依赖关系
  4. 没有命名空间模块。鉴于 Store 的扁平架构,“命名空间” Store 是其定义方式所固有的,您可以说所有 Store 都是命名空间的。

2. 简单使用 Pinia

2.1 安装

yarn add pinia
# 或者使用 npm
npm install pinia

2.2 文件内容

2.2.1 新建仓库文件夹

src/store/index.js:

import { createPinia } from 'pinia';

const pinia = createPinia();

export default pinia;

2.2.2 Vue使用pinia

src/main.js入口文件:

import { createApp } from 'vue';
import App from './App.vue';
// 引入pinia仓库
import pinia from './store';

// 使用pinia
createApp(App).use(pinia).mount('#app');

2.2.3 定义某个具体模块仓库

在此举例,定义用户仓库:在src/store/useUserStore.js

import { defineStore } from 'pinia';

// 定义用户仓库, defineStore()接收的第一个参数为该仓库唯一标识ID
// 第二个参数为定义的仓库内容
const useUserStore = defineStore('user', {
  state() {
    return {
      username: 'fct',
      age: 22
    };
  },
  actions: {
    ageIncrement() {
      this.age++;
    }
  }
});
export default useUserStore;

2.2.4 在组件中使用

<template>
  <h2>学习Pinia</h2>
  <div class="block-area">
    <h3>1.简单使用</h3>
    <h3>用户名:{{ userStore.username }}</h3>
    <h3>年龄:{{ userStore.age }}</h3>
    <button @click="ageIncrement">年龄增加</button>
  </div>
</template>

<script setup>
import useUserStore from './store/useUserStore';

const userStore = useUserStore();

function ageIncrement() {
  // 三种方法均可更新age
  // userStore.age++;
  // userStore.$patch({ age: userStore.age + 1 });
  userStore.ageIncrement();
}
</script>

二、核心概念

1. 定义Store仓库

Store 是使用 defineStore() 定义的,并且它需要一个唯一名称,作为第一个参数传递,也称为 id,是必要的,Pinia 使用它来将 store 连接到 devtools,将返回的函数可命名为 useXXX

import { defineStore } from 'pinia'

// useXXXStore 可以是 useUserStore、useCartStore 之类的任何东西
// 第一个参数是应用程序中 store 的唯一 id
export const useXXXXStore = defineStore('main', {
  // other options...
})

1.1 使用

使用Store仓库,就是在组件内执行defineStore()返回的函数。

一旦 store 被实例化,你就可以直接在 store 上访问 stategettersactions 中定义的任何属性。

store 是一个用reactive 包裹的对象,这意味着不需要在 getter 之后写.value,但是,就像setup 中的props 一样,我们不能对其进行解构,否则将失去响应式。

<script setup>
import useUserStore from './store/useUserStore';
import { storeToRefs } from 'pinia';
import { computed } from 'vue';

// 实例化 store
const userStore = useUserStore();
// 解构后的值,将不是响应式的
const { username, age } = userStore;

// 使用计算属性将是响应式的
const usernameComputed = computed(() => userStore.username);
const ageComputed = computed(() => userStore.age);

// 使用 storeToRefs, 是响应式引用
const { username: usernameRef, age: ageRef } = storeToRefs(userStore);
</script>

2. State

默认情况下,您可以通过 store 实例访问状态来直接读取和写入状态。

import useUserStore from './store/useUserStore';

const userStore = useUserStore();
// 读取
console.log(userStore.username, userStore.age);
// 写入
userStore.age++;

2.1 重置state

import useUserStore from './store/useUserStore';

const userStore = useUserStore();
// 通过调用 store 上的 $reset() 方法将状态 重置 到其初始值:
userStore.$reset();

2.2 同时修改多个state

import useUserStore from './store/useUserStore';

const userStore = useUserStore();
// 除了直接用 userStore.age++ 修改 store,你还可以调用 $patch 方法。 
// 它允许您使用部分“state”对象同时应用多个更改:
userStore.$patch({
    age: userStore.age + 1,
    username: '常青'
});

注意:

使用这种语法应用某些突变非常困难或代价高昂:任何集合修改(例如,从数组中推送、删除、拼接元素)都需要您创建一个新集合。 正因为如此,$patch 方法也接受一个函数来批量修改集合内部分对象的情况:

userStore.$patch((state) => {
  state.friends.push({ name: 'fct', age: 23 });
  state.hasChanged = true;
})

这里没有直接赋予friends一个新的数组,而只是在原来的数组中push(推送)了个新对象。

2.3 替换state

import useUserStore from './store/useUserStore';

const userStore = useUserStore();
// 可以通过将其 $state 属性设置为新对象来替换 Store 的整个状态:
userStore.$state = { age: 666, username: 'Paimon' };

📌注意:

使用$state,不是直接完全替换state,即不是采用为state直接赋我们给予的对象:

// ❌ 不是直接替换赋值
userStore.state = { age: 666, username: 'Paimon' };

而是采取合并策略,当我们设置的新对象里与原来state的值有重名情况,则覆盖(替换)掉原state中的数据,其他未重名数据则直接加入原state对象。

// 类似这样
Object.assign(userStore.state, { age: 666, username: 'Paimon' })

2.4 订阅状态的改变

可以通过 store 的 $subscribe() 方法查看状态及其变化,类似于 Vuex 的 subscribe 方法。 与常规的 watch() 相比,使用 $subscribe() 的优点是 subscriptions 只会在 patches 之后触发一次(例如,当使用上面的函数版本时)。

import useUserStore from './store/useUserStore';

const userStore = useUserStore();
userStore.$subscribe((mutation, state) => {
  // import { MutationType } from 'pinia'
  mutation.type // 'direct' | 'patch object' | 'patch function'
  // 与 userStore.$id 相同
  mutation.storeId // 'user'
  // 仅适用于 mutation.type === 'patch object'
  mutation.payload
  // state 即为状态
})

默认情况下,state subscriptions 绑定到添加它们的组件,当组件被卸载时,它们将被自动删除。 如果要在卸载组件后保留它们,请将 { detached: true } 作为第二个参数传递给 detach 当前组件的 state subscription

// 此订阅将在组件卸载后保留
userStore.$subscribe((mutation, state) => {
    
}, { detached: true })

3. Getters

Getters类似于在组件中的 computed,getters依赖Store中state。

3.1 简单使用/访问其他getter

Getters接收“state状态”作为第一个参数, 大多数时候,getter 只会依赖state状态,但是,他们可能需要使用其他 getter。我们可以使用this来访问整个 store 的实例,从而访问其他getter。

export const useStore = defineStore('main', {
  state: () => ({
    counter: 0,
  }),
  getters: {
    // 自动将返回类型推断为数字
    doubleCount(state) {
      return state.counter * 2
    },
    // 在TypeScript中定义返回类型
    // 使用this,返回类型必须明确设置
    // doublePlusOne(): number {
    doublePlusOne() {
      return this.counter * 2 + 1
    },
    // 使用其他getter
    doubleCountPlusOne() {
      return this.doubleCount + 1
    },
  },
})

在组件中使用:

<template>
  <p>Double count is {{ store.doubleCount }}</p>
  <p>doublePlusOne is {{ store.doublePlusOne }}</p>
  <p>doubleCountPlusOne is {{ store.doubleCountPlusOne }}</p>
</template>

<script setup>
import { useStore } from './store/useStore';
const store = useStore();
store.count = 3;
</script>

3.2 getter接收参数

一般使用getter都是直接返回具体的值,而当我们返回一个函数时可以接收任何参数:

import { defineStore } from 'pinia';

// 定义用户仓库
const useUserStore = defineStore('user', {
  state() {
    return {
      username: 'fct',
      age: 22,
      friends: [
        { id: 111, name: 'lihua' },
        { id: 112, name: 'liulei' },
        { id: 113, name: 'wikiy' }
      ]
    };
  },
  getters: {
    getFriendById: (state) => {
      return (id) => state.friends.find((friend) => friend.id === id);
    }
  }
});
export default useUserStore;

在组件中使用:

<template>
  <div>
    <h3>getter的使用</h3>
    <p>朋友111: {{ userStore.getFriendById(111) }}</p>
    <p>朋友113: {{ userStore.getFriendById(113) }}</p>
  </div>
</template>

<script setup>
import useUserStore from './store/useUserStore';
const userStore = useUserStore();
</script>

3.3 访问其他 Store 的getter

使用其他store中的内容,可以直接引入store,然后实例化,就可以取出想要的数据。

import { useOtherStore } from './other-store'

export const useStore = defineStore('main', {
  state: () => ({
    // ...
  }),
  getters: {
    otherGetter(state) {
      const otherStore = useOtherStore()
      return state.localData + otherStore.data
    },
  },
})

4. Actions

Actions 相当于组件中的methods, 非常适合定义业务逻辑

4.1 简单使用

getters 一样,操作可以通过 this 访问 whole store instanceactions 可以是异步的,您可以在其中await 任何 API 调用甚至其他操作。

import { defineStore } from 'pinia';

// 定义用户仓库
const useListStore = defineStore('list', {
  state() {
    return {
      list: []
    };
  },
  },
  actions: {
    async getAsyncData() {
      const data = await fetch('url路径');
      const res = await data.json()
      this.list = res;
    }
  }
});
export default useListStore;

组件中使用:

<script setup>
import useListStore from './store/useListStore';
const listStore = useListStore();
// Actions 像 methods 一样被调用:
listStore.getAsyncData()
</script>

4.2 访问其他store

要使用另一个 store ,您可以直接在操作内部使用它,与getters使用相同。

import { useAuthStore } from './auth-store'

export const useSettingsStore = defineStore('settings', {
  state: () => ({
    preferences: {}
    // ...
  }),
  actions: {
    async fetchUserPreferences(preferences) {
      // 访问其他store
      const auth = useAuthStore()
      // 使用 state
      if (auth.isAuthenticated) {
        this.preferences = await fetchPreferences()
      } else {
        throw new Error('User must be authenticated')
      }
    },
  },
})

4.3 订阅Actions

可以使用 store.$onAction() 订阅 action 及其结果。接收一个回调函数作为参数。

  • after 处理 Promise 并允许您在 action 完成后执行函数。
  • onError 允许您在处理中抛出错误。
const unsubscribe = someStore.$onAction(
  ({
    name, // action 的名字
    store, // store 实例
    args, // 调用这个 action 的参数
    after, // 在这个 action 执行完毕之后,执行这个函数
    onError, // 在这个 action 抛出异常的时候,执行这个函数
  }) => {
    // 记录开始的时间变量
    const startTime = Date.now()
    // 这将在 `store` 上的操作执行之前触发
    console.log(`Start "${name}" with params [${args.join(', ')}].`)

    // 如果 action 成功并且完全运行后,after 将触发。
    // 它将等待任何返回的 promise
    after((result) => {
      console.log(
        `Finished "${name}" after ${
          Date.now() - startTime
        }ms.\nResult: ${result}.`
      )
    })

    // 如果 action 抛出或返回 Promise.reject ,onError 将触发
    onError((error) => {
      console.warn(
        `Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
      )
    })
  }
)

// 手动移除订阅
unsubscribe()

4.3.1 手动保留订阅

默认情况下,action subscriptions 绑定到添加它们的组件,当组件被卸载时,它们将被自动删除。 如果要在卸载组件后保留它们,请将 true 作为第二个参数传递给当前组件的 detach action subscription

<script setup>
import useSomeStore from './store/useSomeStore';  

const someStore = useSomeStore()
// 此订阅将在组件卸载后保留
someStore.$onAction(callback, true)
</script>
posted @ 2022-11-01 20:40  青柠i  阅读(83)  评论(0编辑  收藏  举报