都2025年了,为什么还要写一篇文章专门介绍Vue的组件通信呢?正如我其它文章里面提到的,前端工程化的前提就是合理的组件规划,组件的编写一定要遵守高内聚、低耦合,还有很重要的开闭原则,组件划分的时候一定要考虑后期的功能扩展、功能变动 。如果你划分的组件,在功能需求发生改变或者后期功能扩展的时候让人根本无从下手,那你就要反思一下你自己的专业性。这篇文章除了介绍Vue官方提供的一些常规组件通信方式之外,还会介绍一个很抽象的概念,传递组件。
一、官方提供的组件通信方式
1、props传值
最常用的一种方式,这种方式你可以传递任何数据类型,string, number, boolean, array, object, function
比较取巧的一种方式是,你可以传递一个函数,使得子组件可以改父组件的变量,同时向父组件传递数据
html
// parent
<template>
<ChildCom :delivery-data="receiveDataFromChild"/>
</template>
<script setup lang="ts">
import ChildCom from './ChildCom.vue'
const receiveDataFromChild = (data: object) => {
console.log('接收到了子组件的data', data);
}
</script>
// child
<template>
<h2>child</h2>
</template>
<script setup lang="ts">
const props = defineProps<{
deliveryData: (data: object) => void
}>()
props.deliveryData({ hello: 'this is message from child component' })
</script>
2、emit通信
官方提供的向父组件传递数据的方式
html
<template>
<ChildCom @get-data="receiveDataFromChild"/>
</template>
<script setup lang="ts">
import ChildCom from './ChildCom.vue'
const receiveDataFromChild = (data: object) => {
console.log('接收到了子组件的data', data);
}
</script>
<template>
<h2>child</h2>
</template>
<script setup lang="ts">
const emit = defineEmits<{
getData: [{ name: string }]
}>()
emit('getData', { name: 'child' })
</script>
3、Vue2的$bus
全局事件总线,需要监听和发起
4、Vue3的provide/inject
可以在有父子关系的组件中跨组件通信,如果你想传递一个不能被子组件修改的值的话,有很多方式,比如传递计算属性、仅仅传递一个getter函数
js
// main.js
import { provide } from 'vue'
provide('appName', 'My App')
const obj = {
name: 'zhangsan'
}
const getObjName = () => {
return obj.name
}
provide('getObjName', getObjName)
html
<script setup lang="ts">
// 项目的任何页面
import { inject } from 'vue';
const appName = inject('appName');
const objName = inject('getObjName') as () => string
console.log(appName, objName())
</script>
5、pinia
官方推荐的替代vuex的状态管理库,使用更方便,数据也更清晰
6、作用域插槽
作用域插槽写到这里,不仅仅是因为默认插槽、具名插槽可以向父组件传递组件信息的同时不丢失响应性,还因为,作用域插槽可以有emit函数的功能,向父组件暴露子组件的变量。
html
<template>
<ul>
<li v-for="item in studentsArr" :key="item.name">
<slot name="header" :student="item">
{{ item.name }} - {{ item.age }} - {{ item.class }}
</slot>
</li>
</ul>
</template>
<script setup lang="ts">
const studentsArr = [
{
name: 'zhangsan',
age: 18,
class: 1
},
{
name: 'lisi',
age: 19,
class: 1
},
]
</script>
```html
<template>
<ChildCom>
<template #header="{ student }">
<span>姓名:{{ student.name }},年龄:{{ student.age }},班级:{{ student.class }}</span>
</template>
</ChildCom>
</template>
<script setup lang="ts">
import ChildCom from './ChildCom.vue'
</script>
二、逆向插槽 ------ 子组件向父组件传递组件
如果你的组件划分是合理的话,上述方式已经完完全全足够你开发任何项目了,但是,你自己合规,并不能代表接手的别人的代码也合规。遇到过一个场景,之前的开发他也会组件划分,但是划分方式极不合理,后期功能扩展的时候完全无法下手,由此引发了我对组件传值的思考。下文将完整介绍我的思路发展过程。示例代码只简单的描述组件关系。项目其实使用的是ts,但是为了降低一次性的信息量,案例使用js展示
html
<template>
<!-- 布局组件layout-com.vue -->
<CardCom title="现在它只支持传入文字">
<!-- 会有很多个业务组件,这里只写一个用作示例 -->
<BusinessCom/>
<BusinessCom1/>
<BusinessCom2/>
<BusinessCom3/>
</CardCom>
</template>
<script setup name="LayoutCom">
import CardCom from './card-com.vue'
import BusinessCom from './bussiness.vue'
</script>
html
<template>
<!-- 卡片布局组件card-com.vue -->
<div class="card-wp">
<div class="card-title-wp">
<div class="card-title-left">
<h3>{{ title }}</h3>
</div>
<p class="card-title-right">title-right</p>
</div>
<div>
<p>这里展示内容</p>
<!-- 默认插槽显示位置 -->
<slot></slot>
</div>
</div>
</template>
<script setup name="CardCom">
defineProps({
title: {
type: String,
default: ''
}
})
</script>
html
<template>
<!-- 业务组件business-com.vue -->
<button @click="handleClick"> 点这个按钮num++ </button>
<p>{{ num }}</p>
</template>
<script setup name="BusinessCom">
const num = ref(0)
const handleClick = () => {
// 仅做展示用,实际业务场景比这复杂的多
num.value += 1
}
</script>
上边展示了现有组件之间的关系,新需求是,将业务组件的部分操作移动到卡片的标题右边,也就是card-title-left的div里面。
很明显,就像我之前提到的,如果组件划分、使用的方式合理的话,官方提供的那些通信方式完完全全够用,比如,像下边这样。
html
<template>
<!-- 布局组件layout-com.vue -->
<CardCom title="现在它只支持传入文字">
<BusinessCom/>
</CardCom>
<CardCom title="现在它只支持传入文字">
<BusinessCom/>
</CardCom>
<CardCom title="现在它只支持传入文字">
<BusinessCom/>
</CardCom>
</template>
<script setup name="LayoutCom">
import CardCom from './card-com.vue'
import BusinessCom from './bussiness.vue'
</script>
如果是这种方式的话,很简单,在CardCom里面加一个具名插槽 就可以了,但是,聪明的前辈程序员不是这么写的,它现在只对这个组件用了一次,而且,基于原有的组件方式,已经实现了很复杂的交互场景,要想改的话,当前业务场景相当于完全重写。
所以,如果是你们,你们现在打算怎么做?
好的,重点来了,现在介绍一下我的解决方案
- 首先,我们都知道javascript的特点,万物皆对象,任何参数其实都可以通过函数参数的方式传递,而vue是一个js框架,那么,我们是不是可以通过参数的方式传递vue组件?
- 好的,那么,让我们打印一下vue引入的组件,看看在控制台会输出什么?
- 它是不是个对象?那么,我们是不是就可以通过参数的方式传递这个组件?试一下,看看代码能不能运行
html
<template>
<!-- 布局组件layout-com.vue -->
<CardCom title="现在它只支持传入文字">
<template #title-operate>
<!-- 必须加这个判断,不然不符合业务规则 -->
<component v-if="deliveryCom" :is="deliveryCom"/>
</template>
<!-- 会有很多个业务组件,这里只写一个用作示例 -->
<BusinessCom @delivery="transferComponent" />
<BusinessCom1 />
<BusinessCom2 />
<BusinessCom3 />
</CardCom>
</template>
<script setup name="LayoutCom">
import CardCom from './card-com.vue'
import BusinessCom from './bussiness.vue'
const deliverCom = shallowRef(null)
const transferComponent = (params) => {
if(params.componentName === "BussinessCom") {
deliverCom.value = params.component
}
}
</script>
html
<template>
<!-- 卡片布局组件card-com.vue -->
<div class="card-wp">
<div class="card-title-wp">
<div class="card-title-left">
<h3>{{ title }}</h3>
<!-- 新加的一行代码 -->
<slot name="titleOperate"/>
</div>
<p class="card-title-right">title-right</p>
</div>
<div>
<p>这里展示内容</p>
<!-- 默认插槽显示位置 -->
<slot></slot>
</div>
</div>
</template>
<script setup name="CardCom">
defineProps({
title: {
type: String,
default: ''
}
})
</script>
html
<template>
<!-- vue3 -->
<!-- 业务组件business-com.vue -->
<!-- <button @click="handleClick"> 点这个按钮num++ </button> -->
<p>{{ num }}</p>
</template>
<script setup lang="jsx" name="BusinessCom">
defineEmits([''])
const num = ref(0)
const handleClick = () => {
// 仅做展示用,实际业务场景比这复杂的多
num.value += 1
}
const deliveryTest = defineComponent({
render() {
return <button onClick={handleClick}> 点这个按钮num++ </button>
}
})
onMounted(()=>{
emit('delivery', {
componentName: 'BussinessCom',
component: deliveryTest
})
})
</script>
html
<template>
<!-- vue2 -->
<!-- 业务组件business-com.vue -->
<!-- <button @click="handleClick"> 点这个按钮num++ </button> -->
<p>{{ num }}</p>
</template>
<script>
export default {
data() {
return {
num: 0,
};
},
mounted() {
this.$emit("delivery", {
componentName: "BussinessCom",
component: {
render: () => {
return <button onClick={this.handleClick}> 点这个按钮num++ </button>;
},
},
});
},
methods: {
handleClick() {
this.num++;
},
},
};
</script>
完美运行,响应式未丢失,而且,原有的通信的方式不再局限于js本身的数据格式,而且实现了逆向插槽 ,即组件的子组件向父组件传递。虽然每次增加一个组件都需要新定义一个变量,但是,这是为了修补别人的代码做出的替代方案,毕竟,一个合格的程序员不会写出这种离谱的结构。
上面的这种方式还用到的jsx语法,不会的同学可以去了解一下,这种方式使得Vue本身其实也具有巨大的灵活性,Vue模板本身就是前端编程界的一个壮举,再加入jsx,使得Vue具有了react的灵活性。同时,还有它强大的响应式系统和本身就做了的组件优化,这俩都是react相对的弱项。