Pinia 使用指南

358 阅读6分钟

一、介绍

Pinia 是一个用于 Vue 的状态管理库,类似 Vuex, 是 Vue 的另一种状态管理方案

为什么使用Pinia

  • PiniaAPI 设计非常接近 Vuex5 的提案。(作者是 Vue 核心团队成员)
  • 无需像 Vuex 自定义复杂的类型来支持 TypeScript,天生具备完美的类型推断。所有东西都是类型化的,API的设计方式是尽可能地利用TS类型推理。
    • Vuex 中,TypeScript 的类型提示做得不是很好,在进行类型推导时,只能找到它的 state。特别是写代码的过程中,代码提示就很不智能。
    • Pinia,就能推导出定义的所有 stategetteraction,这样在写代码的时候,就会方便很多。

  • 极其的轻量化Pinia 大小大约为1k

  • 没有mutations

  • 无需动态添加 stores,默认都是动态的

  • 不再有模块的嵌套结构Pinia 在设计提供了一个扁平的结构,仍然能够在存储空间之间进行交叉组合

  • 模块化设计,你引入的每一个 store 在打包时都可以自动拆分他们。

  • 支持服务端渲染

  • 热更新

    • 不必重载页面即可修改 Store
    • 开发时可保持当前的 State

二、安装Pinia

# 使用 yarn
yarn add pinia

# 使用 npm 
npm install pinia

⚠️ 注意: 如果你的应用使用的 Vue 版本低于 2.7,你还需要安装组合式 API 包:@vue/composition-api

三、挂载Pinia

创建一个 Pinia 实例 (根 Store) 并将其传递给应用:

// Vue2 的main.js
// Vue2,还需要安装一个插件并pinia在应用程序的根目录插入创建的插件
import { createPinia, PiniaPlugin } from 'pinia'

Vue.use(PiniaPlugin)
const pinia = createPinia()

new Vue({
  el: '#app',
  // 其他配置...
  pinia,
})

// 在 Vue 2 中,Pinia 使用的是 Vuex 的现有接口 (因此不能与 Vuex 一起使用) 
// Vue3 的mian.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const pinia = createPinia()
const app = createApp(App)

app.use(pinia)
app.mount('#app')

四、基本使用

1. 目录结构

Pinia 的目录被称为stores而不是store,这是为了强调 Pinia 支持多个store。

# Vuex example (assuming namespaced modules)
src
└── store
    ├── index.js           # Initializes Vuex, imports modules
    └── modules
        ├── module1.js     # 'module1' namespace
        └── module2.js		 # 'module2' namespace
        └── module3.js     # 'module3' namespace
# Pinia equivalent, note ids match previous namespaces
src
└── stores
    ├── module1.js        # 'module1' id
    ├── module2.js        # 'module2' id
    └── module3.js        # 'module3' id

2. 定义 Store

Store 是用 defineStore 定义的:

  • 第一个参数:要求是一个独一无二的名字
  • 第二个参数 可接受两类值:Setup 函数或 Option 对象。
  • 你可以任意命名 defineStore的返回值,但最好使用 store 的名字,同时以 use 开头且以 Store结尾。(比如 useUserStoreuseCounterStoreuseProductStore)

2.1. Option Store

// src/stores/counter.js
import { defineStore } from "pinia"

// 对外部暴露一个 use 方法,该方法会导出我们定义的 state
export const useCounterStore = defineStore('counter', {
  // state 类似于 data, 表示数据源
  // 为了完整类型推理,推荐使用箭头函数
  state: () => ({
    // 所有这些属性都将自动推断出它们的类型
    count: 0,
    name: 'Amelia',
  }),
  // getters 类似于 computed,可对 state 的值进行二次计算
  getters: {
    // 可以在函数的第一个参数中拿到 state
    double: (state) => state.count * 2,
    doubleCountPlusOne() {
      // 通过 this,你可以访问到其他任何 getter
      return this.doubleCount + 1
    },
  },
  // actions 类似于 methods, 用来修改 state
  actions: {
    increment() {
      // action 中的 this 指向👉 state
      this.count++
    },
  }
})

2.2. Setup Store

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

