依赖注入+中央事件总线:Vue 3组件通信新玩法

news2024/9/20 9:47:54

​🌈个人主页:前端青山
🔥系列专栏: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,在onMountedonBeforeUnmount监听和移出事件总线

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方法(已暴露)
}

总结

中央事件总线
  • 优点

    • 简单易用,适用于较小规模的应用程序。

    • 不需要修改现有组件即可添加新的监听器。

  • 缺点

    • 随着应用规模的增长,事件名称可能会变得难以管理和追踪。

    • 可能导致组件间的耦合度增加。

依赖注入
  • 优点

    • 更好的组织性和可维护性,因为依赖关系是显式的。

    • 适用于需要在多个组件间共享数据和服务的情况。

    • 支持树状结构中的组件通信,无需直接父子关系。

  • 缺点

    • 对于简单的通信场景可能显得过于复杂。

    • 如果过度使用,可能会导致组件之间过于紧密的耦合。

结言
  • 对于简单的跨组件通信,可以考虑使用中央事件总线。

  • 对于更复杂的通信需求,依赖注入提供了更好的组织性和可维护性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2041454.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Prometheus部署和基本操作

1 项目目标 &#xff08;1&#xff09;对Prometheus有基本的了解 &#xff08;2&#xff09;能够部署出一套Prometheus看板系统 &#xff08;3&#xff09;对Prometheus界面熟悉 1.1 规划节点 主机名 主机IP 节点规划 prome-master01 10.0.1.10 服务端 prome-node01 …

java基础学习笔记1

Java编程规范 命名风格 1. 【强制】代码中的命名均不能以下划线或美元符号开始&#xff0c;也不能以下划线或美元符号结束。 反例&#xff1a;_name / __name / $name / name_ / name$ / name__ 2. 【强制】代码中的命名严禁使用拼音与英文混合的方式&#xff0c;更不允许直…

社交媒体分析:如何利用Facebook的数据提升业务决

在数字化时代&#xff0c;社交媒体已经成为企业战略中不可或缺的一部分。Facebook&#xff0c;作为全球最大的社交平台之一&#xff0c;提供了丰富的数据资源&#xff0c;这些数据不仅能够帮助企业了解市场趋势&#xff0c;还能提升业务决策的精准度。本文将探讨如何有效利用Fa…

CV党福音:YOLOv8实现实例分割(一)

前面我们得知YOLOv8不但可以实现目标检测任务&#xff0c;还包揽了分类、分割、姿态估计等计算机视觉任务。在上一篇博文中&#xff0c;博主已经介绍了YOLOv8如何实现分类&#xff0c;在这篇博文里&#xff0c;博主将介绍其如何将实例分割给收入囊中。 YOLOv8实例分割架构图 …

Spring Boot3.3.X整合Mybatis-Plus

前提说明&#xff1a; 项目的springboot版本为&#xff1a;<version>3.3.2</version> 需要整合的mybatis-plus版本&#xff1a;<version>3.5.7</version> 废话不多说&#xff0c;开始造吧 1.准备好数据库和表 2.配置全局文件application.properti…

本地连接服务器redis

详细步骤 1.看一下服务器上redis实例的运行状态&#xff1a; [rootiZuf67k70ucx14s6zcv54dZ var]# ps aux | grep redis-server若显示&#xff1a; 则说明服务器上的redis已经启动了&#xff0c;若没有&#xff0c;则请重启一下&#xff1a; sudo systemctl restart redis…

原来,考证还可以领取补贴Money

武汉ZF真的对打工人太好了&#xff0c;只要社保交满 12 个月就可以参加职业技能考试&#xff0c;考试通过就能领 2K 的补贴。 而且证考了对找工作工资也能比别人高几百&#xff0c;真的太爽了&#xff0c;有空的姐妹都去给我考&#xff01;&#xff01;&#xff01; 没空的也给…

思科三层交换机实现EIGIP路由协议6

#路由协议实现# #任务六三层交换机实现EIGIP路由协议6# #1配置计算机的IP地址、子网掩码和网关 #2配置Switch-A的名称及其接口IP地址 Switch(config)#hostname Switch-A Switch-A(config)#ip routing Switch-A(config)#int g0/1 Switch-A(config-if)#no switchport Switc…

docker pull实现断点续传

问题背景 在使用Docker拉取DockerHub的镜像时&#xff0c;经常会出现网络不稳定的问题&#xff0c;这就导致拉取到一半的镜像会重新拉取&#xff0c;浪费时间。例如下面这种情况&#xff1a; 第二次拉取 这是一个网络中断的场景&#xff0c;第二次重新拉取的时候&#xff0c;同…

