什么是状态管理 ?
全局状态 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 的写法和其他用法请参考官网