// src/stores/counter.js
import { defineStore } from "pinia"

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)  // ref() 就是 state 属性
  const name = ref('Amelia')
  const double = computed(() => count.value * 2)  // computed() 就是 getters
  const increment = () => {  // function() 就是 actions
    count.value++
  }

  return { count, double, increment }
})

⚠️ 注意:要让 pinia 正确识别 state,你必须setup store返回 state所有属性。这意味着,你不能在 store 中使用私有属性。不完整返回会影响 SSR ,开发工具和其他插件的正常运行。

3. 组件中使用 Store

Pinia 提供了两种方式来使用 storeOptions ApiComposition Api 中都完美支持。

3.1. Options Api

在 Options Api 中,可直接使用官方提供的 mapActionsmapState 方法,导出 store 中的 stategetteraction,其用法与 Vuex 基本一致,很容易上手。

// src/components/HelloWorld.vue
import { mapActions, mapState } from 'pinia'
import { useCounterStore } from '@/stores/counter'
 
export default {
  name: 'HelloWorld',
  computed: {
    ...mapState(useCounterStore, ['count', 'double'])
  },
  methods: {
    ...mapActions(useCounterStore, ['increment'])
  }
}

如果需要访问 store 里的大部分内容,映射 store 的每一个属性太麻烦,可以用 mapStores()来访问整个 store:

// src/components/HelloWorld.vue
import { mapStores } from 'pinia'
import { useCounterStore } from '@/stores/counter'
import { useUserStore } from '@/stores/user'
// const useUserStore = defineStore('user', {
//   // ...
// })
export default {
  computed: {
    // 注意,我们不是在传递一个数组,而是一个接一个的 store。
    ...mapStores(useCounterStore, useUserStore),
  },

  methods: {
    async buyStuff() {
      // 可以 id+'Store' 的形式访问每个 store 。可以在任何地方使用他们!
      if (this.userStore.isAuthenticated()) {
        await this.counterStore.increment()
        this.$router.push('/purchased')
      }
    },
  },
}

3.2. Composition Api

// src/components/HelloWorld.vue
<script setup>
import { useCounterStore } from '@/stores/counter'
  
// 可以在组件中的任意位置访问 `counterStore` 变量 ✨
const counterStore = useCounterStore()
const count = counterStore.count
  
// ✅ 这样使用computed 写是响应式的
const double = computed(() => counterStore.double)
// 💡 当然你也可以直接使用 `counterStore.double`
const doubleConut = counterStore.double
  
const increment = counterStore.increment
</script>

<template>
  <p>Double count is {{ counterStore.double }}</p>
</template>

4. 解构store

⚠️ 注意:store 是一个用 reactive 包装的对象,这意味着不需要在 getters 后面写 .value。就像 setup 中的 props 一样,我们不能对它进行解构。

<script setup>
  import { useCounterStore } from '@/stores/counter'
  import { computed } from 'vue'

  const counterStore = useCounterStore()
  // ❌ 这将不起作用,因为它破坏了响应性
  // 这就和直接解构 `props` 一样
  const { name, double } = counterStore
  name // 将始终是 "Amelia"
  double // 将始终是 0
</script>

为了从 store 中提取属性时保持其响应性,需要使用 storeToRefs()

<script setup>
  import { storeToRefs } from 'pinia'
  import { useCounterStore } from '@/stores/counter'
  
  const counterStore = useCounterStore()
  // `name` 和 `double` 是响应式的 ref
  // 同时通过插件添加的属性也会被提取为 ref
  // 并且会跳过所有的 action 或非响应式 (不是 ref 或 reactive) 的属性
  const { name, double } = storeToRefs(counterStore)
  // 作为 action 的 increment 可以直接解构
  const { increment } = counterStore
</script>

5. 向 getter 传递参数

Pinia 中,getter 可以接收参数,就像组件中的计算属性一样。这允许你根据需要创建更灵活和可重用的 getter

// src/stores/counter.js
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  state: () => ({
    numbers: [1, 2, 3, 4, 5],
  }),
  getters: {
    // 定义一个接受参数的 getter
    filteredNumbers: (state) => {
      return (minValue) => state.numbers.filter((number) => number >= minValue)
    },
  },
});
<script setup>
  import { useCounterStore } from '@/stores/counter';
  const counterStore = useCounterStore()

  // 使用 computed 创建一个响应式的引用,并传递参数给 getter
  const filteredNumbers = computed(() => counterStore.filteredNumbers(3));