电子元件-潮湿敏感度MSL等级

目录&#xff1a; 1、什么是MSL 2、MSL测定的流程 3、MSL的分类★ 4、其他 1、什么是MSL MSL&#xff1a;MSL 是 Moisture Sensitivity Level 的缩写&#xff0c;是湿气敏感性等级的意思。 MSL 的提出就是为了给湿度敏感性 SMD 元件的封装提供一种分类标准&#xff0c;从而…

UE虚幻引擎可以云渲染吗?应用趋势与挑战了解

虚幻云渲染技术是基于虚幻引擎的云端渲染技术&#xff0c;将虚幻引擎的渲染计算任务通过云计算的方式进行处理和渲染、并将渲染结果传输到终端设备上进行展示。虚幻引擎云渲染技术在近年来得到了迅猛的发展&#xff0c;并在各个领域得到了广泛的应用&#xff0c;包括游戏、电影…

使用CompletableFuture遇到的一个小坑

在使用CompletableFuture时&#xff0c;发现获取数据时&#xff0c;有时候数据获取不到(值为null)。 代码如下&#xff1a; package com.example.mavendemo.completablefuture;import com.google.common.collect.Lists; import lombok.extern.slf4j.Slf4j;import java.util.A…

STM32第十二节(中级篇):串口通信(第一节)——功能框图讲解

前言 我们在51单片机中就已经学习过了串口通信的相关知识点&#xff0c;那么我们现在在32单片机上进一步学习通信的原理。我们主要讲解串口功能框图以及串口初始化结构体以及固件库讲解。 STM32第十二节&#xff08;中级篇&#xff09;&#xff1a;串口通信&#xff08;第一节…

免费的泛域名SSL证书如何申请

申请免费泛域名SSL证书的新指南 1. 选择合适的证书颁发机构 首先&#xff0c;寻找一个提供免费泛域名SSL证书的证书颁发机构&#xff08;CA&#xff09;。JoySSL是目前最知名的免费证书提供商之一&#xff0c;它支持泛域名证书&#xff0c;允许您为单个域名及其所有子域名提供…

【图像特效系列】图像的各种特效处理 | 图像素描特效的实践

目录 一 图像素描特效 实践① 实践② 图像特效系列主要是对输入的图像进行处理,生成指定特效效果的图片。图像素描特效会将图像的边界都凸显出来;图像怀旧特效是指图像经历岁月的昏暗效果;图像光照特效是指图像存在一个类似于灯光的光晕特效,图像像素值围绕光照中心点呈…

mac要装虚拟机吗

在Mac上安装虚拟机可以带来多种好处&#xff0c;‌但同时也存在一些潜在的影响。‌ 首先&#xff0c;‌虚拟机技术允许在同一设备上运行多个操作系统&#xff0c;‌这对于需要测试不同操作系统兼容性的开发者和IT专业人员来说非常有用。‌此外&#xff0c;‌虚拟机还能解决软件…

[星瞳科技]如何用OpenMV制造一个可以追小球的云台?

材料 你需要以下东西&#xff1a; OpenMV3D打印件 (在pantilt/stl下载)pcb固定板(pantilt/eagle下载)2个微型舵机一个锂电池 连接 按照上图连接。 资料下载 主要都在github上&#xff0c;可以点击查看下载&#xff1a;OpenMV-Pan-Tilt/pan-tilt/src at master SingTown/…

苹果手机qq文件怎么恢复?4个方法快速搞定

在日常生活中&#xff0c;我们经常通过QQ来传输和接收各种文件&#xff0c;无论是工作文档还是珍贵的照片&#xff0c;这些文件都承载着重要的信息和记忆。 然而&#xff0c;我们有时会不小心删除了这些宝贵的QQ文件。面对这样的困境&#xff0c;QQ文件怎么恢复&#xff1f;本…

轻空间成功承建上海浦东川沙镇气膜游泳馆

近期&#xff0c;轻空间&#xff08;江苏&#xff09;膜结构科技有限公司顺利完成了上海浦东川沙镇气膜游泳馆的建设工作。作为轻空间在体育设施领域的又一重要项目&#xff0c;这一游泳馆的成功落成&#xff0c;充分展现了轻空间在气膜结构建筑设计与施工方面的卓越技术和丰富…

W31-02-excel和logging使用,实现自动登录百度,并搜索雷军

接上文&#xff1a;W31-01&#xff0c;本文改为使用excel驱动实现数据驱动维护自动化数据&#xff0c;实现数据与代码分离&#xff0c;主要知识点如下&#xff1a; 1.使用excel相关功能&#xff1a; pip install openyxl -i https://pypi.tuna.tsinghua.edu.cn/simple 2.反…