前端开发整理(五): Vue框架的组件通信和全局状态管理
组件间通信
父子组件通信
父子组件通信是指,B 组件引入到 A 组件里渲染,此时 A 是 B 的父级;B 组件的一些数据需要从 A 组件拿,B 组件有时也要告知 A 组件一些数据变化情况;Child.vue 是直接挂载在 Father.vue 下面的。
# 父组件
Father.vue
│ # 子组件
└─Child.vue
常用的方法有:
| 方案 | 父组件向子组件 | 子组件向父组件 |
|---|---|---|
| props / emits | props | emits |
| v-model / emits | v-model | emits |
| ref / emits | ref | emits |
| provide / inject | provide | inject |
| EventBus | emit / on | emit / on |
| Reactive State | - | - |
| Vuex | - | - |
| Pinia | - | - |
为了方便阅读,下面的父组件统一叫 Father.vue ,子组件统一叫 Child.vue 。
props / emits
这是 Vue 跨组件通信最常用,也是基础的一个方案,它的通信过程是:
- 父组件 Father.vue 通过 props 向子组件 Child.vue 传值
- 子组件 Child.vue 则可以通过 emits 向父组件 Father.vue 发起事件通知
最常见的场景就是统一在父组件发起 AJAX 请求,拿到数据后,再根据子组件的渲染需要传递不同的 props 给不同的子组件使用。
下发 props
下发的过程是在 Father.vue 里完成的,父组件在向子组件下发 props 之前,需要导入子组件并启用它作为自身的模板,然后在setup 里处理好数据并 return 给<template /> 用。
在 Father.vue 的<script /> 里:
// Father.vue
import { defineComponent } from 'vue'
import Child from '@cp/Child.vue'
interface Member {
id: number
name: string
}
export default defineComponent({
// 需要启用子组件作为模板
components: {
Child,
},
// 定义一些数据并 `return` 给 `<template />` 用
setup() {
const userInfo: Member = {
id: 1,
name: 'Petter',
}
// 不要忘记 `return` ,否则 `<template />` 拿不到数据
return {
userInfo,
}
},
})
然后在 Father.vue 的<template /> 这边拿到 return 出来的数据,把要传递的数据通过属性的方式绑定在组件标签上:
<!-- Father.vue -->
<template>
<Child
title="用户信息"
:index="1"
:uid="userInfo.id"
:user-name="userInfo.name"
/>
</template>
在<template /> 绑定属性这里,如果是普通的字符串,比如上面的title,则直接给属性名赋值就可以。如果是变量名,或者其他类型如number 、boolean 等,比如上面的index,则需要通过属性动态绑定的方式来添加,使用v-bind: 或者: 符号进行绑定。
这样就完成了 props 数据的下发。
接收 props
接收的过程是在 Child.vue 里完成的,在<script /> 部分,子组件通过与setup 同级的 props 来接收数据。
它可以是一个string[] 数组,把要接受的变量名放到这个数组里,直接放进来作为数组的item :
// Child.vue
export default defineComponent({
props: ['title', 'index', 'userName', 'uid'],
})
但这种情况下,使用者不知道这些属性的使用限制,例如是什么类型的值、是否必传等等。
带有类型限制的 props
和 TypeScript 一样,类型限制可以为程序带来更好的健壮性, Vue 的 props 也支持增加类型限制。
相对于传递一个string[] 类型的数组,更推荐的方式是把 props 定义为一个对象,以对象形式列出,每个 Property 的名称和值分别是各自的名称和类型,只有合法的类型才允许传入。
支持的类型有:
| 类型 | 含义 |
|---|---|
| String | 字符串 |
| Number | 数值 |
| Boolean | 布尔值 |
| Array | 数组 |
| Object | 对象 |
| Date | 日期数据,e.g.new Date() |
| Function | 函数,e.g. 普通函数、箭头函数、构造函数 |
| Promise | Promise 类型的函数 |
| Symbol | Symbol 类型的值 |
给 props 加上类型限制的示例代码为:
// Child.vue
export default defineComponent({
props: {
title: String,
index: Number,
userName: String,
uid: Number,
},
})
如果需要对某个 Prop 允许多类型,比如这个uid 字段,可能是数值,也可能是字符串,那么可以在类型这里,使用一个数组,把允许的类型都加进去:
// Child.vue
export default defineComponent({
props: {
// 单类型
title: String,
index: Number,
userName: String,
// 这里使用了多种类型
uid: [Number, String],
},
})
可选以及带有默认值的 props
所有 props 默认都是可选的,如果不传递具体的值,则默认值都是undefined ,可能引起程序运行崩溃, Vue 支持对可选的 props 设置默认值,也是通过对象的形式配置 props 的选项。
其中支持配置的选项有:
| 选项 | 类型 | 含义 |
|---|---|---|
| type | string | 类型 |
| required | boolean | 是否必传,true 代表必传,false 代表可选 |
| default | any | 与type 选项的类型相对应的默认值,如果required 选项是false ,但这里不设置默认值,则会默认为undefined |
| validator | function | 自定义验证函数,需要 return 一个布尔值,true 代表校验通过,false 代表校验不通过,当校验不通过时,控制台会抛出警告信息 |
了解了配置选项后,接下来再对 props 进行改造,将其中部分选项设置为可选,并提供默认值:
// Child.vue
export default defineComponent({
props: {
// 可选,并提供默认值
title: {
type: String,
required: false,
default: '默认标题',
},
// 默认可选,单类型
index: Number,
// 添加一些自定义校验
userName: {
type: String,
// 在这里校验用户名必须至少 3 个字
validator: (v) => v.length >= 3,
},
// 默认可选,但允许多种类型
uid: [Number, String],
},
})
使用 props
该部分也是在Child.vue中操作。
在<template /> 部分, Vue 3 的使用方法和 Vue 2 是一样的,比如要渲染父组件传入的 props :
<!-- Child.vue -->
<template>
<p>标题:{{ title }}</p>
<p>索引:{{ index }}</p>
<p>用户id:{{ uid }}</p>
<p>用户名:{{ userName }}</p>
</template>
在 Vue 2 里,只需要通过this.uid 、this.userName 就可以使用父组件传下来的 Prop ,但是 Vue 3 没有了this ,所以是通过setup 的入参进行操作:
// Child.vue
export default defineComponent({
props: {
title: String,
index: Number,
userName: String,
uid: Number,
},
// 在这里需要添加一个入参
setup(props) {
// 该入参包含了当前组件定义的所有 props
console.log(props)
},
})
关于 Setup 函数的第一个入参props :
- 该入参包含了当前组件定义的所有 props (如果父组件 Father.vue 传进来的数据在 Child.vue 里未定义,不仅不会拿到,并且在控制台会有警告信息)。
- 该入参可以随意命名,比如可以写成一个下划线
_,通过_.uid也可以拿到数据,但是语义化命名是一个良好的编程习惯。 - 该入参具备响应性,父组件修改了传递下来的值,子组件也会同步得到更新,因此请不要直接解构,可以通过 toRef 或 toRefs API 转换为响应式变量
传递和获取非props属性
如果父组件 Father.vue 传进来的数据在 Child.vue 里未定义,不仅不会拿到,并且在控制台会有警告信息。这种情况虽然无法从 props 里拿到对应的数据,但也不意味着不能传递任何未定义的属性数据,在父组件,除了可以给子组件绑定 props ,还可以根据实际需要去绑定一些特殊的属性。
比如给子组件设置class、id,或者data-xxx 之类的一些自定义属性,如果子组件 Child.vue 的<template /> 里只有一个根节点,那么这些属性默认会自动继承并渲染在 Node 节点上。
这一类非 props 属性通常称之为attrs 。
在 Child.vue 里,可以通过setup 的第二个参数context 里的attrs 来获取到这些属性,并且父组件传递了什么类型的值,获取到的也是一样的类型,这一点和使用Element.getAttribute() 完全不同。Vue 3 允许多个根节点,多个根节点的情况下,无法直接继承这些 attrs 属性(在inheritAttrs: true 的情况也下无法默认继承),需要在子组件 Child.vue 里通过v-bind 绑定到要继承在节点上。
绑定、接收、调用与校验emits
父组件 Father.vue 需要获取子组件 Child.vue 的数据更新情况,可以由子组件通过 emits 进行通知。事件的逻辑是由父组件决定的,因此需要在父组件 Father.vue 的<script /> 里先声明数据变量和一个更新函数,并且这个更新函数通常会有一个入参作为数据的新值接收。
和 props 一样,可以指定是一个数组,把要接收的 emit 事件名称写进去;如果子组件需要更新数据并通知父组件,可以使用setup 第二个参数context 里的emit 方法触发。emit 方法最少要传递一个参数:事件名称。
事件名称是指父组件 Father.vue 绑定事件时@update-age="updateAge" 里的update-age ,如果改成@hello="updateAge" ,那么事件名称就需要使用hello ,一般情况下事件名称和更新函数的名称会保持一致,方便维护。
对于需要更新数据的情况,emit 还支持传递更多的参数,对应更新函数里的入参,所以可以看到上面例子里的emit('update-age', 2) 有第二个参数,传递了一个2 的数值,就是作为父组件updateAge 的入参newAge 传递。
如果需要通信的数据很多,建议第二个入参使用一个对象来管理数据;子组件在传递新数据时,就应该使用对象的形式传递;这对于更新表单等数据量较多的场景非常好用。
和 props 一样,子组件在接收 emits 时也可以对这些事件做一些验证,这个时候就需要将 emits 配置为对象,然后把事件名称作为key ,value 则对应为一个用来校验的方法。
v-model / emits
使用 v-model 的方式更为简单:
- 在 Father.vue ,通过 v-model 向 Child.vue 传值
- Child.vue 通过自身设定的 emits 向 Father.vue 通知数据更新
v-model 的用法和 props 非常相似,但是很多操作上更为简化,但操作简单带来的 “副作用” ,就是功能上也没有 props 那么多。
绑定 v-model
在 Father.vue 里操作,和下发 props 的方式类似,都是在子组件上绑定 Father.vue 定义好的数据,这是绑定一个数据的例子:
<!-- Father.vue -->
<template>
<Child v-model:username="userInfo.name" />
</template>
Vue 3 可以直接绑定 v-model ,而无需在子组件指定 model 选项,并且 Vue 3 的 v-model 需要使用英文冒号 : 指定要绑定的属性名,同时也支持绑定多个 v-model 。如果要绑定多个数据,写多个 v-model 即可:
<!-- Father.vue -->
<template>
<Child
v-model:uid="userInfo.id"
v-model:username="userInfo.name"
v-model:age="userInfo.age"
/>
</template>
一个 v-model 其实就是一个 prop ,它支持的数据类型和 prop 是一样的,所以子组件在接收数据的时候,完全按照 props 去定义就可以了。
配置emits
使用 props / emits ,如果要更新父组件的数据,还需要在父组件声明一个更新函数并绑定事件给子组件,才能够更新。
而使用 v-model / emits ,无需在父组件声明更新函数,只需要在子组件 Child.vue 里通过update: 前缀加上 v-model 的属性名这样的格式,即可直接定义一个更新事件:
// Child.vue
export default defineComponent({
props: {
uid: Number,
username: String,
age: Number,
},
// 注意这里的 `update:` 前缀
emits: ['update:uid', 'update:username', 'update:age'],
})
ref / emits
父组件可以给子组件绑定ref 属性,然后通过 Ref 变量操作子组件的数据或者调用子组件里面的方法。先在<template /> 处给子组件标签绑定ref 属性:
<!-- Father.vue -->
<template>
<Child ref="child" />
</template>
然后在<script /> 部分定义好对应的变量名称child (记得要 return 出来哦),即可通过该变量操作子组件上的变量或方法:
// Father.vue
import { defineComponent, onMounted, ref } from 'vue'
import Child from '@cp/Child.vue'
export default defineComponent({
components: {
Child,
},
setup() {
// 给子组件定义一个 `ref` 变量
const child = ref<InstanceType<typeof Child>>()
// 请保证视图渲染完毕后再执行操作
onMounted(async () => {
// 执行子组件里面的 AJAX 请求函数
await child.value!.queryList()
// 显示子组件里面的弹窗
child.value!.isShowDialog = true
})
// 必须 `return` 出去才可以给到 `<template />` 使用
return {
child,
}
},
})
需要注意的是,在子组件 Child.vue 里,变量和方法也需要在setup 里 return 出来才可以被父组件调用到。
爷孙组件通信
爷孙组件是比父子组件通信要更深层次的引用关系(也有称之为 “隔代组件” )。
C 组件被引入到 B 组件里, B 组件又被引入到 A 组件里渲染,此时 A 是 C 的爷爷级别(可能还有更多层级关系),它们之间的关系可以假设如下:
Grandfather.vue
└─Son.vue
└─Grandson.vue
Grandson.vue 并非直接挂载在 Grandfather.vue 下面,他们之间还隔着至少一个 Son.vue (在实际业务中可能存在更多层级),如果使用 props ,只能一级组件一级组件传递下去,太繁琐。
因此需要更直接的通信方式来解决这种问题,这一 Part 就是讲一讲 C 和 A 之间的数据传递,常用的方法有:
| 方案 | 爷组件向孙组件 | 孙组件向爷组件 |
|---|---|---|
| provide / inject | provide | inject |
| EventBus | emit / on | emit / on |
| Reactive State | - | - |
| Vuex | - | - |
| Pinia | - | - |
因为上下级的关系的一致性,爷孙组件通信的方案也适用于父子组件通信,只需要把爷孙关系换成父子关系即可,为了方便阅读,下面的爷组件统一叫 Grandfather.vue,子组件统一叫 Grandson.vue 。
provide / inject
这个通信方式也是有两部分:
- Grandfather.vue 通过 provide 向孙组件 Grandson.vue 提供数据和方法
- Grandson.vue 通过 inject 注入爷爷组件 Grandfather.vue 的数据和方法
无论组件层次结构有多深,发起 provide 的组件都可以作为其所有下级组件的依赖提供者。provide 和 inject 绑定并不是可响应的,这是刻意为之的,除非传入了一个可侦听的对象。
发起 provide
在 Vue 3 , provide 需要导入并在setup 里启用,并且现在是一个全新的方法,每次要 provide 一个数据的时候,就要单独调用一次。provide 的 TS 类型如下:
// `provide` API 本身的类型
function provide<T>(key: InjectionKey<T> | string, value: T): void
// 入参 `key` 的其中一种类型
interface InjectionKey<T> extends Symbol {}
每次调用 provide 的时候都需要传入两个参数:
| 参数 | 说明 |
|---|---|
| key | 数据的名称 |
| value | 数据的值 |
其中 key 一般使用string 类型就可以满足大部分业务场景,如果有特殊的需要(例如开发插件时可以避免和用户的业务冲突),可以使用InjectionKey<T> 类型,这是一个继承自 Symbol 的泛型:
import type { InjectionKey } from 'vue'
const key = Symbol() as InjectionKey<string>
一个具体案例如下:
// Grandfather.vue
import { defineComponent, provide, ref } from 'vue'
export default defineComponent({
setup() {
// 声明一个响应性变量并 provide 其自身
// 孙组件获取后可以保持响应性
const msg = ref('Hello World!')
provide('msg', msg)
// 只 provide 响应式变量的值
// 孙组件获取后只会得到当前的值
provide('msgValue', msg.value)
// 声明一个方法并 provide
function printMsg() {
console.log(msg.value)
}
provide('printMsg', printMsg)
},
})
接收 inject
在 Vue 3 , inject 和 provide 一样,也是需要先导入然后在setup 里启用,也是一个全新的方法,每次要 inject 一个数据的时候,也是要单独调用一次。
另外还有一个特殊情况需要注意,当 Grandson.vue 的父级、爷级组件都 provide 了相同名字的数据下来,那么在 inject 的时候,会优先选择离它更近的组件的数据。
根据不同的场景, inject 可以接受不同数量的入参,入参类型也各不相同。
默认情况下, inject API 的 TS 类型如下:
function inject<T>(key: InjectionKey<T> | string): T | undefined
每次调用时只需要传入一个参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| key | string | 与 provide 相对应的数据名称 |
在孙组件里 inject 爷组件 provide 下来的数据:
// Grandson.vue
import { defineComponent, inject } from 'vue'
import type { Ref } from 'vue'
export default defineComponent({
setup() {
// 获取响应式变量
const msg = inject<Ref<string>>('msg')
console.log(msg!.value)
// 获取普通的字符串
const msgValue = inject<string>('msgValue')
console.log(msgValue)
// 获取函数
const printMsg = inject<() => void>('printMsg')
if (typeof printMsg === 'function') {
printMsg()
}
},
})
在每个 inject 都使用尖括号<> 添加了相应的 TS 类型,并且在调用变量的时候都进行了判断,这是因为默认的情况下, inject 除了返回指定类型的数据之外,还默认带上undefined 作为可能的值。
如果明确数据不会是undefined ,也可以在后面添加as 关键字指定其 TS 类型,这样 TypeScript 就不再因为可能出现undefined 而提示代码有问题:
// Grandson.vue
import { defineComponent, inject } from 'vue'
import type { Ref } from 'vue'
export default defineComponent({
setup() {
// 获取响应式变量
const msg = inject('msg') as Ref<string>
console.log(msg.value)
// 获取普通的字符串
const msgValue = inject('msgValue') as string
console.log(msgValue)
// 获取函数
const printMsg = inject('printMsg') as () => void
printMsg()
},
})
inject API 还支持设置默认值,可以接受更多的参数。默认情况下,只需要传入第二个参数指定默认值即可,此时它的 TS 类型如下:
function inject<T>(key: InjectionKey<T> | string, defaultValue: T): T
对于不可控的情况,建议在 inject 时添加一个兜底的默认值,防止程序报错:
// Grandson.vue
import { defineComponent, inject, ref } from 'vue'
import type { Ref } from 'vue'
export default defineComponent({
setup() {
// 获取响应式变量
const msg = inject<Ref<string>>('msg', ref('Hello'))
console.log(msg.value)
// 获取普通的字符串
const msgValue = inject<string>('msgValue', 'Hello')
console.log(msgValue)
// 获取函数
const printMsg = inject<() => void>('printMsg', () => {
console.log('Hello')
})
printMsg()
},
})
需要注意的是, inject 的什么类型的数据,其默认值也需要保持相同的类型。
inject API 在第二个 TS 类型的基础上,还有第三个 TS 类型,可以传入第三个参数:
function inject<T>(
key: InjectionKey<T> | string,
defaultValue: () => T,
treatDefaultAsFactory?: false,
): T
当第二个参数是一个工厂函数,那么可以添加第三个值,将其设置为 true ,此时默认值一定会是其 return 的值。在 Grandson.vue 里新增一个 inject ,接收一个不存在的函数名,并提供一个工厂函数作为默认值:
// Grandson.vue
import { defineComponent, inject } from 'vue'
interface Food {
name: string
count: number
}
export default defineComponent({
setup() {
// 获取工厂函数
const getFood = inject<() => Food>('nonexistentFunction', () => {
return {
name: 'Pizza',
count: 1,
}
})
console.log(typeof getFood) // function
const food = getFood()
console.log(food) // {name: 'Pizza', count: 1}
},
})
此时因为第三个参数默认为 Falsy 值 ,所以可以得到一个函数作为默认值,并可以调用该函数获得一个 Food 对象。如果将第三个参数传入为true ,再运行程序则会在const food = getFood() 这一行报错。因为此时第三个入参告知 inject ,默认值是一个工厂函数,因此默认值不再是函数本身,而是函数的返回值,所以typeof getFood 得到的不再是一个function 而是一个object :
// Grandson.vue
import { defineComponent, inject } from 'vue'
interface Food {
name: string
count: number
}
export default defineComponent({
setup() {
// 获取工厂函数
const getFood = inject<() => Food>(
'nonexistentFunction',
() => {
return {
name: 'Pizza',
count: 1,
}
},
true
)
console.log(typeof getFood) // object
// 此时下面的代码无法运行
// 报错 Uncaught (in promise) TypeError: getMsg is not a function
const food = getFood()
console.log(food)
},
})
兄弟组件通信
兄弟组件是指两个组件都挂载在同一个 Father.vue 下,但两个组件之间并没有什么直接的关联,先看看它们的关系:
Father.vue
├─Brother.vue
└─LittleBrother.vue
这种层级关系下,如果组件之间要进行通信,目前通常有这两类选择:
下面的内容将进入全局组件通信的讲解。
全局组件通信
全局组件通信是指项目下两个任意组件,不管是否有直接关联(例如父子关系、爷孙关系)都可以直接进行交流的通信方案。
举个例子,像下面这种项目结构, B2.vue 可以采用全局通信方案直接向 D2.vue 发起交流,而无需经过它们各自的父组件。
A.vue
├─B1.vue
├───C1.vue
├─────D1.vue
├─────D2.vue
├───C2.vue
├─────D3.vue
└─B2.vue
常用的方法有:
| 方案 | 发起方 | 接收方 |
|---|---|---|
| EventBus | emit | on |
| Reactive State | - | - |
| Vuex | - | - |
| Pinia | - | - |
EventBus
EventBus 通常被称之为 “全局事件总线” ,是用在全局范围内通信的一个常用方案;Vue 3 应用实例不再实现事件触发接口,因此移除了$on 、$off 和$once 这几个事件 API ,无法像 Vue 2 一样利用 Vue 实例创建 EventBus 。
以 mitt 为例,示范如何创建一个 Vue 3 的 EventBus ,首先需要安装它。
npm i mitt
然后在 src/libs 文件夹下,创建一个名为 eventBus.ts 的文件,文件内容和 Vue 2 的写法其实是一样的,只不过是把 Vue 实例换成了 mitt 实例。
// src/libs/eventBus.ts
import mitt from 'mitt'
export default mitt()
接下来就可以定义通信的相关事件了,常用的 API 和参数如下:
| 方法名称 | 作用 |
|---|---|
| on | 注册一个侦听事件,用于接收数据 |
| emit | 调用方法发起数据传递 |
| off | 用来移除侦听事件 |
on 的参数:
| 参数 | 类型 | 作用 |
|---|---|---|
| type | string | symbol |
| handler | function | 接收到数据之后要做什么处理的回调函数 |
这里的handler 建议使用具名函数,因为匿名函数无法销毁。
emit 的参数:
| 参数 | 类型 | 作用 |
|---|---|---|
| type | string | symbol |
| data | any | 与 on 对应的,允许接收的数据 |
off 的参数:
| 参数 | 类型 | 作用 |
|---|---|---|
| type | string | symbol |
| handler | function | 要被删除的,与 on 对应的 handler 函数名 |
创建和移除侦听事件
在需要暴露交流事件的组件里,通过on 配置好接收方法,同时为了避免路由切换过程中造成事件多次被绑定,从而引起多次触发,需要在适当的时机off 掉:
import { defineComponent, onBeforeUnmount } from 'vue'
import eventBus from '@libs/eventBus'
export default defineComponent({
setup() {
// 声明一个打招呼的方法
function sayHi(msg = 'Hello World!') {
console.log(msg)
}
// 启用侦听
eventBus.on('sayHi', sayHi)
// 在组件卸载之前移除侦听
onBeforeUnmount(() => {
eventBus.off('sayHi', sayHi)
})
},
})
调用侦听事件
在需要调用交流事件的组件里,通过emit 进行调用:
import { defineComponent } from 'vue'
import eventBus from '@libs/eventBus'
export default defineComponent({
setup() {
// 调用打招呼事件,传入消息内容
eventBus.emit('sayHi', 'Hello')
},
})
Reactive State
在 Vue 3 里,使用响应式的 reactive API 也可以实现一个小型的状态共享库,如果运用在一个简单的 H5 活动页面这样小需求里,完全可以满足使用。
创建状态中心
首先在 src 目录下创建一个 state 文件夹,并添加一个 index.ts 文件,写入以下代码:
// src/state/index.ts
import { reactive } from 'vue'
// 如果有多个不同业务的内部状态共享
// 使用具名导出更容易维护
export const state = reactive({
// 设置一个属性并赋予初始值
message: 'Hello World',
// 添加一个更新数据的方法
setMessage(msg: string) {
this.message = msg
},
})
设定状态更新逻辑
接下来在一个组件 Child.vue 的<script /> 里添加以下代码,分别进行了以下操作:
- 打印初始值
- 对 state 里的数据启用侦听器
- 使用 state 里的方法更新数据
- 直接更新 state 的数据
// Child.vue
import { defineComponent, watch } from 'vue'
import { state } from '@/state'
export default defineComponent({
setup() {
console.log(state.message)
// Hello World
// 因为是响应式数据,所以可以侦听数据变化
watch(
() => state.message,
(val) => {
console.log('Message 发生变化:', val)
},
)
setTimeout(() => {
state.setMessage('Hello Hello')
// Message 发生变化: Hello Hello
}, 1000)
setTimeout(() => {
state.message = 'Hi Hi'
// Message 发生变化: Hi Hi
}, 2000)
},
})
观察全局状态变化
继续在另外一个组件 Father.vue 里写入以下代码,导入 state 并在<template /> 渲染其中的数据:
<!-- Father.vue -->
<template>
<div>{{ state.message }}</div>
<Child />
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import Child from '@cp/Child.vue'
import { state } from '@/state'
export default defineComponent({
components: {
Child,
},
setup() {
return {
state,
}
},
})
</script>
可以观察到当 Child.vue 里的定时器执行时, Father.vue 的视图也会同步得到更新。
Vuex
Vuex 是 Vue 生态里面非常重要的一个成员,运用于状态管理模式。它也是一个全局的通信方案,对比 EventBus,Vuex 的功能更多,更灵活,但对应的学习成本和体积也相对较大,通常大型项目才会用上 Vuex 。
Pinia 已经成为 Vue 生态最新的官方状态管理库,不仅适用于 Vue 3 ,也支持 Vue 2 ,而 Vuex 将进入维护状态,不再增加新功能, Vue 官方强烈建议在新项目中使用 Pinia 。
Pinia
Pinia 和 Vuex 一样,也是 Vue 生态里面非常重要的一个成员,也都是运用于全局的状态管理。但面向 Componsition API 而生的 Pinia ,更受 Vue 3 喜爱,已被钦定为官方推荐的新状态管理工具。下一部分会对其进行详细介绍。
全局状态管理
Pinia的安装和启用
Pinia 目前还没有被广泛的默认集成在各种脚手架里,所以如果原来创建的项目没有 Pinia ,则需要手动安装它:
# 需要 cd 到的项目目录下
npm install pinia
查看的 package.json ,看看里面的dependencies 是否成功加入了 Pinia 和它的版本号(下方是示例代码,以实际安装的最新版本号为准),从而验证Pinia是否集成到的项目里:
{
"dependencies": {
"pinia": "^2.0.11"
}
}
然后打开src/main.ts 文件,添加下面那两行有注释的新代码:
import { createApp } from 'vue'
import { createPinia } from 'pinia' // 导入 Pinia
import App from '@/App.vue'
createApp(App)
.use(createPinia()) // 启用 Pinia
.mount('#app')
也可以通过 Create Preset 创建新项目(选择 vue 技术栈进入,选择 vue3-ts-vite 模板),可以得到一个集成常用配置的项目启动模板,该模板现在使用 Pinia 作为全局状态管理工具。
状态树结构
开始写代码之前,先来看一个对比,直观的了解 Pinia 的状态树构成,才能在后面的环节更好的理解每个功能的用途。
| 作用 | Vue Component | Vuex | Pinia |
|---|---|---|---|
| 数据管理 | data | state | state |
| 数据计算 | computed | getters | getters |
| 行为方法 | methods | mutations / actions | actions |
可以看到 Pinia 的结构和用途都和 Vuex 与 Component 非常相似,并且 Pinia 相对于 Vuex ,在行为方法部分去掉了 mutations (同步操作)和 actions (异步操作)的区分,更接近组件的结构,入门成本会更低一些。
下面来创建一个简单的 Store ,开始用 Pinia 来进行状态管理。
创建 Store
Pinia 的核心也是称之为 Store 。参照 Pinia 官网推荐的项目管理方案,也是先在src 文件夹下创建一个stores 文件夹,并在里面添加一个index.ts 文件,然后就可以来添加一个最基础的 Store 。Store 是通过defineStore 方法来创建的,它有两种入参形式:
形式1:接收两个参数
接收两个参数,第一个参数是 Store 的唯一 ID ,第二个参数是 Store 的选项:
// src/stores/index.ts
import { defineStore } from 'pinia'
export const useStore = defineStore('main', {
// Store 选项...
})
形式2:接收一个参数
接收一个参数,直接传入 Store 的选项,但是需要把唯一 ID 作为选项的一部分一起传入:
// src/stores/index.ts
import { defineStore } from 'pinia'
export const useStore = defineStore({
id: 'main',
// Store 选项...
})
不论是哪种创建形式,都必须为 Store 指定一个唯一 ID 。另外可以看到这里把导出的函数名命名为useStore ,以use 开头是 Vue 3 对可组合函数的一个命名约定。并且使用的是export const 而不是export default,这样在使用的时候可以和其他的 Vue 组合函数保持一致,都是通过import { xxx } from 'xxx' 来导入。
如果有多个 Store ,可以分模块管理,并根据实际的功能用途进行命名( e.g.useMessageStore 、useUserStore 、useGameStore … )。
管理 state
给 Store 添加 state
通过一个箭头函数的形式来返回数据,并且能够正确的帮推导 TypeScript 类型:
// src/stores/index.ts
import { defineStore } from 'pinia'
export const useStore = defineStore('main', {
// 先定义一个最基本的 message 数据
state: () => ({
message: 'Hello World',
}),
// ...
})
需要注意一点的是,如果不显式 return ,箭头函数的返回值需要用圆括号 () 套起来,这个是箭头函数的要求。所以相当于这样写:
// ...
export const useStore = defineStore('main', {
state: () => {
return {
message: 'Hello World',
}
},
// ...
})
手动指定数据类型
虽然 Pinia 会帮推导 TypeScript 的数据类型,但有时候可能不太够用,比如下面这段代码,请留意代码注释的说明:
// ...
export const useStore = defineStore('main', {
state: () => {
return {
message: 'Hello World',
// 添加了一个随机消息数组
randomMessages: [],
}
},
// ...
})
randomMessages 的预期应该是一个字符串数组string[] ,但是这个时候 Pinia 会帮推导成never[] ,那么类型就对不上了。这种情况下就需要手动指定 randomMessages 的类型,可以通过as 来指定:
// ...
export const useStore = defineStore('main', {
state: () => {
return {
message: 'Hello World',
// 通过 as 关键字指定 TS 类型
randomMessages: [] as string[],
}
},
// ...
})
或者使用尖括号<> 来指定,这两种方式是等价的:
// ...
export const useStore = defineStore('main', {
state: () => {
return {
message: 'Hello World',
// 通过尖括号指定 TS 类型
randomMessages: <string[]>[],
}
},
// ...
})
获取和更新 state
获取 state 有多种方法,略微有区别,但相同的是,他们都是响应性的。
使用 store 实例
对于Pinia,数据直接是挂在store 上的,而不是store.state 上的。所以,可以直接通过store.message 直接调用 state 里的数据:
import { defineComponent } from 'vue'
import { useStore } from '@/stores'
export default defineComponent({
setup() {
// 像 useRouter 那样定义一个变量拿到实例
const store = useStore()
// 直接通过实例来获取数据
console.log(store.message)
// 这种方式需要把整个 store 给到 template 去渲染数据
return {
store,
}
},
})
但一些比较复杂的数据这样写会很长,所以有时候更推荐用下面介绍的 computed API 和 storeToRefs API 等方式来获取。
使用 computed API
现在 state 里已经有定义好的数据了,下面这段代码是在 Vue 组件里导入的 Store ,并通过计算数据computed 拿到里面的message 数据传给 template 使用:
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { useStore } from '@/stores'
export default defineComponent({
setup() {
// 像 useRouter 那样定义一个变量拿到实例
const store = useStore()
// 通过计算拿到里面的数据
const message = computed(() => store.message)
console.log('message', message.value)
// 传给 template 使用
return {
message,
}
},
})
</script>
如果要更新数据:
- 可以通过提前定义好的 Store Actions 方法进行更新。
- 在定义 computed 变量的时候,配置好setter的行为:
// 其他代码和上一个例子一样,这里省略... // 修改:定义 computed 变量的时候配置 getter 和 setter const message = computed({ // getter 还是返回数据的值 get: () => store.message, // 配置 setter 来定义赋值后的行为 set(newVal) { store.message = newVal }, }) // 此时不再抛出 Write operation failed: computed value is readonly 的警告 message.value = 'New Message.' // store 上的数据已成功变成了 New Message. console.log(store.message)
使用 storeToRefs API
这是一个专门为 Pinia Stores 设计的 API ,类似于 toRefs ,区别在于,它会忽略掉 Store 上面的方法和非响应性的数据,只返回 state 上的响应性数据:
import { defineComponent } from 'vue'
import { useStore } from '@/stores'
// 记得导入这个 API
import { storeToRefs } from 'pinia'
export default defineComponent({
setup() {
const store = useStore()
// 通过 storeToRefs 来拿到响应性的 message
const { message } = storeToRefs(store)
console.log('message', message.value)
return {
message,
}
},
})
通过这个方式拿到的 message 变量是一个 Ref 类型的数据,所以可以像普通的 ref 变量一样进行读取和赋值。
// 直接赋值即可
message.value = 'New Message.'
// store 上的数据已成功变成了 New Message.
console.log(store.message)
使用 toRefs API
如 使用storeToRefs API 部分所说,该 API 本身的设计就是类似于 toRefs ,所以也可以直接用 toRefs 把 state 上的数据转成 ref 变量:
// 注意 toRefs 是 vue 的 API ,不是 Pinia
import { defineComponent, toRefs } from 'vue'
import { useStore } from '@/stores'
export default defineComponent({
setup() {
const store = useStore()
// 跟 storeToRefs 操作都一样,只不过用 Vue 的这个 API 来处理
const { message } = toRefs(store)
console.log('message', message.value)
return {
message,
}
},
})
像上面这样,对 store 执行 toRefs 会把 store 上面的 getters 、 actions 也一起提取,如果只需要提取 state 上的数据,可以这样做:
// 只传入 store.$state
const { message } = toRefs(store.$state)
使用 toRef API
toRef 是 toRefs 的兄弟 API ,一个是只转换一个字段,一个是转换所有字段,所以它也可以用来转换 state 数据变成 ref 变量:
// 注意 toRef 是 vue 的 API ,不是 Pinia
import { defineComponent, toRef } from 'vue'
import { useStore } from '@/stores'
export default defineComponent({
setup() {
const store = useStore()
// 遵循 toRef 的用法即可
const message = toRef(store, 'message')
console.log('message', message.value)
return {
message,
}
},
})
使用 actions 方法
Pinia 所有操作都集合为 action ,无需区分同步和异步,按照平时的函数定义即可更新 state ,具体操作详见管理 actions一节。
批量更新 state
在获取和更新state 部分说的都是如何修改单个 state 数据,那么有时候要同时修改很多个,会显得比较繁琐。
Pinia 也提供了一个$patch API 用于同时修改多个数据,它接收一个参数:
| 参数 | 类型 | 语法 |
|---|---|---|
| partialState | 对象 / 函数 | store.$patch(partialState) |
传入一个对象
当参数类型为对象时,key 是要修改的 state 数据名称,value 是新的值(支持嵌套传值),用法如下:
// 继续用前面的数据,这里会打印出修改前的值
console.log(JSON.stringify(store.$state))
// 输出 {"message":"Hello World","randomMessages":[]}
/**
* 注意这里,传入了一个对象
*/
store.$patch({
message: 'New Message',
randomMessages: ['msg1', 'msg2', 'msg3'],
})
// 这里会打印出修改后的值
console.log(JSON.stringify(store.$state))
// 输出 {"message":"New Message","randomMessages":["msg1","msg2","msg3"]}
对于简单的数据,直接修改成新值是非常好用的。
但有时候并不单单只是修改,而是要对数据进行拼接、补充、合并等操作,相对而言开销就会很大,这种情况下,更适合传入一个函数 来处理。
传入一个函数
当参数类型为函数时,该函数会有一个入参state ,是当前实例的 state ,等价于store.$state ,用法如下:
// 这里会打印出修改前的值
console.log(JSON.stringify(store.$state))
// 输出 {"message":"Hello World","randomMessages":[]}
/**
* 注意这里,这次是传入了一个函数
*/
store.$patch((state) => {
state.message = 'New Message'
// 数组改成用追加的方式,而不是重新赋值
for (let i = 0; i < 3; i++) {
state.randomMessages.push(`msg${i + 1}`)
}
})
// 这里会打印出修改后的值
console.log(JSON.stringify(store.$state))
// 输出 {"message":"New Message","randomMessages":["msg1","msg2","msg3"]}
全量更新 state
虽然可以对所有数据都执行一次 “补丁更新” 来达到 “全量更新” 的目的,但 Pinia 也提供了一个更好的办法。从前面多次提到 state 数据可以通过store.$state 来拿到,而这个属性本身是可以直接赋值的。
state 上现在有message 和randomMessages 这两个数据,那么要全量更新为新的值,就这么操作:
store.$state = {
message: 'New Message',
randomMessages: ['msg1', 'msg2', 'msg3'],
}
注意必须遵循 state 原有的数据和对应的类型。
重置 state
Pinia 提供了一个$reset API 挂在每个实例上面,用于重置整棵 state 树为初始数据:
// 这个 store 是上面定义好的实例
store.$reset()
具体例子:
// 修改数据
store.message = 'New Message'
console.log(store.message) // 输出 New Message
// 3s 后重置状态
setTimeout(() => {
store.$reset()
console.log(store.message) // 输出最开始的 Hello World
}, 3000)
订阅 state
Pinia 提供了一个用于订阅 state 的$subscribe API 。
订阅 API 的 TS 类型
在了解这个 API 的使用之前,先看一下它的 TS 类型定义:
// $subscribe 部分的 TS 类型
// ...
$subscribe(
callback: SubscriptionCallback<S>,
options?: { detached?: boolean } & WatchOptions
): () => void
// ...
添加订阅
$subscribe API 的功能类似于 watch ,但它只会在 state 被更新的时候才触发一次,并且在组件被卸载时删除(参考:组件的生命周期)。
从订阅API的TS类型 可以看到,它可以接受两个参数,第一个参数是必传的 callback 函数,一般情况下默认用这个方式即可,使用例子:
// 可以在 state 出现变化时,更新本地持久化存储的数据
store.$subscribe((mutation, state) => {
localStorage.setItem('store', JSON.stringify(state))
})
这个 callback 里面有 2 个入参:
| 入参 | 作用 |
|---|---|
| mutation | 本次事件的一些信息 |
| state | 当前实例的 state |
其中 mutation 包含了以下数据:
| 字段 | 值 |
|---|---|
| storeId | 发布本次订阅通知的 Pinia 实例的唯一 ID(由创建 Store 时指定) |
| type | 有 3 个值:返回direct 代表直接更改数据;返回patch object 代表是通过传入一个对象 更改;返回patch function 则代表是通过传入一个函数 更改 |
| events | 触发本次订阅通知的事件列表 |
| payload | 通过传入一个函数 更改时,传递进来的荷载信息,只有type 为patch object 时才有 |
如果不希望组件被卸载时删除订阅,可以传递第二个参数 options 用以保留订阅状态,传入一个对象。
可以简单指定为{ detached: true },也可以搭配 watch API 的选项一起用:
store.$subscribe(
(mutation, state) => {
// ...
},
{ detached: true },
)
移除订阅
在添加订阅 部分已了解过,默认情况下,组件被卸载时订阅也会被一并移除,但如果之前启用了 detached 选项,就需要手动取消了。前面在订阅API的TS类型 里提到,在启用 $subscribe API 之后,会有一个函数作为返回值,这个函数可以用来取消该订阅。
// 定义一个退订变量,它是一个函数
const unsubscribe = store.$subscribe(
(mutation, state) => {
// ...
},
{ detached: true },
)
// 在合适的时期调用它,可以取消这个订阅
unsubscribe()
管理 getters
Pinia 的getters 是用来计算数据的。
给 Store 添加 getter
添加普通的 getter
继续用刚才的message ,来定义一个 Getter ,用于返回一句拼接好的句子:
// src/stores/index.ts
import { defineStore } from 'pinia'
export const useStore = defineStore('main', {
state: () => ({
message: 'Hello World',
}),
// 定义一个 fullMessage 的计算数据
getters: {
fullMessage: (state) => `The message is "${state.message}".`,
},
// ...
})
添加引用 getter 的 getter
有时候可能要引用另外一个 getter 的值来返回数据,这个时候不能用箭头函数了,需要定义成普通函数而不是箭头函数,并在函数内部通过this 来调用当前 Store 上的数据和方法。继续在上面的例子里,添加多一个emojiMessage 的 getter ,在返回fullMessage 的结果的同时,拼接多一串 emoji :
export const useStore = defineStore('main', {
state: () => ({
message: 'Hello World',
}),
getters: {
fullMessage: (state) => `The message is "${state.message}".`,
// 这个 getter 返回了另外一个 getter 的结果
emojiMessage(): string {
return `🎉🎉🎉 ${this.fullMessage}`
},
},
})
如果只写 JavaScript ,可能对这一条所说的限制觉得很奇怪,事实上用 JS 写箭头函数来引用确实不会报错,但如果用的是 TypeScript ,不按照这个写法,在 VSCode 提示和执行 TSC 检查的时候都会给抛出一条错误。
另外关于普通函数的 TS 返回类型,官方建议显式的进行标注,就像这个例子里的emojiMessage(): string 里的: string 。
给 getter 传递参数
getter 本身是不支持参数的,但和 Vuex 一样,支持返回一个具备入参的函数,用来满足需求。
import { defineStore } from 'pinia'
export const useStore = defineStore('main', {
state: () => ({
message: 'Hello World',
}),
getters: {
// 定义一个接收入参的函数作为返回值
signedMessage: (state) => {
return (name: string) => `${name} say: "The message is ${state.message}".`
},
},
})
调用的时候是这样:
const signedMessage = store.signedMessage('Petter')
console.log('signedMessage', signedMessage)
// Petter say: "The message is Hello World".
这种情况下,这个 getter 只是调用的函数的作用,不再有缓存,如果通过变量定义了这个数据,那么这个变量也只是普通变量,不具备响应性。
// 通过变量定义一个值
const signedMessage = store.signedMessage('Petter')
console.log('signedMessage', signedMessage)
// Petter say: "The message is Hello World".
// 2s 后改变 message
setTimeout(() => {
store.message = 'New Message'
// signedMessage 不会变
console.log('signedMessage', signedMessage)
// Petter say: "The message is Hello World".
// 必须这样再次执行才能拿到更新后的值
console.log('signedMessage', store.signedMessage('Petter'))
// Petter say: "The message is New Message".
}, 2000)
获取和更新 getter
getter 和 state 都属于数据管理,读取和赋值的方法是一样的,请参考上方获取和更新 state 一节的内容。
管理 actions
Pinia 只需要用actions 就可以解决各种数据操作,无需像 Vuex 一样区分为mutations / actions 两大类。
给 Store 添加 action
可以为当前 Store 封装一些可以开箱即用的方法,支持同步和异步。
// src/stores/index.ts
import { defineStore } from 'pinia'
export const useStore = defineStore('main', {
state: () => ({
message: 'Hello World',
}),
actions: {
// 异步更新 message
async updateMessage(newMessage: string): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
// 这里的 this 是当前的 Store 实例
this.message = newMessage
resolve('Async done.')
}, 3000)
})
},
// 同步更新 message
updateMessageSync(newMessage: string): string {
// 这里的 this 是当前的 Store 实例
this.message = newMessage
return 'Sync done.'
},
},
})
在 action 里,如果要访问当前实例的 state 或者 getter ,只需要通过this 即可操作,方法的入参完全不再受 Vuex 那样有固定形式的困扰。
调用 action
在 Pinia 中只要像普通的函数一样使用即可,不需要和 Vuex 一样调用 commit 或者 dispatch。
export default defineComponent({
setup() {
const store = useStore()
const { message } = storeToRefs(store)
// 立即执行
console.log(store.updateMessageSync('New message by sync.'))
// 3s 后执行
store.updateMessage('New message by async.').then((res) => console.log(res))
return {
message,
}
},
})
添加多个 Store
到这里,对单个 Store 的配置和调用相信都已经清楚了,实际项目中会涉及到很多数据操作,还可以用多个 Store 来维护不同需求模块的数据状态。
这一点和 Vuex 的 Module 比较相似,目的都是为了避免状态树过于臃肿,但用起来会更为简单。
目录结构建议
建议统一存放在src/stores 下面管理,根据业务需要进行命名,比如user 就用来管理登录用户相关的状态数据。
src
└─stores
│ # 入口文件
├─index.ts
│ # 多个 store
├─user.ts
├─game.ts
└─news.ts
里面暴露的方法就统一以use 开头加上文件名,并以Store 结尾,作为小驼峰写法,比如user 这个 Store 文件里面导出的函数名就是:
// src/stores/user.ts
export const useUserStore = defineStore('user', {
// ...
})
然后以index.ts 里作为统一的入口文件,index.ts 里的代码写为:
export * from './user'
export * from './game'
export * from './news'
这样在使用的时候,只需要从@/stores 里导入即可,无需写完整的路径,例如只需要这样:import { useUserStore } from '@/stores',而无需这样:import { useUserStore } from '@/stores/user'。
在 Vue 组件 / TS 文件里使用
这里以一个比较简单的业务场景举例,希望能够方便的理解如何同时使用多个 Store 。
假设目前有一个userStore 是管理当前登录用户信息,gameStore 是管理游戏的信息,而 “个人中心” 这个页面需要展示 “用户信息” ,以及 “该用户绑定的游戏信息”,那么就可以这样:
import { defineComponent, onMounted, ref } from 'vue'
import { storeToRefs } from 'pinia'
// 这里导入要用到的 Store
import { useUserStore, useGameStore } from '@/stores'
import type { GameItem } from '@/types'
export default defineComponent({
setup() {
// 先从 userStore 获取用户信息(已经登录过,所以可以直接拿到)
const userStore = useUserStore()
const { userId, userName } = storeToRefs(userStore)
// 使用 gameStore 里的方法,传入用户 ID 去查询用户的游戏列表
const gameStore = useGameStore()
const gameList = ref<GameItem[]>([])
onMounted(async () => {
gameList.value = await gameStore.queryGameList(userId.value)
})
return {
userId,
userName,
gameList,
}
},
})
切记每个 Store 的 ID 必须不同,如果 ID 重复,在同一个 Vue 组件 / TS 文件里定义 Store 实例变量的时候,会以先定义的为有效值,后续定义的会和前面一样。
Store 之间互相引用
如果在定义一个 Store 的时候,要引用另外一个 Store 的数据,也是很简单,回到那个 message 的例子,添加一个 getter ,它会返回一句问候语欢迎用户:
// src/stores/message.ts
import { defineStore } from 'pinia'
// 导入用户信息的 Store 并启用它
import { useUserStore } from './user'
const userStore = useUserStore()
export const useMessageStore = defineStore('message', {
state: () => ({
message: 'Hello World',
}),
getters: {
// 这里就可以直接引用 userStore 上面的数据了
greeting: () => `Welcome, ${userStore.userName}!`,
},
})
假设现在userName 是 Petter ,那么会得到一句对 Petter 的问候:
const messageStore = useMessageStore()
console.log(messageStore.greeting) // Welcome, Petter!
专属插件的使用
Pinia 拥有非常灵活的可扩展性,有专属插件可以开箱即用满足更多的需求场景。
如何查找插件
插件有统一的命名格式pinia-plugin-* ,所以可以在 npmjs 上搜索这个关键词来查询目前有哪些插件已发布。
点击查询:pinia-plugin - npmjs
如何使用插件
这里以 pinia-plugin-persistedstate 为例,这是一个让数据持久化存储的 Pinia 插件。插件也是独立的 npm 包,需要先安装,再激活,然后才能使用。
激活方法会涉及到 Pinia 的初始化过程调整,这里不局限于某一个插件,通用的插件用法如下(请留意代码注释):
// src/main.ts
import { createApp } from 'vue'
import App from '@/App.vue'
import { createPinia } from 'pinia' // 导入 Pinia
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' // 导入 Pinia 插件
const pinia = createPinia() // 初始化 Pinia
pinia.use(piniaPluginPersistedstate) // 激活 Pinia 插件
createApp(App)
.use(pinia) // 启用 Pinia ,这一次是包含了插件的 Pinia 实例
.mount('#app')
使用前
Pinia 默认在页面刷新时会丢失当前变更的数据,没有在本地做持久化记录:
// 其他代码省略
const store = useMessageStore()
// 假设初始值是 Hello World
setTimeout(() => {
// 2s 后变成 Hello World!
store.message = store.message + '!'
}, 2000)
// 页面刷新后又变回了 Hello World
使用后
按照 persistedstate 插件的文档说明,在其中一个 Store 启用它,只需要添加一个persist: true 的选项即可开启:
// src/stores/message.ts
import { defineStore } from 'pinia'
import { useUserStore } from './user'
const userStore = useUserStore()
export const useMessageStore = defineStore('message', {
state: () => ({
message: 'Hello World',
}),
getters: {
greeting: () => `Welcome, ${userStore.userName}`,
},
// 这是按照插件的文档,在实例上启用了该插件,这个选项是插件特有的
persist: true,
})
回到的页面,现在这个 Store 具备了持久化记忆的功能了,它会从 localStorage 读取原来的数据作为初始值,每一次变化后也会将其写入 localStorage 进行记忆存储。
// 其他代码省略
const store = useMessageStore()
// 假设初始值是 Hello World
setTimeout(() => {
// 2s 后变成 Hello World!
store.message = store.message + '!'
}, 2000)
// 页面刷新后变成了 Hello World!!
// 再次刷新后变成了 Hello World!!!
// 再次刷新后变成了 Hello World!!!!
可以在浏览器查看到 localStorage 的存储变化,以 Chrome 浏览器为例,按 F12 ,打开 Application 面板,选择 Local Storage ,可以看到以当前 Store ID 为 Key 的存储数据。这是其中一个插件使用的例子,更多的用法请根据自己选择的插件的 README 说明操作。