</script>

<template>
  <div>
    <p>Filtered numbers greater than or equal to 3:</p>
    <ul>
      <li v-for="number in filteredNumbers" :key="number">{{ number }}</li>
    </ul>
  </div>
</template>

五、修改数据状态

1. 直接修改

直接通过在方法中操作 store.属性名 来修改。

<script setup>
  import { useCounterStore } from '@/stores/counter'
  const counterStore = useCounterStore()
  
  const add = () => {
    counterStore.count++ 
  }
</script>

2. 使用 $patch 方法 修改

如果你想同时修改多个状态,或者修改状态需要进行复杂操作,可以使用 $patch 方法,

  • $patch()允许你用一个 state 的补丁对象在同一时间更改多个属性。
// $patch + 对象
<script setup>
  import { useCounterStore } from '@/stores/counter'
  const counterStore = useCounterStore()
  
  const add = () => {
    counterStore.$patch({
      count: store.count++,
      name: 'DIO'
    })
  }
</script>

不过,用这种语法的话,有些变更真的很难实现或者很耗时:任何集合的修改(例如,向数组中添加、移除一个元素或是做 splice 操作)都需要你创建一个新的集合。

  • 因此,$patch 方法也接受一个函数来组合这种难以用补丁对象实现的变更。
// $patch + 函数 
<script setup>
  import { useCounterStore } from '@/stores/counter'
  const counterStore = useCounterStore()
  
  const add = () => {
    counterStore.$patch((state) => {
      state.count = store.count++
      state.name = 'DIO'
      // state.items.push({ name: 'shoes', quantity: 1 })
    })
  }
</script>

3. 使用 action 修改 🌟

在 store 的 actions 中定义的方法,可以包含任何复杂的逻辑,并且它们是响应式的:

// src/stores/counterStore.js
import { defineStore } from "pinia"

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Amelia'
  }),
  getters: {
    // ...
  },
  actions: {
    changeState() {
      this.count++
      this.name = 'DIO'
    },
  }
})
// 组件中使用
<script setup>
  import { useCounterStore } from '@/stores/counter'
  const counterStore = useCounterStore()
  
  const add = () => {
    counterStore.changeState() // 调用 actions 中的 changeState 方法
  }
</script>

🌟 使用 actions 是修改状态的推荐方式,因为它保持了逻辑的封装和可测试性。

六、store之间的相互调用

在 Pinia 中,store 之间的相互调用相对简单。由于 Pinia store 是一个对象,你可以简单地在一个 store 中导入另一个 store,并调用其 actions、getters 或直接修改其 state。

// src/stores/user.js
import { defineStore } from 'pinia';

export const useUserStore = defineStore('user', {
  state: () => ({
    // 用户状态
    userInfo: {
      id: 1,
      name: 'John Doe',
      // ...
    },
  }),
  actions: {
    updateName(name) {
      this.userInfo.name = name;
    },
    // ...
  },
});

1. 在一个 store 中调用另一个 store 的 actions

// src/stores/product.js
import { defineStore } from 'pinia';
import { useUserStore } from './user';

export const useProductStore = defineStore('product', {
  state: () => ({
    // 产品状态
    products: [
      // ...
    ],
  }),
  actions: {
    addProduct(product) {
      // 在添加产品时,可能需要检查用户是否登录
      const userStore = useUserStore();
      if (userStore.userInfo.id) {
        this.products.push(product);
      } else {
        console.error('User must be logged in to add a product');
      }
    },
    // ...
  },
});

2. 在组件中同时使用多个 store

// 组件中使用
<script setup>
  import { useProductStore, useUserStore } from '@/stores';

  const productStore = useProductStore();
  const userStore = useUserStore();

  const updateUserName = () => {
    userStore.updateName('Jane');
  }
  
  const addProduct = () => {
    productStore.addProduct({ id: 1, name: 'New Product' })
  }
</script>
<template>
  <div>
    <h1>User: {{ userStore.userInfo.name }}</h1>
    <button @click="updateUserName">Update User Name</button>
    <button @click="addProduct">Add Product</button>
  </div>
</template>