Vue3中的常见组件通信之pinia
概述
在vue3中常见的组件通信有props、mitt、v-model、 r e f s 、 refs、 refs、parent、provide、inject、pinia、slot等。不同的组件关系用不同的传递方式。常见的撘配形式如下表所示。
组件关系 | 传递方式 |
---|---|
父传子 | 1. props 2. v-model 3. $refs 4. 默认插槽、具名插槽 |
子传父 | 1. props 2. 自定义事件 3. v-model 4. $parent 5. 作用域插槽 |
祖传孙、孙传祖 | 1. $attrs 2. provide、inject |
兄弟间、任意组件间 | 1. mitt 2. pinia |
props和自定义事件详见:
Vue3中的常见组件通信之props和自定义事件
mitt用法详见:
Vue3中的常见组件通信之mitt
v-model用法详见:
Vue3中的常见组件通信之v-model
$attrs
用法详见:
Vue3中的常见组件通信之$attrs
$refs
和$parent
详见:
Vue3中的常见组件通信之$refs
和$parent
provide和inject详见:
Vue3中的常见组件通信之provide和inject
接下来是pinia。
8.pinia
pinia是一个集中式状态管理工具,是符合直觉的集中式状态管理工具。
8.1准备组件
首先准备3个组件,父组件代码如下:
vue
<template>
<div class="father">
<Header/>
<Content/>
</div>
</template>
<script setup lang="ts" name="Index">
import Header from './Header.vue';
import Content from './Content.vue';
</script>
<style scoped>
.father{
height: 300px;
width: 800px;
background-color: rgb(169, 169, 169);
margin: 5px;
padding: 0;
}
</style>
Header组件代码如下:
vue
<template>
<div class="header">
<img class="thoto" :src="user.thoto" alt="头像">
<span>{{ user.name }}</span>
</div>
</template>
<script setup lang="ts" name="Header">
import {ref,reactive} from 'vue'
let user = reactive({
id:'m0_63165331',
name:'m0_63165331',
thoto:'https://xyyhxxx.oss-cn-beijing.aliyuncs.com/picGoImg/202406111305778.jpg'
})
</script>
<style scoped>
.header{
height: 50px;
background-color: #3b818c;
border-bottom: 1px solid rgb(255, 255, 255);
overflow: hidden;
font-size: 0;
text-align: center;
line-height: 50px;
}
span{
font-size: 16px;
vertical-align: middle;
color: #fff;
}
.thoto{
height: 35px;
border-radius: 50%;
vertical-align: middle;
margin-right: 5px;
}
</style>
Content组件代码如下:
vue
<template>
<div class="outer">
<div class="user">
<img class="thoto" :src="user.thoto" alt="头像">
<br>
<div class="user-inf">
<span>账号:{{ user.id }}</span>
<br>
<span>昵称:{{ user.name }}</span>
<br>
<span>性别:{{ user.gender }}</span>
<br>
<span>排名:{{ user.rank }}</span>
<br>
<span>粉丝:{{ user.fans }}</span>
</div>
</div>
<div class="content">
<textarea name="msg" >评论内容</textarea>
</div>
</div>
</template>
<script setup lang="ts" name="Content">
import {ref,reactive} from 'vue'
let user = reactive({
id:'m0_63165331',
name:'m0_63165331',
gender:'男',
rank:19102,
fans:1040,
thoto:'https://xyyhxxx.oss-cn-beijing.aliyuncs.com/picGoImg/202406111305778.jpg'
})
</script>
<style scoped>
.outer{
height: 250px;
overflow: hidden;
}
.user{
height: 240px;
width: 150px;
margin: 5px;
background-color: #c6e6e8;
text-align: center;
float: left;
border-radius: 5px;
box-shadow: 0 0 5px black;
}
.user-inf{
width: 150px;
text-align: left;
padding: 0 20px;
}
span{
font-size: 16px;
color: #000;
font-size: 10px;
font-family: 微软雅黑;
font-weight: 600;
}
.thoto{
height: 50px;
border-radius: 40%;
margin: 10px;
}
.content{
width:630px;
height: 240px;
/* background-color: #c7d2d4; */
float: right;
margin: 5px;
}
textarea{
width:630px;
height: 240px;
background-color: #c6e6e8;
border: none;
padding: 5px;
border-radius: 5px;
box-shadow: 0 0 5px black;
}
</style>
呈现效果如下:
8.2 搭建pinia环境
第一步先安装pinia
sh
npm indtall pinia
第二步在src/main.ts中引入createPinia并创建pinia,然后再安装pinia插件,如下代码:
typescript
import {createApp} from 'vue'
import App from './App.vue'
//1.引入createPinia,用于创建pinia
import {createPinia} from 'pinia'
// 创建应用
const app = createApp(App)
// 2.创建pinia
const pinia = createPinia()
// 3.安装pinia插件
app.use(pinia)
// 挂载应用
app.mount('#app')
此时我们已经创建了pinia环境,在浏览器开发者工具中能看到pinia
8.3存储和读取数据
pinia是一个轻量化的状态管理工具,一般把组件中共用的数据存储在store中,不适合把所有的数据都用pinia来管理。前面创建的例子中user数据是共用的,可以放入到store中。
Store
是一个保存:状态 、业务逻辑 的实体,每个组件都可以读取 、写入它。
它有三个概念:state
、getter
、action
,相当于组件中的: data
、 computed
和 methods
。
在src文件夹下新建stoer文件夹,然后在文件夹下新建文件user.ts。这个文件用于存储user相关的数据、逻辑。
在user.ts文件中先//引入defineStore,用于创建store。
typescript
//引入defineStore,用于创建store
import {defineStore} from 'pinia'
store的变量名建议以use开头Srore结尾的大驼峰式命名。如下代码创建useUserStore,并暴露。defineStore接收两个参数,第一个参数是id值,官方建议与文件名保持一致,第二个参数有两种写法,第一种是选项式写法,参数是配置对象,第二种是组合式写法,是函数。
如下代码是选项式写法:它有一个state配置项,state必须写成函数,内部return一个对象,对象内部写具体的数据,如下:
typescript
export const useUserStore = defineStore('user',{
//状态
state() {
return {
user:{
id:'m0_63165331',
name:'m0_63165331',
gender:'男',
rank:19102,
fans:1040,
thoto:'https://xyyhxxx.oss-cn-beijing.aliyuncs.com/picGoImg/202406111305778.jpg'
}
}
},
})
这样写完就拥有了一个用于存储user相关内容的仓库,接下来需要在组件中引入仓库,引入后可直接调用便可得到对应的store,如下代码:
typescript
// 引入对应的useXxxxxStore
import { useUserStore } from '../store/user';
// 调用useXxxxxStore得到对应的store
const userStore = useUserStore()
那个这个userStore具体是什么,可以在控制台打印输出得到如下内容:
从上面控制台打印的信息可以得到,userStore是个响应式对象,它身上有user对象,还有 s t a t e , state, state,state身上也有user对象,说明有两种方式可以拿到user对象的数据,如下:
typescript
console.log(userStore.user)
console.log(userStore.$state.user)
控制台结果如下:
可以看到两种拿数据的方法得到的结果是完全一样的,所以我们使用简单的方式来拿数据,在页面中展示用如下代码:
vue
<div class="user">
<img class="thoto" :src="userStore.user.thoto" alt="头像">
<br>
<div class="user-inf">
<span>账号:{{ userStore.user.id }}</span>
<br>
<span>昵称:{{ userStore.user.name }}</span>
<br>
<span>性别:{{ userStore.user.gender }}</span>
<br>
<span>排名:{{ userStore.user.rank }}</span>
<br>
<span>粉丝:{{ userStore.user.fans }}</span>
</div>
</div>
运行后结果跟没有用pinia之前是完全一样的,不再附图。
8.4 修改数据
pinia修改数据有三种方式。
**第一种修改方式:**是符合直觉的方式,拿到数据后就直接修改,如下代码,先创建一个方法用来更改粉丝数量:
typescript
function addFans(){
userStore.user.fans += 1
}
然后在组件中添加按钮并绑定事件:
vue
<button @click="addFans">粉丝数量+1</button>
运行后效果如下:
**第二种修改方式:**是批量修改方式,用$path,它只需要把要修改的数据传递即可,如下代码:
typescript
function changeInf(){
userStore.$patch( {
user : {
name:'一叶知秋',
rank:5000,
fans:10000,
}
})
}
添加按钮并绑定事件,如下:
vue
<button @click="changeInf">更改数据</button>
运行效果如下:
**第三种修改方式:**借助pinia的action修改。在user.ts文件中的defineStore第二个参数中增加如下配置项:
typescript
//动作
actions:{
changeRankAddFans(){
if (this.user.fans<10000){
this.user.fans += 1000
};
if (this.user.rank>10000){
this.user.rank -= 1000
}
}
},
在组件中引入过userStore后可以直接调用,如下代码添加按钮并绑定事件:
vue
<button @click="userStore.changeRankAddFans">粉丝数量+1000,排名往前1000</button>
运行后如下效果:
8.5 getters
pinia中的getters类似计算属性,如下代码所示,在user.ts中添加getters配置项:
typescript
getters:{
changeRank(state){
if (state.user.rank > 10000){
return Math.floor(state.user.rank / 10000) + "万+"
}else{
return state.user.rank
}
}
}
在组件页面中调整显示内容如下:
html
<span>排名:{{ userStore.changeRank }}</span>
运行后效果如下,排名超过1万时显示1万+,在1万名以内显示具体数字:
8.6 $subscribe
pinia中的$subscribe
可以对pinia仓库中的数据进行监视,与watch类似,如下代码:
typescript
userStore.$subscribe(()=>{
console.log('userStore中的数据发生变化了')
})
$subscribe
是一个函数,需要调用,调用的同时要传入一个函数,可以用箭头函数。上面代码运行后,当数据发生变化的时候这句话就会打印,如下图示意:
$subscribe
函数传入的函数中可以接收两个参数,第一个参数是数据变化的信息,第二个参数是变化后的数据。如下代码示意:
typescript
userStore.$subscribe((mutate,state)=>{
console.log('userStore中的数据发生变化了')
console.log(mutate)
console.log(state)
})
8.7 store组合式写法
前面8.3小节提到过store有两种写法,一种是选项式,一种是组合式,前面演示的均为选项式,下面代码为组合式写法。
typescript
export const useUserStore = defineStore('user',()=>{
let user = reactive({
id:'m0_63165331',
name:'m0_63165331',
gender:'男',
rank:19102,
fans:1040,
thoto:'https://xyyhxxx.oss-cn-beijing.aliyuncs.com/picGoImg/202406111305778.jpg'
})
function changeRankAddFans(){
if (user.fans<10000){
user.fans += 1000
};
if (user.rank>10000){
user.rank -= 1000
}
}
let changeRank = computed(()=>{
if (user.rank > 10000){
return Math.floor(user.rank / 10000) + "万+"
}else{
return user.rank
}
})
return {user,changeRankAddFans,changeRank}
})
8.8 小结
pinia是符合直接的vue.js的状态管理工具。
Store
是一个保存:状态 、业务逻辑 的实体,每个组件都可以读取 、写入它。
它有三个概念:state
、getter
、action
,相当于组件中的: data
、 computed
和 methods
。
它可以按照选项式风格来编写代码,也可以按照组合式风格来编写代码。
以下是完整代码:
src/main.ts中代码如下
typescript
import {createApp} from 'vue'
import App from './App.vue'
//引入createPinia,用于创建pinia
import {createPinia} from 'pinia'
// 创建应用
const app = createApp(App)
// 创建pinia
const pinia = createPinia()
// 安装pinia插件
app.use(pinia)
// 挂载应用
app.mount('#app')
src/store/user.ts中代码如下:
typescript
//引入defineStore,用于创建store
import {defineStore} from 'pinia'
import {reactive,computed} from 'vue'
//创建seUserStore并暴露
//选项式写法
// export const useUserStore = defineStore('user',{
// //状态
// state() {
// return {
// user:{
// id:'m0_63165331',
// name:'m0_63165331',
// gender:'男',
// rank:19102,
// fans:1040,
// thoto:'https://xyyhxxx.oss-cn-beijing.aliyuncs.com/picGoImg/202406111305778.jpg'
// }
// }
// },
// //动作
// actions:{
// changeRankAddFans(){
// if (this.user.fans<10000){
// this.user.fans += 1000
// };
// if (this.user.rank>10000){
// this.user.rank -= 1000
// }
// }
// },
// //计算
// getters:{
// changeRank(state){
// if (state.user.rank > 10000){
// return Math.floor(state.user.rank / 10000) + "万+"
// }else{
// return state.user.rank
// }
// }
// }
// })
//组合式写法
export const useUserStore = defineStore('user',()=>{
let user = reactive({
id:'m0_63165331',
name:'m0_63165331',
gender:'男',
rank:19102,
fans:1040,
thoto:'https://xyyhxxx.oss-cn-beijing.aliyuncs.com/picGoImg/202406111305778.jpg'
})
function changeRankAddFans(){
if (user.fans<10000){
user.fans += 1000
};
if (user.rank>10000){
user.rank -= 1000
}
}
let changeRank = computed(()=>{
if (user.rank > 10000){
return Math.floor(user.rank / 10000) + "万+"
}else{
return user.rank
}
})
return {user,changeRankAddFans,changeRank}
})
Father组件中代码:
vue
<template>
<div class="father">
<Header/>
<Content/>
</div>
</template>
<script setup lang="ts" name="Index">
import Header from './Header.vue';
import Content from './Content.vue';
</script>
<style scoped>
.father{
height: 300px;
width: 800px;
background-color: rgb(169, 169, 169);
margin: 5px;
padding: 0;
}
</style>
Header组件代码:
vue
<template>
<div class="header">
<img class="thoto" :src="userStore.user.thoto" alt="头像">
<span>{{ userStore.user.name }}</span>
</div>
</template>
<script setup lang="ts" name="Header">
// 引入对应的useXxxxxStore
import { useUserStore } from '../store/user';
// 调用useXxxxxStore得到对应的store
const userStore = useUserStore()
</script>
<style scoped>
.header{
height: 50px;
background-color: #3b818c;
border-bottom: 1px solid rgb(255, 255, 255);
overflow: hidden;
font-size: 0;
text-align: center;
line-height: 50px;
}
span{
font-size: 16px;
vertical-align: middle;
color: #fff;
}
.thoto{
height: 35px;
border-radius: 50%;
vertical-align: middle;
margin-right: 5px;
}
</style>
Content组件:
vue
<template>
<div class="outer">
<div class="user">
<img class="thoto" :src="userStore.user.thoto" alt="头像">
<br>
<div class="user-inf">
<span>账号:{{ userStore.user.id }}</span>
<br>
<span>昵称:{{ userStore.user.name }}</span>
<br>
<span>性别:{{ userStore.user.gender }}</span>
<br>
<span>排名:{{ userStore.changeRank }}</span>
<br>
<span>粉丝:{{ userStore.user.fans }}</span>
</div>
</div>
<div class="content">
<textarea name="msg" >评论内容</textarea>
<button @click="addFans">粉丝数量+1</button>
<button @click="changeInf">更改数据</button>
<button @click="userStore.changeRankAddFans">粉丝数量+1000,排名往前1000</button>
</div>
</div>
</template>
<script setup lang="ts" name="Content">
import {ref,reactive,toRefs} from 'vue'
// 引入对应的useXxxxxStore
import { useUserStore } from '../../store/user';
// 调用useXxxxxStore得到对应的store
const userStore = useUserStore()
userStore.$subscribe((mutate,state)=>{
console.log('userStore中的数据发生变化了')
console.log(mutate)
console.log(state)
})
//修改数据第一种方式
function addFans(){
userStore.user.fans += 1
}
//修改数据第二种方式
function changeInf(){
userStore.$patch( {
user : {
name:'一叶知秋',
rank:5000,
fans:10000,
}
})
}
</script>
<style scoped>
.outer{
height: 250px;
overflow: hidden;
}
.user{
height: 240px;
width: 150px;
margin: 5px;
background-color: #c6e6e8;
text-align: center;
float: left;
border-radius: 5px;
box-shadow: 0 0 5px black;
}
.user-inf{
width: 150px;
text-align: left;
padding: 0 20px;
}
span{
font-size: 16px;
color: #000;
font-size: 10px;
font-family: 微软雅黑;
font-weight: 600;
}
.thoto{
height: 50px;
border-radius: 40%;
margin: 10px;
}
.content{
width:630px;
height: 240px;
/* background-color: #c7d2d4; */
float: right;
margin: 5px;
}
textarea{
width:630px;
height: 200px;
background-color: #c6e6e8;
border: none;
padding: 5px;
border-radius: 5px;
box-shadow: 0 0 5px black;
}
button{
background-color: #c6e6e8;
height: 30px;
margin-top: 5px;
margin-right: 10px;
border-radius: 5px;
padding: 5px;
box-shadow: 0 0 5px black;
border: none;
transition: box-shadow 0.3s;
}
button:hover{
box-shadow: 0 0 8px rgb(132, 132, 132);
}
button:active{
box-shadow: 0 0 10px rgb(255, 255, 255);
}
</style>