什么是状态管理 ?
全局状态 Store (如 Pinia) 是一个保存状态和业务逻辑的实体,与组件树没有绑定,有点像一个永远存在的组件,每个组件都可以读取和写入它。
三大核心概念
- state 属性 —— 相当于组件中的 data
- getter 计算属性 —— 相当于组件中的 computed
- action 操作属性的行为 —— 相当于组件中的 methods
什么时候使用 Store?
只有多个组件(父子组件除外,至少是兄弟组件)都需要使用的数据,才应使用 Store。
若使用组合式函数能实现,或者应该只是组件的本地状态的数据,就不应使用 Store。
为什么使用 Pinia ?
更简单易用,官方推荐。
安装 Pinia
通常创建 vue3 项目时,选择安装 Pinia 就会自动集成。
但若目前项目里没有,则按如下流程操作
1. 安装 Pinia
npm i pinia
2. 导入使用 Pinia
src/main.ts
import { createPinia } from 'pinia'
app.use(createPinia())
Pinia 的使用
以全局状态 counter 为例:
1. 定义状态管理器
- state 属性用 ref()
- getters 用 computed()
- actions 用 function()
新建文件 src/stores/counter.ts
import { ref, computed } from 'vue' import { defineStore } from 'pinia' // 定义状态管理器生成函数 useCounterStore,第一个参数为状态管理器的名称 export const useCounterStore = defineStore('counter', () => { // State -- 定义目标状态 count,默认值为 0 const count = ref(0) // Getter -- 定义自动计算的状态,它随目标状态 count 的变化,会自动生成新的值 const doubleCount = computed(() => count.value * 2) // Action -- 定义操作目标状态的方法,用于修改目标状态 function increment() { count.value++ } // 返回定义的 State,Getter,Action return { count, doubleCount, increment } })
模块化
stores 中的每一个状态管理器定义文件,就是一个模块。
根据业务需要,将同类型的状态放在一个状态管理器中进行管理,文件名为模块名.ts,如 counter.ts
状态管理器生成函数的命名规则【推荐】
以 use 开头,Store 结尾,以状态管理器 counter 为例,状态管理器生成函数的名称为 useCounterStore
2. 使用状态管理器
<script setup lang="ts"> // 导入状态管理器 import { useCounterStore } from '@/stores/counter' // 生成状态管理器实例 const counter = useCounterStore() </script> <template> <div> <!-- 使用状态管理器中的 State --> <p>count: {{ counter.count }}</p> <!-- 使用状态管理器中的 Getter --> <p>双倍的 count: {{ counter.doubleCount }}</p> <!-- 使用状态管理器中的 Action --> <button @click="counter.increment">count+1</button> </div> </template>
Action 中支持异步获取数据
src/stores/user.ts
import { ref } from 'vue' import type { Ref } from 'vue' import { defineStore } from 'pinia' import axios from 'axios' interface userInfo { id: number name: String } export const useUserStore = defineStore('user', () => { const userList: Ref<userInfo[]> = ref([]) // Action -- 支持异步 async function getList() { try { const res = await axios.get('http://jsonplaceholder.typicode.com/users') userList.value = res.data } catch (error) { return error } } return { userList, getList } })
src/test.vue
<script setup lang="ts"> import { useUserStore } from '@/stores/user' const User = useUserStore() </script> <template> <div> <ul> <li v-for="item in User.userList" :key="item.id">{{ item.name }}</li> </ul> <button @click="User.getList">获取用户列表</button> </div> </template>
解构时使用 storeToRefs() 保持响应式
src/stores/login.ts
import { ref } from 'vue' import { defineStore } from 'pinia' import axios from 'axios' export const useLoginStore = defineStore('login', () => { const ifLogin = ref(false) const userInfo = ref({}) async function login() { ifLogin.value = true try { const res = await axios.get('https://jsonplaceholder.typicode.com/posts/1') userInfo.value = res.data } catch (error) { return error } } function logout() { ifLogin.value = false userInfo.value = {} } return { ifLogin, userInfo, login, logout } })
src/test.vue
<script setup lang="ts"> import { storeToRefs } from 'pinia' import { useLoginStore } from '@/stores/login' const LoginStore = useLoginStore() // store 中的 State 需用 storeToRefs() 处理后,才能在解构时保持响应式 const { ifLogin, userInfo } = storeToRefs(LoginStore) // store 中的 Action 可以直接解构 const { login, logout } = LoginStore </script> <template> <div> <p>ifLogin:{{ ifLogin }}</p> <p>userInfo:{{ userInfo }}</p> <button v-if="ifLogin" @click="logout">登出</button> <button v-else @click="login">登录</button> </div> </template>
修改 State 的三种方式
方式一 : 使用 Action【推荐】
比较规范的写法,是将所有对 store 中 State 的修改都用 Action 实现,比如
// src/stores/login.ts 中 function logout() { ifLogin.value = false userInfo.value = {} }
页面中调用触发修改
import { useLoginStore } from '@/stores/login' const LoginStore = useLoginStore() const { login, logout } = LoginStore
<button @click="logout">登出</button>
方式二 : 使用 $patch
页面中使用 $patch 触发修改
import { useLoginStore } from '@/stores/login' const LoginStore = useLoginStore() function logout() { LoginStore.$patch((state) => { state.ifLogin = false state.userInfo = {} }) }
方式三 : 直接修改【不推荐】
从代码结构的角度考虑,全局的状态管理不应直接在各个组件中随意修改,而应集中在 action 中统一方法修改
import { useLoginStore } from '@/stores/login' const LoginStore = useLoginStore() function logout() { LoginStore.ifLogin = false LoginStore.userInfo = {} }
自行实现 $reset() 方法
用选项式 API 时,可以通过调用 store 的 $reset() 方法将 state 重置为初始值。
但在组合式 API 中,需要自行实现 $reset() 方法,如
export const useCounterStore = defineStore('counter', () => { const count = ref(0) // $reset 在组合式 API 中需自行实现 function $reset() { count.value = 0 } return { count, $reset } })
状态持久化(避免刷新页面后状态丢失)
1. 安装插件 pinia-plugin-persistedstate
npm i pinia-plugin-persistedstate
2. 导入启用
src/main.ts
import { createPinia } from 'pinia' import { createPersistedState } from 'pinia-plugin-persistedstate' const pinia = createPinia() pinia.use( createPersistedState({ // 所有 Store 都开启持久化存储 auto: true }) )
app.use(pinia)
配置关闭持久化
针对需要关闭持久化的 store ,添加配置
{ persist: false }
详细范例如下:src/stores/counter.ts
import { ref, computed } from 'vue' import { defineStore } from 'pinia' export const useCounterStore = defineStore( 'counter', () => { const count = ref(0) const doubleCount = computed(() => count.value * 2) function increment() { count.value++ } return { count, doubleCount, increment } }, // 关闭持久化 { persist: false } )
当然,也可以默认全局不启用持久化
// src/main.ts 中 pinia.use( createPersistedState({ auto: false }) )
只针对部分 store 开启持久化
// 开启持久化(目标 store 中) { persist: true }
更多高级用法,可以参考插件的官网
https://prazdevs.github.io/pinia-plugin-persistedstate/zh/
选项式 API 的写法和其他用法请参考官网
https://pinia.vuejs.org/zh/