🌈个人主页:前端青山
🔥系列专栏:Vue篇
🔖人终将被年少不可得之物困其一生
依旧青山,本期给大家带来Vue篇专栏内容:Vue-依赖注入-中央事件总线
大家好,依旧青山,
最近呢也随着需求的变更调优,加载数字孪生地图的缓慢,要将原有vue3+Ts数据大屏子菜单整合到一个地图环境下(注:无需加载其余地图场景,同一地图环境下切换不同菜单),也就是主页面及子菜单调用一次地图环境即可,页面很好集合前嵌套,但是不同页面对地图的操作该如何呢?
那么我首先做的就是封装一个公共的地图调用方法,以组件形式引入所有子菜单实现跨组件通信!
以数字孪生地图为例
import { ElLoading } from 'element-plus'
import mapJson from '@/utils/tjbhJson';
import textArr from '@/utils/textJson';
import cloudRenderer from "51superapi"
import { ref, onMounted, onBeforeUnmount,reactive,watchEffect } from "vue"
// 引入前缀路径
//封装51地图函数
const app = new cloudRenderer("mapDiv");
export default function (){
let loadingInstance: any; // 在更宽泛的作用域定
const prefixUrl = import.meta.env.VITE_APP_BASE_API || '';
//配置51地图参数
const startRenderConfig = reactive({
"url": "http://192.168.1.20:8080", //[必须] 云渲染服务地址; 8889:固定端口
"order": "123456", //[必须] 渲染口令; 在云渲染客户端上获得
"resolution": [window.innerWidth > 1920? 4096 : window.innerWidth,window.innerHeight >1080? 1209 : window.innerHeight], //[可选] 设置渲染场景像素分辨率
"nodestyle": `width:${window.innerWidth > 1920? 4096 : window.innerWidth};height:${window.innerHeight >1080? 1209 : window.innerHeight};position:absolute;top:0px;left:0px;bottom:0px;right:0px;margin:auto;`, //[可选] 设置渲染场景容器DOM节点样式, 与设置渲染场景像素分辨率配对使用
"keyboard": "keyboardnofn", //[可选] 初始建盘事件, 开启wasd方向键 [选项: keyboard/keyboardnofn; 详见注册键盘事件]
"setlogmode": true, //[可选] 开启/关闭SuperAPI调用日志, 默认false
})
// 设置初始分辨率
startRenderConfig.resolution = [
window.innerWidth > 1920 ? 4096 : window.innerWidth,
window.innerHeight > 1080 ? 1209 : window.innerHeight,
];
//围绕中心旋转
let jsonData = {
"time": 50, //相机旋转一周所需要的时间, (单位:秒)
"direction": "stop" //clockwise:顺时针; anticlockwise:逆时针; stop:停止旋转
}
//添加区域轮廓
let jsondata2 = {
"id": "range_id",
"coord_type": 0, //坐标类型(0:经纬度坐标, 1:cad坐标)
"cad_mapkey": "", //CAD基准点Key值, 项目中约定
"coord_z": 0, //高度(单位:米)
"coord_z_type": 0, //坐标高度类型(0:相对3D世界表面;1:相对3D世界地面;2:相对3D世界海拔; 注:cad坐标无效)
"type": "loop_line", //样式类型; 注①
"color": "ffffff", //轮廓颜色(HEXA颜色值)
"range_height": 60, //围栏高度(单位:米)
"stroke_weight": 10, //底部轮廓线宽度(单位:米; 注: 区域中含有内环"inner_points"时无效)
"fill_area": "none", //底部区域填充类型; 注②
"geojson": mapJson, //geojson数据; 注③
}
//添加3d文字信息与区域轮廓
const pushAllCovering = () => {
app.SuperAPI("Add3DText", textArr, (status: any) => {
console.log(status); //成功、失败回调
});
//添加区域轮廓
app.SuperAPI('AddGeoRange', jsondata2).then((_back: any) => {
})
}
//初始地图视角
const Camejsondata = {
"coord_type": 0, //坐标类型(0:经纬度坐标, 1:cad坐标)
"cad_mapkey": "", //CAD基准点Key值, 项目中约定
"coord_z": "2.06", //海拔高度(单位:米)
"center_coord": "117.689178,39.01527", //中心点的坐标 lng,lat
"arm_distance": 3000, //镜头距中心点距离(单位:米)
"pitch": 30, //镜头俯仰角(5~89)
"yaw": 70, //镜头偏航角(0正北, 0~359)
"fly": true //true: 飞行动画(有一个短暂飞行动画,并按照arm_distance,pitch,yaw设置镜头);
//false: 立刻跳转过去(瞬移)
}
//设置渲染质量
let jsonDate ={
"quality": "epic" //low:低; medium:中; high:高; epic:超高;
}
// 地图事件注册函数
const myHandleResponseFunction = (data: string) => {
const jsonObject = typeof data === "object" ? JSON.parse(JSON.stringify(data)) : JSON.parse(data);
switch (jsonObject.func_name) {
case "APIAlready":
app.SuperAPI("RemoveAllCovering", {
covering_type: "all", //覆盖物类型, 详见下表
})
.then((_back: any) => {
console.log(_back);
});
pushAllCovering(); //添加区域轮廓
//设置镜头绕场景中心点旋转
app.SuperAPI("SetCameraRotate", jsonData, (e: any) => {
})
//设置当前场景镜头视界
app.SuperAPI("SetCameraInfo", Camejsondata, (status: any) => {
})
app.SuperAPI("SetRenderQuality", jsonDate, (status:any) => {
console.log(status,'设置渲染质量'); //成功、失败回调
})
loadingInstance.close();
break;
case 'OnPOIClick':
const coord = jsonObject.args.coord;
const poiId = jsonObject.args.id;
console.log(poiId,"poiId")
break;
}
return data;
}
const myStartRender = async () => {
try {
// 设置初始分辨率
startRenderConfig.resolution = [
window.innerWidth > 1920 ? 4096 : window.innerWidth,
window.innerHeight > 1080 ? 1209 : window.innerHeight,
];
await app.startRender(startRenderConfig).then((el: any) => {
loadingInstance = ElLoading.service({ // 赋值给外部变量
lock: true,
text: '地图加载中',
background: 'rgba(0, 0, 0, 0.7)',
});
// 事件注册;事件监听处理器函数, 接收所有从云渲染返回的事件, 数据等信息
app.RegisterCloudResponse(myHandleResponseFunction);
})
} catch (error) {
console.error("error:", error)
}
}
const SuperAPI = () => {
//先删除全部覆盖物 覆盖物类型, 详见下表
app.SuperAPI("RemoveAllCovering", { "covering_type": "poi" }, (status: any) => {
console.log(status); //成功、失败回调
})
}
// 监听窗口大小变化
watchEffect(() => {
startRenderConfig.resolution = [
window.innerWidth > 1920 ? 4096 : window.innerWidth,
window.innerHeight > 1080 ? 1209 : window.innerHeight,
];
});
return { app, startRenderConfig, myStartRender, myHandleResponseFunction,prefixUrl,loadingInstance, SuperAPI }
}
把公共地图渲染部分封装为一个ts文件,并暴露出myStartRender
函数方便在主页面onMounted
函数中调用并渲染地图,依次执行即可,那么大家可以看到还暴露出一个app
进行全局调用,是因为这个数字孪生地图的操作都要以app.(地图操作Api)
的形式调用
最终我们在页面中删除公共部分,只需引入公共函数即可!
<template>
<div id="main-content">
<!-- 地图盒子 -->
<div id="mapDiv">
</div>
<Header :naturalHazards="header"/>
<div v-if="header == '主页面'">
<NaturalHazard/>
</div>
<div v-else-if="header == '菜单一'">
<EarlyWarningDetection />
</div>
<div v-else-if="header == '菜单二'">
<DisasterGeneralData />
</div>
<div v-else-if="header == '菜单三'">
<JobFacilities />
</div>
<div v-else-if="header == '菜单四'">
<RiskHiddenDanger />
</div>
<div v-else-if="header == '菜单五'">
<VideoSurveillance />
</div>
<div v-else-if="header == '菜单六'">
<HydrologicMonitoring />
</div>
<div v-else-if="header == '菜单七'">
<FloodFightingMaterials />
</div>
<div v-else-if="header == '菜单八'">
<RescueTeam />
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount, nextTick, provide } from "vue";
//引入51地图SuperAPI
import useSuperApi from "@/utils/useSuperApi"
const {
app,
prefixUrl,
SuperAPI,
} = useSuperApi()
onMounted(() => {
nextTick(() => {
myStartRender()
})
});
onBeforeUnmount(() => {
app.StopRenderCloud(); //关闭云渲染, 释放资源
})
</script>
那么随之而来问题也就来了,当地图出现poi点的时候,我们点击对应的poi点肯定要实现不同的事件,我们现在所封装的app暴露出来可以进行打点操作,点击poi点的操作是由地图函数内部执行
// 地图事件注册函数
const myHandleResponseFunction = (data: string) => {
const jsonObject = typeof data === "object" ? JSON.parse(JSON.stringify(data)) : JSON.parse(data);
switch (jsonObject.func_name) {
case 'OnPOIClick':
const coord = jsonObject.args.coord;
const poiId = jsonObject.args.id;
console.log(poiId,"poiId"点击poi点所获得的id及经纬度)
break;
}
return data;
}
在没有整合之前调用的时候是在当前页面的地图函数下执行,请看下方
// 地图事件注册函数
const myHandleResponseFunction = (data: string) => {
const jsonObject = typeof data === "object" ? JSON.parse(JSON.stringify(data)) : JSON.parse(data);
switch (jsonObject.func_name) {
case 'OnPOIClick':
const coord = jsonObject.args.coord;
const poiId = jsonObject.args.id;
handlePOIClick(poiId, coord);
break;
}
return data;
}
// 处理自定义POI Label点击事件的函数
const handlePOIClick = (poiId: string, coord: string) => {
const [type, id] = poiId.split('_'); // 分割前缀和ID
switch (type) { // 假设id格式为"type_ID",通过前缀区分类型
case 'ggwhcs':
//公共文化场所
handelGgwhcs(id);
break;
case 'lyjq':
//旅游景区
handelLyjq(id);
break;
default:
console.log(`未识别的POI类型: ${poiId}`);
break;
}
}
那么现在我们封装成一个公共函数,且渲染地图只在主页面调用,就要想办法将函数内部的poiId和coord
作为参数暴露出去,方便我们每个子页面调用执行不同的操作,这里我就想到了vue的中央事件总线和依赖注入!
Vue3
提供了多种机制来支持组件间的通信,包括中央事件总线和依赖注入。选择哪种方式取决于具体的应用场景和需求
中央事件总线使用
在处理地图的poi点点击事件时,我们可以先使用中央事件总线来执行我们组件不同页面点击poi点的处理逻辑,
首先,在utils
文件夹下创建一个EventBus.ts
文件
//封装中央事件总线
class EventBus {
private events: Record<string, Function[]> = {};
on(event: string, callback: Function) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
}
off(event: string, callback: Function) {
if (!this.events[event]) return;
this.events[event] = this.events[event].filter(cb => cb !== callback);
}
emit(event: string, ...args: any[]) {
if (!this.events[event]) return;
this.events[event].forEach(callback => callback(...args));
}
}
const eventBus = new EventBus();
export default eventBus;
在main.ts
中创建一个全局的事件总线
app.config.globalProperties.$bus = {}; // 直接在全局属性中创建事件总线
然后再封装的内部地图函数poi点击事件时进行发送事件
case 'OnPOIClick':
const coord = jsonObject.args.coord;
const poiId = jsonObject.args.id;
console.log(poiId,"poiId")
eventBus.emit('poi-click', poiId, coord); // 发送事件
break;
然后再主页面和各个子页面引入eventBus
,在onMounted
和onBeforeUnmount
监听和移出事件总线
onMounted(() => {
nextTick(() => {
eventBus.on('poi-click', handlePOIClick);
//handlePOIClick为poi点击事件
init();//初始化函数
})
});
onBeforeUnmount(() => {
//移出监听
eventBus.off('poi-click', handlePOIClick);
})
这时,不管是我们的主页面,还是子菜单,都可以在切换的时候对应页面的poi点进行不同的处理逻辑了
依赖注入使用
单个菜单调用地图不同服务的事情解决了,那子菜单和主页面或子菜单和子菜单之间还有通信的复杂操作呢
比如在主页面的Echarts图表中,柱状图列出了A
页面和B
页面的统计数据,当我点击不同的柱状图时要切换到当前菜单,并直接选中状态及地图出现对应的操作,这时,基于这种复杂的操作我们可以使用依赖注入
在主页面先通过ref
绑定对应组件,并引入provide
提供依赖
<template>
<div id="main-content">
<!-- 地图盒子 -->
<div id="mapDiv">
</div>
<Header :naturalHazards="header"/>
<div v-if="header == '主页面'">
<NaturalHazard/>
</div>
<div v-else-if="header == '菜单一'">
<EarlyWarningDetection />
</div>
<div v-else-if="header == '菜单二'" ref="disasterGeneralData">
<DisasterGeneralData />
</div>
<div v-else-if="header == '菜单三'">
<JobFacilities />
</div>
<div v-else-if="header == '菜单四'">
<RiskHiddenDanger />
</div>
<div v-else-if="header == '菜单五'">
<VideoSurveillance />
</div>
<div v-else-if="header == '菜单六'">
<HydrologicMonitoring />
</div>
<div v-else-if="header == '菜单七'">
<FloodFightingMaterials />
</div>
<div v-else-if="header == '菜单八'">
<RescueTeam />
</div>
</div>
</template>
<script setup lang="ts">
import { Search } from '@element-plus/icons-vue'
import { ref, onMounted, onBeforeUnmount, nextTick, provide } from "vue";
eam/index.vue";
import useSuperApi from "@/utils/useSuperApi";
import eventBus from '@/utils/EventBus';
const {
app,
myStartRender,
prefixUrl,
SuperAPI
} = useSuperApi()
const header = ref<any>("自然灾害")
let disasterGeneralData = ref<any>()
const setNames = (name: any) => {
//这里我们提供一个函数来接收传进来的name
}
provide("setNames",setNames)
onMounted(() => {
nextTick(() => {
myStartRender()
})
});
onBeforeUnmount(() => {
app.StopRenderCloud(); //关闭云渲染, 释放资源
})
</script>
<style scoped lang="scss">
</style>
然后在我们的图表组件页面中注入依赖
let setNames: any = inject("setNames")
当我们点击对应的echarts图表时
zgrwczqktance.value.on('click', (params: any) => {
nextTick(() => {
setNames(params.name)//传入对应name
})
});
那么我们子菜单页面肯定是要通过传入的name来执行不同的地图操作或展示详情等逻辑...
const setName = (name: any) => {
const mappings:any = {
"菜单一": [1, '菜单一'],
"菜单二": [3, '菜单二'],
"菜单三": [6, '菜单三'],
"菜单四": [11, '菜单四'],
"菜单五": [7, '菜单五'],
};
const [id, description] = mappings[name] || [];
if (id !== undefined) {
(name === "菜单一" || name === "菜单二" || name === "菜单四" || name === "菜单五" || name === "菜单三")
? abreastClicks(id, description)
: abreastClick(id, description);
}
};
然后我们把这个方法通过defineExpose
给暴露出去
defineExpose({
setName
})
最后在我们的主页面通过ref
所绑定实例再取到依赖注入传入的参数和暴露的内部方法来进行通信啦
let disasterGeneralData = ref<any>()//ref绑定实例
const setNames = (name: any) => {
disasterGeneralData.value.setName(name)//子菜单内部的setName方法(已暴露)
}
总结
中央事件总线
-
优点:
-
简单易用,适用于较小规模的应用程序。
-
不需要修改现有组件即可添加新的监听器。
-
-
缺点:
-
随着应用规模的增长,事件名称可能会变得难以管理和追踪。
-
可能导致组件间的耦合度增加。
-
依赖注入
-
优点:
-
更好的组织性和可维护性,因为依赖关系是显式的。
-
适用于需要在多个组件间共享数据和服务的情况。
-
支持树状结构中的组件通信,无需直接父子关系。
-
-
缺点:
-
对于简单的通信场景可能显得过于复杂。
-
如果过度使用,可能会导致组件之间过于紧密的耦合。
-
结言
-
对于简单的跨组件通信,可以考虑使用中央事件总线。
-
对于更复杂的通信需求,依赖注入提供了更好的组织性和可维护性。