一、介绍
Pinia
是一个用于 Vue
的状态管理库,类似 Vuex
, 是 Vue
的另一种状态管理方案。
为什么使用Pinia
Pinia
的API
设计非常接近Vuex5
的提案。(作者是 Vue 核心团队成员)- 无需像
Vuex
自定义复杂的类型来支持TypeScript
,天生具备完美的类型推断。所有东西都是类型化的,API的设计方式是尽可能地利用TS类型推理。- 在
Vuex
中,TypeScript
的类型提示做得不是很好,在进行类型推导时,只能找到它的state
。特别是写代码的过程中,代码提示就很不智能。 - 而
Pinia
,就能推导出定义的所有state
、getter
、action
,这样在写代码的时候,就会方便很多。
- 在
-
极其的轻量化,
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
结尾。(比如useUserStore
,useCounterStore
,useProductStore
)
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
提供了两种方式来使用 store
,Options Api
和 Composition Api
中都完美支持。
3.1. Options Api
在 Options Api 中,可直接使用官方提供的 mapActions
和 mapState
方法,导出 store 中的 state
、getter
、action
,其用法与 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>