Vue3 Pinia使用
在学习 Vue2 的宝子们一定都知道,在 vue2 版本中,如果想要使用状态管理器,那么一定是集成 Vuex,首先说明一点,Vuex 在 vue3 项目中依旧是可以正常使用的,是 vue 项目的正规军。但是,今天我们学习一下小菠萝,Pinia 目前也已经是 vue 官方正式的状态库。适用于 vue2 和 vue3。可以简单的理解成 Pinia 就是 Vuex5。也就是说, Vue3 项目,建议使用Pinia,当然很多公司或者是项目由 vue2 转为 vue3 之后,由于习惯了使用 vuex ,所以说,在 vue3 当中继续使用 vuex 的,也不是少数,都知道就可以,根据实际情况来选择。
什么是 Pinia
Pinia 是 Vue 的存储库,它允许您跨组件/页面共享状态。Pinia 的成功可以归功于他管理存储数据的独特功能,例如:可扩展性、存储模块组织、状态变化分组、多存储创建等。
Pinia 的优点
Pinia 被 vue 纳入正规编制,肯定是有原因的,那 pinia 有啥优点呢,主要是一下几点:
- pinia 符合直觉,易于学习。
- pinia 是轻量级状态管理工具,大小只有1KB.
- pinia 模块化设计,方便拆分。
- pinia 没有 mutations,直接在 actions 中操作 state,通过 this.xxx 访问响应的状态,尽管可以直接操作 state,但是还是推荐在 actions 中操作,保证状态不被意外的改变。
- store 的 action 被调度为常规的函数调用,而不是使用 dispatch 方法或者是 MapAction 辅助函数,这是在 Vuex 中很常见的。
- 支持多个 store。
- 支持 Vue devtools、SSR、webpack 代码拆分。
相关资料
Pinia 中文网:https://pinia.web3doc.top/
Pinia 安装
安装 pinia 就很简单了,直接命令安装就可以了。
npm install pinia -save
或者
yarn add pinia
Pinia 使用
安装完 pinia ,然后就是使用了,使用的第一步,就是在项目中引入 pinia。
Pinia 导入
首先在 main.js 文件中引入,很简单,不要慌宝子们。
import {createPinia} from 'pinia'
然后,这个 pinia 就在项目中导入了,但是上面是 vue3 的写法哈,我起的这个项目是 vue3 的。
Pinia 是支持 vue2 的,如果是 vue2 的项目,导入的方式是下面的样子:
import {PiniaVuePlugin} from 'pinia'
好的,我们还是以 vue3 来介绍这个 Pinia。
导入的时候是 hook ,我们需要调用一下
const state = createPinia()
调用完成,state 是以插件的形式存在的,所以说最后我们需要在项目使用一下。
好的,编写完上边这一大堆,我们就实现了在 vue3 项目中导入 pinia 的全部操作。
接下来就可以具体的使用 pinia 了。
Pinia 基本使用
创建 index.ts 文件
使用起来相对简单一些,我们首先在根目录下创建一个 store 文件夹,这个都晓得哈,当年用 vuex 的时候也是这个结构。毕竟 pinia 就是用来替换掉 vuex 的嘛。
创建完 store 文件夹,在里面创建一个 ts 文件,叫做 index.ts 。
编写 index.ts 文件
然后我们开始编写 index.ts 文件。
首先我们先引入 pinia。
import { defineStore } from "pinia";
由于 defineStore 也是一个 hooks ,所以说我们可以直接导出一下。
export const Test = defineStore()
这样子写是会报错的,因为这个 defineStore 是需要传参数的,其中第一个参数是id,就是一个唯一的值,简单点说就可以理解成是一个命名空间,我们可以写一个枚举再传值。
我们在同级在创建一个名字叫做 store_name.ts 的文件写一个枚举数据导出。
export const enum Names {
TEST = "TEST"
}
然后在 index.ts 文件中引入一下枚举数据,然后传给这个 defineStore。
import { defineStore } from "pinia";
import { Names } from "./store_name";
export const useInfoStore = defineStore(Names.TEST)
ok, 这个样子还是报错的,因为还有其他的参数需要传递,第二个参数就是一个对象,里面有三个模块需要处理,第一个是 state,第二个是 getters,第三个是 actions。
- state 和之前我们 vuex 里面的写法是不一样的,在 vuex 里面呢,state 是一个对象,但是在 pinia 中,state 必须是一个箭头函数,然后在返回一个对象。
- getters 模块呢,类似于计算属性,是有缓存的,主要是帮助我们修饰一些值。
- actions 呢,类似于 methods,帮助我们做一些同步的或者是异步的操作,提交 state 之类的。
到此完整的 index.ts 文件代码:
import { defineStore } from "pinia";
import { Names } from "./store_name";
export const useInfoStore = defineStore(Names.TEST, {
state: () => {
return {
name: '我是𝒆𝒅.',
age: 10,
}
},
getters: {
},
actions: {
}
})
好的,其实到这个地方为止呢,其实我们已经可以使用 pinia 了,我们写一个页面使用一下 pinia 的值。
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
</script>
<style scoped>
</style>
保存代码,刷新一下页面,我们可以看到,数据已经渲染在界面上了。
如果我们有 devtools 工具的话,我们可以看见我们的数据是可以查询到的。
未完待续~
继续
修改 Pinia 的值
修改 Pinia 的值有很多中方式,我们一个一个来。
修改 pinia 的值,其实就是修改 state 的值,上边的案例我们写了一个 state,里面有两个值,一个是 name, 一个是 age,今天我们的任务就是实现修改 pinia 中 state 的值。
修改值的方式呢,常见的有五种,我们一个一个看。
方式一:直接修改
这种方式呢,非常的简单粗暴,直接修改就可以了。
比如,上边我们页面上有一个按钮,点击按钮的时候实现 age 加一操作。
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
<el-button @click="changeAge">年龄+1</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
const changeAge = () => {
userInfo.age++
}
</script>
<style scoped>
</style>
保存刷新页面,我们看一下效果。
OK,成功实现了点击按钮的时候,age 自动 + 1 操作。
注意哈: 在 vuex 里面是坚决不允许这样子直接操作 state 数据的,但是小菠萝是可以允许滴!
方式二:$patch 函数修改
在我们实例化 const userInfo = useInfoStore()
这个 state 的时候,其实这个 userInfo
中,有一个方法,就是 patch 函数,它可以帮助我们批量修改。比如点击按钮,同时修改 name 和 age 的值,直接上代码:
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
<el-button @click="change">修改name和age</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
// 方式二: 通过 patch 函数批量修改 name 和 age
const change = () => {
userInfo.$patch({
age: 11,
name: '𝒆𝒅.'
})
}
</script>
<style scoped>
</style>
保存刷新页面,开始测试效果。
好的,通过使用 patch 函数实现批量修改 state 的数据。
方式三:$patch 函数修改
哎呦喂,为什么和第二种一样,没错滴宝子们,和第二种一样都是使用 patch 函数来实现修改,但是有区别,方式二实在 patch 函数中传入修改的对象值,但是这种方式传入的是一个函数,作用是啥子呢?就是可以进行逻辑操作,比如说判断之类的。
上代码:
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
<el-button @click="change">修改name和age</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
// 方式三
const change = () => {
userInfo.$patch((state) => { // 这里传入的state就是pinia的state
state.age = 11
state.name = '𝒆𝒅.'
})
}
</script>
<style scoped>
</style>
保存看一下效果,其实和方式二是一模一样的。
好的,这就是通过 patch 传入函数的方式修改 state 内容。
方式四:$state 方式
首先说一点儿哈,第四种方式不是很常用。为啥呢?尽管它也可以修改 state 的值,但是这种方式有一个很大的弊端,什么弊端呢,与其说是修改 state 的值,倒不如说直接替换掉 state,什么意思呢?简单点说,比如上边的 state 里面有两个值,一个是 name,一个是 age,如果我们只想修改 age 的值,那么我们必须把 age 和 name 的值都写上才可以,如果只写 age ,那么 name 的值就没有了,理解吧?直接看代码。
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
<el-button @click="change">修改name和age</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
// 方式四:state
const change = () => {
userInfo.$state = {
name: '𝒆𝒅.',
age: 11
}
}
</script>
<style scoped>
</style>
保存刷新页面,点击按钮查看效果,其实和上面的案例是一样的,可以修改 name 和 age。
我们可以看到,把数据全部传进去,数据改了。
方式五: action 方式
这个方式我们需要借助 actions 来实现,所以说我们需要去 store 文件夹下的 index.ts 文件中写一个 action。
import { defineStore } from "pinia";
import { Names } from "./store_name";
export const useInfoStore = defineStore(Names.TEST, {
state: () => {
return {
name: '我是𝒆𝒅.',
age: 10,
}
},
getters: {
},
actions: {
setAge () { // 注意,这里就不要写箭头函数了,不然 this 指向会出问题。
this.age = 11
}
}
})
写完 action 我们就可以使用 action 的方式修改 age 的值了。怎么使用呢,我们只需要使用实例去调用一下我们刚才写的 action 函数就行了。
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
<el-button @click="change">修改 age</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
// 方式五
const change = () => {
userInfo.setAge()
}
</script>
<style scoped>
</style>
保存看一下效果。
好的,这个就是通过 action 方式修改。
当然了,这个 action 是可以传参的,我们修改一下 index.ts 文件编写的 action 函数,让他接受一个参数再赋值。
actions: {
setAge(num: number) { // 注意,这里就不要写箭头函数了,不然 this 指向会出问题。
this.age = num
}
}
然后我们页面代码直接传进去参数就可以了。
// 方式五
const change = () => {
userInfo.setAge(12)
}
修改完保存刷新,看一下效果。
好了,上边就是常见的五种修改 state 数据的方式,可以根据自己的实际情况选择使用。
pinia 解构
本来不想写了,但是还是稍微说一下吧。
上面的案例,我们实例化const userInfo = useInfoStore()
了以后呢,这个 userInfo
是可以继续解构操作的,这个也不用细说,会ES的都知道解构操作。
const {name, age} = userInfo
解构完成,我们看一下效果。
全部代码:
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>解构前</p>
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
<p>解构后</p>
<p>pinia 中的信息: {{name}} --- {{age}}</p>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
const { name, age } = userInfo
</script>
<style scoped>
</style>
在页面分别展示一下解构前和解构后的数据,保存刷新看效果。
我们可以看到哈,就是结构前和结构后我们在页面都可以获取到数据展示。
但是有个问题哈,就是解构后的数据,是不具备响应式的,啥意思呢,就是我们修改了 state 的值,页面不会跟着变化,我们测试一下。
编写代码,点击按钮,age 加一,看一下结构前和解构后的数据在页面是否会实时渲染。
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>解构前</p>
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
<p>解构后</p>
<p>pinia 中的信息: {{name}} --- {{age}}</p>
<el-button @click="change">Age +1</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
const { name, age } = userInfo
let change = () => {
userInfo.age++
}
</script>
<style scoped>
</style>
保存刷新,点击按钮查看效果。
通过测试我们可以看到,结构前的是可以实时渲染的,但是解构后的话是不可以的, 因为解构后的不是响应式数据。
怎么解决这个问题呢,其实也很简单哈,官方提供了了一个方法,可以把解构后的数据转换为响应式的数据。
就是 storeToRefs,使用 storeToRefs 需要导入一下。
import { storeToRefs } from 'pinia'
然后把我们解构的对象包裹一下就可以了,这个方法和 toRefs 是类型的,应该理解哈。
const { name, age } = storeToRefs(userInfo)
然后我们修改案例的代码,再看一下效果。
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>解构前</p>
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
<p>解构后</p>
<p>pinia 中的信息: {{name}} --- {{age}}</p>
<el-button @click="change">Age +1</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 导入storeToRefs
import { storeToRefs } from 'pinia'
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
// 包裹一下结构对象
const { name, age } = storeToRefs(userInfo)
let change = () => {
userInfo.age++
}
</script>
<style scoped>
</style>
保存刷新一下。
我们可以看到,解构后的数据也已经变成响应式的了。
或者我们换一个写法,直接操作结构后的数据,记得,要 .value
。
let change = () => {
age.value++
}
效果是一样的。
Pinia 的 actions
actions 是可以处理同步,也可以处理异步,同步的话相对来说简单一点,上面我们通过 action 修改 state 的时候,就用到了 actions 的同步,这里就不再赘述了。
actions 异步
首先我们模拟一个异步函数,比如说登录。
type User = {
name: String,
age: Number
}
const Login = (): Promise<User> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
name: 'wjw',
age: 22
})
}, 2000)
})
}
然后我们在 actions 中就可以调用这个异步的操作了,在 actions 处理异步的时候呢,我们一般是与 async
和 await
连用。
所有 index.ts 文件代码:
import { defineStore } from "pinia";
import { Names } from "./store_name";
type User = {
name: String,
age: Number
}
const Login = (): Promise<User> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
name: 'wjw',
age: 22
})
}, 2000)
})
}
export const useInfoStore = defineStore(Names.TEST, {
state: () => {
return {
name: '我是𝒆𝒅.',
age: 10,
}
},
getters: {
},
actions: {
async setUser() {
const result = await Login()
this.name = result.name
this.age = result.age
}
}
})
然后我们修改一下使用的页面也修改一下。
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
<el-button @click="change">修改用户内容</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
let change = () => {
userInfo.setUser()
}
</script>
<style scoped>
</style>
保存刷新,点击按钮,等两秒钟假装请求,然后看数据变不变。
奈斯! nice! 完成异步案例!
actions 同步、异步连用
这个 actions 里面的方法函数是可以相互调用的,啥意思呢,就是你 actions 里面有好几个方法,这几个方法是可以调过来调过去的,但是注意哈,别玩嗨了,直接死循环了。看个案例:
上面的代码一改造,本来异步模拟获取的 age 数据是 22 ,然后我们调用一个 action 把 age 改成 80,这个是可以的哈。
import { defineStore } from "pinia";
import { Names } from "./store_name";
type User = {
name: String,
age: Number
}
const Login = (): Promise<User> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
name: 'wjw',
age: 22
})
}, 2000)
})
}
export const useInfoStore = defineStore(Names.TEST, {
state: () => {
return {
name: '我是𝒆𝒅.',
age: 10,
}
},
getters: {
},
actions: {
async setUser() {
const result = await Login()
this.name = result.name
this.age = result.age
this.setAge(80)
},
setAge(num: number) { // 注意,这里就不要写箭头函数了,不然 this 指向会出问题。
this.age = num
},
}
})
我们保存代码,刷新点击按钮等两秒钟然后看一下效果。
看到效果,改了吧! 啊哈哈哈哈,666!
getter 函数
接下来我们稍微过一下 getter 函数。
getters 类似于 vue 里面的计算属性,可以对已有的数据进行修饰。有两种写法,我们一个一个看哈。
普通函数方式写法
这个就比较简单了,直接上代码:
getters: {
newName() {
return `这是getter修饰过的名称 ${this.name}`
}
},
这个代码啥意思很简单是吧,就是在之前 name 的基础上拼接上了一个字符串,但是这样 ts 可能不会正确的进行数据类型转换,所以说我们可以加一个类型定义。
getters: {
newName():string {
return `这是getter修饰过的名称 ${this.name}`
}
},
告诉他,就是字符串类型。
然后这个 getter 就可以直接在模板上使用,直接改一下之前的让他在页面上展示出来。
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
<el-button @click="change">修改用户内容</el-button>
<p>getter: {{userInfo.newName}}</p>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
let change = () => {
userInfo.setUser()
}
</script>
<style scoped>
</style>
保存刷新,看一下效果。
现在默认的数据是上面截图的样子,然后我们点一下按钮,修改一下 name,然后看一下效果。
我们可以看见,点击修改 name 之后呢,getter 也会实时的去渲染出来。
相互调用
嘻嘻,没错啦,这个 getter 也是可以像 actions 一样相互调用的啦!
看代码:
getters: {
newName():string {
return `这是getter修饰过的名称 ${this.name} ,他的年纪是 ${this.getAge}`
},
getAge():number {
return this.age
}
},
保存,刷新一下看效果。
也是可以的哈。
API 的使用
pinia 里面呢,有很多的 API,今天就稍微说几个常见的哈。
$reset :重置到初始值
这个 $reset 可以将 state 的数据初始到初始值,比如我们有一个数据,点击按钮改变了,然后我们可以通过这个 API ,将数据恢复到初始状态值。
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
<el-button @click="change">修改用户内容</el-button>
<p>getter: {{userInfo.newName}}</p>
<el-button @click="reset">$reset</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
let change = () => {
userInfo.setUser()
}
// 重置
let reset = () => {
userInfo.$reset()
}
</script>
<style scoped>
</style>
我们先修改用户信息,然后在重置。
流弊!完美实现重置。
$subscribe:监听 state 数据变化
$subscribe 使用来监听的,监听 state 数据的变化,只要 state 里面的数据发生了变化,就会自动走这个函数。
<template>
<h3>pinia</h3>
<!-- 在页面中直接使用就可以了 -->
<p>pinia 中的信息: {{userInfo.name}} --- {{userInfo.age}}</p>
<el-button @click="change">修改用户内容</el-button>
<p>getter: {{userInfo.newName}}</p>
<el-button @click="reset">$reset</el-button>
</template>
<script setup>
// 首先需要引入一下我们刚刚创建的store
import { useInfoStore } from '../store';
// 因为是个方法,所以我们得调用一下
const userInfo = useInfoStore()
let change = () => {
userInfo.setUser()
}
// 重置
let reset = () => {
userInfo.$reset()
}
// 监听 state 的变化,返回一个工厂函数
userInfo.$subscribe((args, state) => {
console.log(args, state)
})
</script>
<style scoped>
</style>
监听函数写一下,然后保存刷新。
可以打印出数据,需要啥进去取就可以了。
$onAction:一调用 actions 就触发
这个看名字就很好理解了吧,就是 action 一调用就会被触发。
它里面只有一个参数 args。写一下关键代码吧。
userInfo.$onAction((args) => {
console.log(args)
})
保存刷新。
我们在点击的时候,之前案例是相互调用了两个 action,所以打印了两个。
其中打印出来的有一个 after ,这是回调,我们可以看一下,监听到了之后再走的回调,我们测试一下。
// $onAction
userInfo.$onAction((args) => {
args.after(() => {
console.log('after 回调')
})
console.log(args)
})
我们先打印了回调,在打印数据,保存看结果。
看到结果,反而是先输出了数据,在走的回调,理解了吧?
我们看到打印的数据还有一个 args,这个args 是 actions 传进来的参数。
好的,继续。
补充
补充一点哈,$onAction 刚才的案例我们只传了一个参数,就是一个工厂函数,其实他还有第二个参数—— true
,传 true 的意义是啥呢?就是当这个组件销毁了,这个 $onAction 还可以继续保活。
// $onAction
userInfo.$onAction((args) => {
args.after(() => {
console.log('after 回调')
})
console.log(args)
}, true)
不止 $onAction
可以传第二个参数,$subscribe
也有第二个参数,只不过 $subscribe 的参数是一个对象,对象里面设置的是 detached 为 true ,效果和 $onAction 是一样的,当然还有其它的参数,和 watch 是类似的。
userInfo.$subscribe((args, state) => {
console.log(args, state)
}, {
detached: true,
deep: true,
flush: 'post'
})
好了,关于常见的 API 也就这样了。
结束!
【版权声明】本博文著作权归作者所有,任何形式的转载都请联系作者获取授权并注明出处!
【重要说明】本文为本人的学习记录,论点和观点仅代表个人而不代表当时技术的真理,目的是自我学习和有幸成为可以向他人分享的经验,因此有错误会虚心接受改正,但不代表此刻博文无误!
【Gitee地址】秦浩铖:https://gitee.com/wjw1014