参考信息
Pinia中文文档:pinia.vuejs.org/zh/introduc…
参考帖子: juejin.cn/post/720784…
一、【什么是 pinia ?】
(1)认识Pinia
Pinia 是 Vue 的存储库,它允许您跨组件/页面共享状态。
上面这个是官网对Pinia的一个定义,从定义上我们其实可以看出来,它可能比Vuex要精炼一些(Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
(2)Pinia与Vuex的区别
我们不是已经有Vuex了吗?为什么还要用Pinia呢?
- Pinia 最初是为了探索 Vuex 的下一次迭代会是什么样子,结合了 Vuex 5 核心团队讨论中的许多想法;
- 最终,团队意识到Pinia已经实现了Vuex5中大部分内容,所以最终决定用Pinia来替代Vuex;
- 与 Vuex 相比,Pinia 提供了一个更简单的 API,提供了 Composition-API 风格的 API;
- 最重要的是,在与 TypeScript 一起使用时具有可靠的类型推断支持;
和Vuex相比,Pinia有很多的优势:
1)比如mutations 不再存在;
2)更友好的TypeScript支持,Vuex之前对TS的支持很不友好;
3)不再有modules的嵌套结构:
- 可以创建多个store,并且灵活使用每一个store,它们是通过扁平化的方式来相互使用的;
4)也不再有命名空间的概念,不需要记住它们的复杂关系;
二、【搭建 pinia 环境】
第一步:安装
通过常用的包管理器进行安装
yarn add pinia
// 或者 npm install pinia
第二步:将pinia挂载到Vue应用中
①src/store/index.ts
import type { App } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const store = createPinia()
store.use(piniaPluginPersistedstate)
export const setupStore = (app: App<Element>) => {
app.use(store)
}
export { store }
②src/main.ts
// 引入状态管理
import { setupStore } from '@/store'
import { createApp } from 'vue'
import App from './App.vue'
// 创建实例
const setupAll = async () => {
const app = createApp(App)
setupStore(app)
app.mount('#app')
}
setupAll()
此时开发者工具中已经有了pinia选项
三、【Store】
(1)定义store
Store是一个保存:状态、业务逻辑 的实体,每个组件都可以读取、写入它。它有三个概念:
state、getter、action,相当于组件中的:data、computed和methods。
在Pinia中是使用defineStore() 定义的
// src/store/modules/user.ts
import { defineStore } from 'pinia';
// 第一个参数是应用程序中 store 的唯一 id
export const useUserStore = defineStore('admin-user', {
// 其他配置项,后面逐一说明
})
- defineStore()会返回一个函数,使用该函数便可以获取该store与其中的数据。将返回的函数命名为 use... 是跨可组合项的约定,以使其符合你的使用习惯。
- 第一个参数需要传入一个唯一的名称,也就是store的id,Pinia用它来将store连接到devtools。
- 第二个参数:可接受两类值:
Setup 函数或 Option 对象。store的配置项,比如配置store内的数据,修改数据的方法等。下面分别介绍两类值:
Option Store
与 Vue 的选项式 API 类似,我们也可以传入一个带有 state、actions 与 getters 属性的 Option 对象
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Eduardo'
}),
getters: {
doubleCount: (state): number => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
你可以认为 state 是 store 的数据 (data),getters 是 store 的计算属性 (computed),而 actions 则是方法 (methods)。
Setup Store
也存在另一种定义 store 的可用语法。与 Vue 组合式 API 的 setup 函数 相似,我们可以传入一个函数,该函数定义了一些响应式属性和方法,并且返回一个带有我们想暴露出去的属性和方法的对象。
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref<number>(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
// 返回所有的状态属性和方法
return { count, doubleCount, increment }
})
在 Setup Store 中:
ref()就是state属性computed()就是gettersfunction()就是actions
注意,要让 pinia 正确识别
state,你必须在 setup store 中返回state的所有属性。这意味着,你不能在 store 中使用私有属性。不完整返回会影响 SSR ,开发工具和其他插件的正常运行。
Setup store 比 Option Store 带来了更多的灵活性,因为你可以在一个 store 内创建侦听器,并自由地使用任何组合式函数。不过,请记住,使用组合式函数会让 SSR 变得更加复杂。
Setup store 也可以依赖于全局提供的属性,比如路由。任何应用层面提供的属性都可以在 store 中使用 inject() 访问,就像在组件中一样:
import { inject } from 'vue'
import { useRoute } from 'vue-router'
import { defineStore } from 'pinia'
export const useSearchFilters = defineStore('search-filters', () => {
const route = useRoute()
// 这里假定 `app.provide('appProvided', 'value')` 已经调用过
const appProvided = inject('appProvided')
// ...
return {
// ...
}
})
警告:不要返回像 route 或 appProvided (上例中)之类的属性,因为它们不属于 store,而且你可以在组件中直接用 useRoute() 和 inject('appProvided') 访问。
你应该选用哪种语法?
和在 Vue 中如何选择组合式 API 与选项式 API 一样,选择你觉得最舒服的那一个就好。两种语法都有各自的优势和劣势。Option Store 更容易使用,而 Setup Store 更灵活和强大。如果你想深入了解两者之间的区别,请查看 Mastering Pinia 中的 Option Stores vs Setup Stores 章节。
(2)使用 store
虽然我们前面定义了一个 store,但在我们使用 <script setup> 调用 useStore()(或者使用 setup() 函数,像所有的组件那样) 之前,store 实例是不会被创建的:
<script setup>
import { useCounterStore } from '@/stores/counter'
// 可以在组件中的任意位置访问 `store` 变量 ✨
const store = useCounterStore()
</script>
如果你还不会使用
setup组件,你也可以通过映射辅助函数来使用 Pinia。
你可以定义任意多的 store,但为了让使用 pinia 的益处最大化 (比如允许构建工具自动进行代码分割以及 TypeScript 推断),你应该在不同的文件中去定义 store。
一旦 store 被实例化,你可以直接访问在 store 的 state、getters 和 actions 中定义的任何属性。我们将在后续章节继续了解这些细节,目前自动补全将帮助你使用相关属性。
我们可以通过devtools来清楚查看到定义的store:
(3)从 Store 解构
store 是一个用 reactive 包装的对象,这意味着不需要在 getters 后面写 .value。就像 setup 中的 props 一样,我们不能对它进行解构:
<script setup>
import { useCounterStore } from '@/stores/counter'
import { computed } from 'vue'
const store = useCounterStore()
// ❌ 这将不起作用,因为它破坏了响应性
// 这就和直接解构 `props` 一样
const { name, doubleCount } = store
name // 将始终是 "Eduardo"
doubleCount // 将始终是 0
setTimeout(() => {
store.increment()
}, 1000)
// ✅ 这样写是响应式的
// 💡 当然你也可以直接使用 `store.doubleCount`
const doubleValue = computed(() => store.doubleCount)
</script>
为了从 store 中提取属性时保持其响应性,你需要使用 storeToRefs()。它将为每一个响应式属性创建引用。当你只使用 store 的状态而不调用任何 action 时,它会非常有用。请注意,你可以直接从 store 中解构 action,因为它们也被绑定到 store 上:
<script setup>
import { storeToRefs } from 'pinia'
const store = useCounterStore()
// `name` 和 `doubleCount` 是响应式的 ref
// 同时通过插件添加的属性也会被提取为 ref
// 并且会跳过所有的 action 或非响应式 (不是 ref 或 reactive) 的属性
const { name, doubleCount } = storeToRefs(store)
// 作为 action 的 increment 可以直接解构
const { increment } = store
</script>
四、【state】
(1)定义state
在Pinia中,状态(State)被定义为返回初始状态的函数:
// src/store/modules/user.js
import { defineStore } from 'pinia';
export const useUserStore = defineStore('admin-user', {
// 推荐使用 完整类型推断的箭头函数
state: () => {
return {
// 所有这些属性都将自动推断其类型
name: 'Hello World',
age: 18,
isStudent: false
}
}
// 还有一种定义state的方式
// state: () => ({
// name: 'Hello World',
// age: 18,
// isStudent: false
// })
})
你并不需要做太多努力就能使你的 state 兼容 TS。确保启用了 strict,或者至少启用了 noImplicitThis,Pinia 将自动推断您的状态类型! 但是,在某些情况下,您应该帮助它进行一些转换:
const useStore = defineStore('storeId', {
state: () => {
return {
// 用于初始化空列表
userList: [] as UserInfo[],
// 用于尚未加载的数据
user: null as UserInfo | null,
}
},
})
interface UserInfo {
name: string
age: number
}
如果你愿意,你可以用一个接口定义 state,并添加 state() 的返回值的类型。
interface State {
userList: UserInfo[]
user: UserInfo | null
}
interface UserInfo {
name: string
age: number
}
const useStore = defineStore('storeId', {
state: (): State => {
return {
userList: [],
user: null,
}
},
})
(2)使用state
默认情况下,你可以通过 store 实例访问 state,直接对其进行读写。
const store = useStore()
store.count++
注意,新的属性如果没有在
state()中被定义,则不能被添加。它必须包含初始状态。例如:如果secondCount没有在state()中定义,我们无法执行store.secondCount = 2。
(3)变更 state
除了用 store.count++ 直接改变 store,你还可以调用 $patch 方法。
$patch 方法提供了两种方式来修改状态:
1、使用对象:传递一个对象来修改状态。这种方式适合简单的状态修改。
store.$patch({
count: store.count + 1,
age: 120,
name: 'DIO',
})
2、使用函数:传递一个函数来修改状态。这种方式适合复杂的状态修改,特别是对集合的修改。例如添加、移除或修改数组中的元素。
store.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})
两种变更 store 方法的主要区别是,$patch() 允许你将多个变更归入 devtools 的同一个条目中。同时请注意,直接修改 state,$patch() 也会出现在 devtools 中,而且可以进行 time travel (在 Vue 3 中还没有)。
(4)重置state
使用Option Store 时,你可以通过调用 store 的 $reset() 方法将 state 重置为初始值。
const store = useStore()
store.$reset()
在 $reset() 内部,会调用 state() 函数来创建一个新的状态对象,并用它替换当前状态。
在 Setup Stores 中,您需要创建自己的 $reset() 方法:
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function $reset() {
count.value = 0
}
return { count, $reset }
})
(5)替换state
你不能完全替换掉 store 的 state,因为那样会破坏其响应性。但是,你可以 使用 $patch。
// 这实际上并没有替换`$state`
store.$state = { count: 24 }
// 在它内部调用 `$patch()`:
store.$patch({ count: 24 })
(6)订阅state
类似于 Vuex 的 subscribe 方法,你可以通过 store 的 $subscribe() 方法侦听 state 及其变化。比起普通的 watch(),使用 $subscribe() 的好处是 subscriptions 在 patch 后只触发一次。
- 直接修改状态:每次直接修改状态时,Pinia 会立即记录变更并触发订阅回调。
- 使用
$patch方法:当使用$patch方法时,Pinia 会在所有变更完成后触发一次订阅回调,无论是对象形式还是函数形式的$patch。
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
items: [],
hasChanged: false
}),
actions: {
addItem(item) {
this.$patch((state) => {
state.items.push(item)
state.hasChanged = true
})
},
removeItem(index) {
this.$patch((state) => {
state.items.splice(index, 1)
state.hasChanged = true
})
}
}
})
// 在组件或其他地方使用 $subscribe 方法
const cartStore = useCartStore()
cartStore.$subscribe((mutation, state) => {
// import { MutationType } from 'pinia'
mutation.type // 'direct' | 'patch object' | 'patch function'
// 和 cartStore.$id 一样
mutation.storeId // 'cart'
// 只有 mutation.type === 'patch object'的情况下才可用
mutation.payload // 传递给 cartStore.$patch() 的补丁对象。
// 每当状态发生变化时,将整个 state 持久化到本地存储。
localStorage.setItem('cart', JSON.stringify(state))
})
默认情况下,state subscription 会被绑定到添加它们的组件上 (如果 store 在组件的 setup() 里面)。这意味着,当该组件被卸载时,它们将被自动删除。如果你想在组件卸载后依旧保留它们,请将 { detached: true } 作为第二个参数,以将 state subscription 从当前组件中分离:
<script setup>
import { useCartStore } from './stores/cartStore'
const cartStore = useCartStore()
// 此订阅器即便在组件卸载之后仍会被保留
cartStore.$subscribe((mutation, state) => {
localStorage.setItem('cart', JSON.stringify(state))
}, { detached: true })
</script>
你可以在
pinia实例上使用watch()函数侦听整个 state。
import { createPinia } from 'pinia'
import { watch } from 'vue'
const pinia = createPinia()
watch(
pinia.state,
(state) => {
// 每当状态发生变化时,将整个 state 持久化到本地存储
localStorage.setItem('piniaState', JSON.stringify(state))
},
{ deep: true }
)
五、【Getter】
(1)定义getter
Getter 完全等同于 store 的 state 的计算值。
可以通过 defineStore() 中的 getters 属性来定义它们。推荐使用箭头函数,并且它将接收 state 作为第一个参数:
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
},
})
大多数时候,getter 仅依赖 state。不过,有时它们也可能会使用其他 getter。
因此,即使在使用常规函数定义 getter 时,我们也可以通过 this 访问到整个 store 实例,但(在 TypeScript 中)必须定义返回类型。
这是为了避免 TypeScript 的已知缺陷,不过这不影响用箭头函数定义的 getter,也不会影响不使用 this 的 getter。
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
// 自动推断出返回类型是一个 number
doubleCount(state) {
return state.count * 2
},
// 返回类型**必须**明确设置
doublePlusOne(): number {
// 整个 store 的 自动补全和类型标注 ✨
return this.doubleCount + 1
},
},
})
然后你可以直接访问 store 实例上的 getter 了:
<script setup>
import { useCounterStore } from './counterStore'
const store = useCounterStore()
</script>
<template>
<p>Double count is {{ store.doubleCount }}</p>
</template>
(2)访问其他getter
与计算属性一样,你也可以组合多个 getter。通过 this,你可以访问到其他任何 getter。在这种情况下,你需要为这个 getter 指定一个返回值的类型。
在counterStore.tscounterStore.js
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
doubleCount(state) {
return state.count * 2
},
doubleCountPlusOne(): number {
return this.doubleCount + 1
},
},
})
在counterStore.js
// 你可以在 JavaScript 中使用 JSDoc (https://jsdoc.app/tags-returns.html)
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
// 类型是自动推断出来的,因为我们没有使用 `this`
doubleCount: (state) => state.count * 2,
// 这里我们需要自己添加类型(在 JS 中使用 JSDoc)
// 可以用 this 来引用 getter
/**
* 返回 count 的值乘以 2 加 1
*
* @returns {number}
*/
doubleCountPlusOne() {
// 自动补全 ✨
return this.doubleCount + 1
},
},
})
(3)向 getter 传递参数
Getter 只是幕后的计算属性,所以不可以向它们传递任何参数。不过,你可以从 getter 返回一个函数,该函数可以接受任意参数:
export const useUserListStore = defineStore('userList', {
getters: {
getUserById: (state) => {
return (userId) => state.users.find((user) => user.id === userId)
},
},
})
并在组件中使用:
<script setup>
import { useUserListStore } from './store'
const userList = useUserListStore()
const { getUserById } = storeToRefs(userList)
// 请注意,你需要使用 `getUserById.value` 来访问
// <script setup> 中的函数
</script>
<template>
<p>User 2: {{ getUserById(2) }}</p>
</template>
请注意,当你这样做时,
getter 将不再被缓存(因为传参)。它们只是一个被你调用的函数。不过,你可以在 getter 本身中缓存一些结果,虽然这种做法并不常见,但有证明表明它的性能会更好:
import { defineStore } from 'pinia'
export const useUserListStore = defineStore('userList', {
state: () => ({
users: [
{ id: 1, name: 'Alice', active: true },
{ id: 2, name: 'Bob', active: false },
{ id: 3, name: 'Charlie', active: true }
]
}),
getters: {
getActiveUserById(state) {
// 过滤出所有活跃用户
const activeUsers = state.users.filter((user) => user.active)
// 返回一个函数,该函数接受 userId 并返回对应的活跃用户
return (userId) => activeUsers.find((user) => user.id === userId)
}
}
})
(4)访问其他 store 的 getter
想要使用另一个 store 的 getter 的话,那就直接在 getter 内使用就好:
import { useOtherStore } from './other-store'
export const useStore = defineStore('main', {
state: () => ({
// ...
}),
getters: {
otherGetter(state) {
const otherStore = useOtherStore()
return state.localData + otherStore.data
},
},
})
(5)使用 setup() 时的用法
作为 store 的一个属性,你可以直接访问任何 getter(与 state 属性完全一样):
// 示例文件路径:
// ./src/stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 3,
}),
getters: {
doubleCount(state) {
return state.count * 2
},
},
})
<script setup>
const store = useCounterStore()
store.count = 3
store.doubleCount // 6
</script>
(6)选项式 API 的用法
① 使用 setup()
虽然并不是每个开发者都会使用组合式 API,但 setup() 钩子依旧可以使 Pinia 在选项式 API 中更易用。并且不需要额外的映射辅助函数!
<script>
import { useCounterStore } from '../stores/counter'
export default defineComponent({
setup() {
const counterStore = useCounterStore()
return { counterStore }
},
computed: {
quadrupleCounter() {
return this.counterStore.doubleCount * 2
},
},
})
</script>
这在将组件从选项式 API 迁移到组合式 API 时很有用,但应该只是一个迁移步骤。始终尽量不要在同一组件中混合两种 API 样式。
六、【Action】
(1)定义action
Action 相当于组件中的 method。它们可以通过 defineStore() 中的 actions 属性来定义,并且它们也是定义业务逻辑的完美选择。
export const useCounterStore = defineStore('main', {
state: () => ({
count: 0,
}),
actions: {
increment() {
this.count++
},
randomizeCounter() {
this.count = Math.round(100 * Math.random())
},
},
})
类似 getter,action 也可通过 this 访问整个 store 实例,并支持完整的类型标注(以及自动补全✨) 。不同的是,action 可以是异步的,你可以在它们里面 await 调用任何 API,以及其他 action!
请注意,你使用什么库并不重要,只要你得到的是一个
Promise。你甚至可以 (在浏览器中) 使用原生fetch函数:
import { defineStore } from 'pinia'
import { mande } from 'mande'
const api = mande('/api/users')
export const useUsers = defineStore('users', {
state: () => ({
userData: null,
// 其他状态属性...
}),
actions: {
async registerUser(login: string, password: string) {
try {
// 调用 API 注册用户,并将返回的数据存储在 userData 中
this.userData = await api.post({ login, password })
showTooltip(`Welcome back ${this.userData.name}!`)
} catch (error) {
showTooltip(error)
// 返回错误信息,以便表单组件显示错误
return error
}
},
},
})
你也完全可以自由地设置任何你想要的参数以及返回任何结果。当调用 action 时,一切类型也都是可以被自动推断出来的。
Action 可以像函数或者通常意义上的方法一样被调用:
<script setup>
const store = useCounterStore()
// 将 action 作为 store 的方法进行调用
store.randomizeCounter()
</script>
<template>
<!-- 即使在模板中也可以 -->
<button @click="store.randomizeCounter()">Randomize</button>
</template>
(2)访问其他 store 的 action
想要使用另一个 store 的话,那你直接在 action 中调用就好了:
import { useAuthStore } from './auth-store'
export const useSettingsStore = defineStore('settings', {
state: () => ({
preferences: null,
// ...
}),
actions: {
async fetchUserPreferences() {
const auth = useAuthStore()
if (auth.isAuthenticated) {
this.preferences = await fetchPreferences()
} else {
throw new Error('User must be authenticated')
}
},
},
})
(3)使用选项式 API 的用法
在下面的例子中,你可以假设相关的 store 已经创建了:
// 示例文件路径:
// ./src/stores/counter.js
import { defineStore } from 'pinia'
const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
actions: {
increment() {
this.count++
},
},
})
① 使用 setup()
虽然并不是每个开发者都会使用组合式 API,但 setup() 钩子依旧可以使 Pinia 在选项式 API 中更易用。并且不需要额外的映射辅助函数!
<script>
import { useCounterStore } from '../stores/counter'
export default defineComponent({
setup() {
const counterStore = useCounterStore()
return { counterStore }
},
methods: {
incrementAndPrint() {
this.counterStore.increment()
console.log('New Count:', this.counterStore.count)
},
},
})
</script>
(4)订阅 action
你可以通过 store.$onAction() 来监听 action 和它们的结果。传递给它的回调函数会在 action 本身之前执行。after 表示在 promise 解决之后,允许你在 action 解决后执行一个回调函数。同样地,onError 允许你在 action 抛出错误或 reject 时执行一个回调函数。这些函数对于追踪运行时错误非常有用,类似于Vue docs 中的这个提示。
这里有一个例子,在运行 action 之前以及 action resolve/reject 之后打印日志记录。
const unsubscribe = someStore.$onAction(
({
name, // action 名称
store, // store 实例,类似 `someStore`
args, // 传递给 action 的参数数组
after, // 在 action 返回或解决后的钩子
onError, // action 抛出或拒绝的钩子
}) => {
// 为这个特定的 action 调用提供一个共享变量
const startTime = Date.now()
// 这将在执行 "store "的 action 之前触发。
console.log(`Start "${name}" with params [${args.join(', ')}].`)
// 这将在 action 成功并完全运行后触发。
// 它等待着任何返回的 promise
after((result) => {
console.log(
`Finished "${name}" after ${
Date.now() - startTime
}ms.\nResult: ${result}.`
)
})
// 如果 action 抛出或返回一个拒绝的 promise,这将触发
onError((error) => {
console.warn(
`Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
)
})
}
)
// 手动删除监听器
unsubscribe()
默认情况下,action 订阅器会被绑定到添加它们的组件上(如果 store 在组件的 setup() 内)。这意味着,当该组件被卸载时,它们将被自动删除。如果你想在组件卸载后依旧保留它们,请将 true 作为第二个参数传递给 action 订阅器,以便将其从当前组件中分离:
<script setup>
const someStore = useSomeStore()
// 此订阅器即便在组件卸载之后仍会被保留
someStore.$onAction(callback, true)
</script>