环信 uni-app Demo升级改造计划——Vue2迁移到Vue3(一)

news2024/12/27 22:18:33

前言

由于环信uni-app Demo 为早期通过工具从微信小程序转换为的 uni-app 项目,经过实际的使用以及复用反馈,目前已经不适用于当前的开发使用,因此开启了整体升级改造计划,目前一期计划将 vue2 代码进行手动转换为 vue3+vite,并剔除原项目中已经无用的项目代码,下面记录一下升级操作,如果升级过程,对大家有所帮助,深感荣幸~

前期准备

  • 【重要】阅读 uni-app 官网文档 Vue2 升级 Vue3 指南文档地址
  • 调研迁移到 Vue3 中原有的 Demo 中哪些三方库或者方法将不可用主要 uview UI 库不支持 Vue3)。
  • 下载并运行环信官网 uni-app 项目(原项目master分支)。Demo下载地址
  • 在 HubilderX 中创建容器项目所谓容器项目即为创建一个空白的 Vue3 模板,用以逐步将 Vue2 的项目代码逐步挪到此项目中。
  • 在空白项目中引入 uni-ui 组件,主要为了使用其组件替换原项目 uviewUI 组件
  • 确认升级流程以及方式本次升级采用渐进式语法修改形式,主要方式为迁移一个组件则将修改一个组件的语法为 vue3,如该组件依赖多个组件则先切断相组件的连接注释大法,后续逐步放开并配套修改。

核心迁移步骤

第一步、导入环信 uni-app SDK

原有 Vue2 版本 uni-app-demo 项目为本地引入 SDK 包,对于有些习惯 npm 安装导入的同学不太友好,目前 uniSDK 已经支持 npm 安装并导入,因此将原有本地引入 js 文件改为通过 npm 安装 SDK 并 import 导入 SDK。

//第一步 打开终端执行 npm install easemob-websdk
//第二步 复制原demo中的utils文件夹至空白项目中
//第三步 找到utils文件夹中的WebIM.js 文件中的导入SDK方式改写为impot 导入 easemob-websdk/uniApp包,具体代码如下。
/* 原项目引入SDK代码 */
import websdk from '../newSDK/uniapp-sdk-4.1.2';
/* 改写后的代码 */
import websdk from 'easemob-websdk/uniApp/Easemob-chat';

第二步、CommonJS 导入导出改写为 ESM

这种改写原因两点:

1、CommonJS 规范在 Vite 中使用本身并不支持,如果支持则需要进行单独配置。

2、原始项目中既有 CommonJS 导入方式,也有 ESM 导入,借此机会进行统一。

进行到此主要是先将原始项目中的 CommonJS 导出 WebIM 实例改为 ESM 导出,后续会在语法改造过程中将所有 CommonJS 规范改写为 ESM 导出,后续将不在本文中提及,实例代码如下

/* 原始项目utils/WebIM.js的导入导出WebIM实例代码段 */
//导入方式
let WebIM = (wx.WebIM = require('./utils/WebIM')['default']);
//导出方式
module.exports = {
  default: WebIM,
};

/* 改写后导入导出 */
//导入方式
import WebIM from '@/utils/WebIM.js';
//导出方式
export default WebIM;

第三步、迁入 App.vue 组件

完整的复制原始项目中的 App.vue 组件(uni 的 Vue3 模板中也支持 Vue2 代码,因此可以放心进行 CV)

App.vue 组件涉及到的改动为注释掉暂时没有引入的 js 文件,后续进行引入,去除 scss 中的 uview 样式代码,引入后续将要完全剔除 uview 组件。

App.vue 中代码较多此示例做了大量的缩减,大致调整之后的结构如下。

<script>
import WebIM from '@/utils/WebIM.js';
//这些导入暂时注释,后续再进行引入
//let msgStorage = require("./components/chat/msgstorage");
//let msgType = require("./components/chat/msgtype");
//let disp = require("./utils/broadcast");
//let logout = false;

//import { onGetSilentConfig } from './components/chat/pushStorage'
export default {
//export default的代码块原封不动,此处先进行了删除,实际迁入不用动。
    data (){
        return {

        }
    }
}
</script>
<style lang="scss">
@import './app.css';
 /*注意这行代码删除 @import "uview-ui/index.scss"; */
</style>

第四步 牛刀小试~ 迁入 Login 组件

先迁入一个 Login 组件热热身,毕竟从登录开始,原始项目中有注册、Token 登录、等等但目前暂不需要所以只需迁入 Login 组件。

在迁入前我们先了解并思考一下,Vue2 的 Options API 与 Vue3 Composition API 一些特点,主要目的是用较小的代价进行 Vue3 语法改造。
Vue3 模版支持 setup 语法糖,因此可以直接使用使用 setup 语法糖方式进行语法改造。

<script setup>
    /* 原始代码片段 */
    let WebIM = require("../../utils/WebIM")["default"];
    let __test_account__, __test_psword__;
    let disp = require("../../utils/broadcast");
    data() {
        return {
          usePwdLogin:false, //是否用户名+手机号方式登录
          name: "",
          psd: "",
          grant_type: "password",
          psdFocus: "",
          nameFocus: "",
          showPassword:false,
          type:'text',
              btnText: '获取验证码'
        };
      },
      /* 改造后的代码 */
    //使用reactive替换并包裹原有data中的参数
    import { reactive } from 'vue'
    import disp from '@/utils/broadcast.js'; //修改为ESM导入
    const WebIM = uni.WebIM; //从挂载到uni下的WebIM中取出WebIM并赋值用以替换原有单独require导入的WebIM
    const loginState = reactive({
      usePwdLogin: true, //是否用户名+手机号方式登录
      name: '',
      psd: '',
      grant_type: 'password',
      psdFocus: '',
      nameFocus: '',
      showPassword: false,
      type: 'text',
      btnText: '获取验证码',
    });

    //methods中的方法提取到外层中,例如将login 登录IM进行调整
    //登录IM
const loginIM = () => {
  runAnimation = !runAnimation;
  if (!loginState.usePwdLogin) {
    if (!__test_account__ && loginState.name == '') {
      uni.showToast({
        title: '请输入手机号!',
        icon: 'none',
      });
      return;
    } else if (!__test_account__ && loginState.psd == '') {
      uni.showToast({
        title: '请输入验证码!',
        icon: 'none',
      });
      return;
    }
    const that = loginState;
    uni.request({
      url: 'https://a1.easemob.com/inside/app/user/login/V2',
      header: {
        'content-type': 'application/json',
      },
      method: 'POST',
      data: {
        phoneNumber: that.name,
        smsCode: that.psd,
      },
      success(res) {
        if (res.statusCode == 200) {
          const { phoneNumber, token, chatUserName } = res.data;
          getApp().globalData.conn.open({
            user: chatUserName,
            accessToken: token,
          });
          getApp().globalData.phoneNumber = phoneNumber;
          uni.setStorage({
            key: 'myUsername',
            data: chatUserName,
          });
        } else if (res.statusCode == 400) {
          if (res.data.errorInfo) {
            switch (res.data.errorInfo) {
              case 'UserId password error.':
                uni.showToast({
                  title: '用户名或密码错误!',
                  icon: 'none',
                });
                break;
              case 'phone number illegal':
                uni.showToast({
                  title: '请输入正确的手机号',
                  icon: 'none',
                });
                break;
              case 'SMS verification code error.':
                uni.showToast({
                  title: '验证码错误',
                  icon: 'none',
                });
                break;
              case 'Sms code cannot be empty':
                uni.showToast({
                  title: '验证码不能为空',
                  icon: 'none',
                });
                break;
              case 'Please send SMS to get mobile phone verification code.':
                uni.showToast({
                  title: '请使用短信验证码登录',
                  icon: 'none',
                });
                break;
              default:
                uni.showToast({
                  title: res.data.errorInfo,
                  icon: 'none',
                });
                break;
            }
          }
        } else {
          uni.showToast({
            title: '登录失败!',
            icon: 'none',
          });
        }
      },
      fail(error) {
        uni.showToast({
          title: '登录失败!',
          icon: 'none',
        });
      },
    });
  } else {
    if (!__test_account__ && loginState.name == '') {
      uni.showToast({
        title: '请输入用户名!',
        icon: 'none',
      });
      return;
    } else if (!__test_account__ && loginState.psd == '') {
      uni.showToast({
        title: '请输入密码!',
        icon: 'none',
      });
      return;
    }
    uni.setStorage({
      key: 'myUsername',
      data: __test_account__ || loginState.name.toLowerCase(),
    });
    console.log(111, {
      apiUrl: WebIM.config.apiURL,
      user: __test_account__ || loginState.name.toLowerCase(),
      pwd: __test_psword__ || loginState.psd,
      grant_type: loginState.grant_type,
      appKey: WebIM.config.appkey,
    });
    getApp().globalData.conn.open({
      apiUrl: WebIM.config.apiURL,
      user: __test_account__ || loginState.name.toLowerCase(),
      pwd: __test_psword__ || loginState.psd,
      grant_type: loginState.grant_type,
      appKey: WebIM.config.appkey,
    });
  }
};
</script>

改造中会遇到了原 Vue2 中原 data 部分参数通过使用 reactive 包裹并重命名,需要注意把语法中的 this.、me.、this.setData 进行替换为包裹后的 state 命名,另外 template 中也要同步进行替换,这一点在后续所有组件改造中都会遇到。

Login 组件需要 page.json 中进行路由的配置,只有配置成功之后我们方可运行项目并展示页面!

此时就可以启动项目运行观察一下看看页面是否可以正常的进行展示,当然是运行到小程序还是 H5 以及 App 上自行选择。

第五步、 迁入“Home 页中的”三个 Tab 页面【conversation 会话列表,mian 联系人页、Setting 我的页面】

迁移各组件,此处使用 conversation 组件作为示例,其余两个组件完全相同的步骤,全部示例代码将在文章末尾给出地址。

在原项目中包括已迁移进来的 App.vue 组件中有下面这样一个方法,其作用即为环信 IM 连接成功之后触发 onOpened 该监听回调,进行路由跳转进入到会话页面,因此不难理解,open 之后首个跳转的页面即为 conversation。

    onLoginSuccess: function (myName) {
      uni.hideLoading();
      uni.redirectTo({
        url: "../conversation/conversation?myName=" + myName,
      });
    },
  • 在原始项目中 copy conversation(会话)组件至容器项目相同目录下,另外不要忘记顺手在 page.json 下配置路由。

  • 开始改写会话组件中的代码

//script 标签增加 setup 使其支持setup语法糖
<script setup>
    /* 引入所需组合式API */
    //computed 用以替换options API中的计算属性,Vue3中计算属性使用略有差异。
    import {reactive,computed} from 'vue'
    /* 引入所需声明周期钩子函数替换原有钩子函数,该写法uni-appvue2升级vue3指南有提及 */
    import { onLoad, onShow, onUnload } from '@dcloudio/uni-app';
    /* 调整disp为import导入 */
    // let disp = require("../../utils/broadcast");
    import disp from '@/utils/broadcast';
    /* 调整WebIM引入直接从uni下取 */
    // var WebIM = require("../../utils/WebIM")["default"];
    const WebIM = uni.WebIM
    let isfirstTime = true;
    /* components中的组件暂时注释,template中的组件引入也暂时注释,
     * 另options API中的components中的组件注册也暂时注释
    */
    // import swipeDelete from "../../components/swipedelete/swipedelete";
    // import longPressModal from "../../components/longPressModal/index";

    /* data 提出用reactive包裹并命名 */
    const conversationState = reactive({
          // 内容省略...
    });

    /* onLoad替换 */
    onLoad(() => {
      //所有通过this. 进行方法方法调用全部删除
      disp.on('em.subscribe', onChatPageSubscribe);
      //监听解散群
      disp.on('em.invite.deleteGroup', onChatPageDeleteGroup);
      //监听未读消息数
      disp.on('em.unreadspot', onChatPageUnreadspot);
      //监听未读加群“通知”
      disp.on('em.invite.joingroup', onChatPageJoingroup);
      //监听好友删除
      disp.on('em.contacts.remove', onChatPageRemoveContacts);
      //监听好友关系解除
      disp.on('em.unsubscribed', onChatPageUnsubscribed);
      if (!uni.getStorageSync('listGroup')) {
        listGroups();
      }
      if (!uni.getStorageSync('member')) {
        getRoster();
      }
      readJoinedGroupName();
    });
    /* onShow替换 */
    onShow(() => {
      uni.hideHomeButton && uni.hideHomeButton();
      setTimeout(() => {
        getLocalConversationlist();
      }, 100);
      conversationState.unReadMessageNum =
        getApp().globalData.unReadMessageNum > 99
          ? '99+'
          : getApp().globalData.unReadMessageNum;
      conversationState.messageNum = getApp().globalData.saveFriendList.length;
      conversationState.unReadNoticeNum =
        getApp().globalData.saveGroupInvitedList.length;
      conversationState.unReadTotalNotNum =
        getApp().globalData.saveFriendList.length +
        getApp().globalData.saveGroupInvitedList.length;
      if (getApp().globalData.isIPX) {
        conversationState.isIPX = true;
      }
    });
    /* 计算属性改写 */
        const showConversationName = computed(() => {
          const friendUserInfoMap = getApp().globalData.friendUserInfoMap;
          return (item) => {
            if (item.chatType === 'singleChat' || item.chatType === 'chat') {
              if (
                friendUserInfoMap.has(item.username) &&
                friendUserInfoMap.get(item.username)?.nickname
              ) {
                return friendUserInfoMap.get(item.username).nickname;
              } else {
                return item.username;
              }
            } else if (
              item.chatType === msgtype.chatType.GROUP_CHAT ||
              item.chatType === msgtype.chatType.CHAT_ROOM
            ) {
              return item.groupName;
            }
          };
        });
        const handleTime = computed(() => {
          return (item) => {
            return dateFormater('MM/DD/HH:mm', item.time);
          };
        });
  /* 将methods中方法全量提取到外层与onLoad onShow等API平级 */
      const listGroups = () => {
          return uni.WebIM.conn.getGroup({
            limit: 50,
            success: function (res) {
              uni.setStorage({
                key: 'listGroup',
                data: res.data,
              });
              readJoinedGroupName();
              getLocalConversationlist();
            },
            error: function (err) {
              console.log(err);
            },
          });
    };

    const getRoster = async () => {
      const { data } = await WebIM.conn.getContacts();
      if (data.length) {
        uni.setStorage({
          key: 'member',
          data: [...data],
        });
        conversationState.member = [...data];
        //if(!systemReady){
        disp.fire('em.main.ready');
        //systemReady = true;
        //}
        getLocalConversationlist();
        conversationState.unReadSpotNum =
          getApp().globalData.unReadMessageNum > 99
            ? '99+'
            : getApp().globalData.unReadMessageNum;
      }
      console.log('>>>>好友列表获取成功', data);
    };
    const readJoinedGroupName = () => {
      const joinedGroupList = uni.getStorageSync('listGroup');
      const groupList = joinedGroupList?.data || joinedGroupList || [];
      let groupName = {};
      groupList.forEach((item) => {
        groupName[item.groupid] = item.groupname;
      });
      conversationState.groupName = groupName;
    };

    //还有很多方法就不一一展示,暂时进行了省略...
    /* onUnload */
    onUnload(() => {
      //页面卸载同步取消onload中的订阅,防止重复订阅事件。
      disp.off('em.subscribe', conversationState.onChatPageSubscribe);
      disp.off('em.invite.deleteGroup', conversationState.onChatPageDeleteGroup);
      disp.off('em.unreadspot', conversationState.onChatPageUnreadspot);
      disp.off('em.invite.joingroup', conversationState.onChatPageJoingroup);
      disp.off('em.contacts.remove', conversationState.onChatPageRemoveContacts);
      disp.off('em.unsubscribed', conversationState.onChatPageUnsubscribed);
    });
</script

在做这三个组件迁移的时候主要的注意事项为,this 的替换,template 中的默认从 vue2 中 data 取的参数也要替换为被 reactive 包裹后的变量名。

启动运行调整

建议迁移一个组件调试一个组件,运行到 H5 端,从登录页面登录进去,并点击三个页面进行切换,观察是否有相应的报错,发现即进行修改并重新运行测试。

第六步、迁入复杂度最高的聊天相关组件。

以单聊作为说明示例:

1)迁入单聊入口组件[pages/chatroom]

chatroom 组件(groupChatroom 作用相同)为单聊功能聊天的入口组件,pages 中其他组件发起单聊聊天时均会跳转至该组件,而该组件同时又承载 components 下的 chat 组件作为容器形成聊天功能。

将 chatroom 组件 copy 至容器项目 pages 下并配置路由映射,为了语义化将 chatroom 更名为 singleChatEntry,并进行语法改造,此时 singleChatEntry 如下:

不要忘了,路由路径配套也要从 chatroom 更名为 singleChatEntry

<template>
  <chat
    id="chat"
    ref="chatComp"
    :chatParams="chatParams"
    chatType="singleChat"
  ></chat>
</template>

<script setup>
import { ref, reactive } from 'vue';
import {
  onLoad,
  onUnload,
  onPullDownRefresh,
  onNavigationBarButtonTap,
} from '@dcloudio/uni-app';
import disp from '@/utils/broadcast';
import chat from '@/components/chat/chat.vue';

const chatComp = ref(null);
let chatParams = reactive({});
onNavigationBarButtonTap(() => {
  uni.navigateTo({
    url: `/pages/moreMenu/moreMenu?username=${chatParams.your}&type=singleChat`,
  });
});
onLoad((options) => {
  let params = JSON.parse(options.username);
  chatParams = Object.assign(chatParams, params);
  // 生成的支付宝小程序在onLoad里获取不到,这里放到全局变量下
  uni.username = params;
  uni.setNavigationBarTitle({
    title: params?.yourNickName || params?.your,
  });
});
onPullDownRefresh(() => {
  uni.showNavigationBarLoading();
  chatComp.value.getMore();
  // 停止下拉动作
  uni.hideNavigationBarLoading();
  uni.stopPullDownRefresh();
});

onUnload(() => {
  disp.fire('em.chatroom.leave');
});
</script>
<style>
    @import './singleChatEntry.css';
</style>

2)完整迁入 components 组件

image.png

components 组件结构如上图,由于音视频功能已经废弃本次迁移决定剔除,但目前迁移方案采取“抓大放小,后续清算”的策略先一起迁入,后续剔除。

引入之后运行起来之后会发现有很多 require not a function 字眼的错误,同样我们要将所有 CommonJS 的导出修改为 ESM 导出,剩下的则是一点一点的去进行语法改造,整个 chat 下其实涉及组件非常多,因为 IM 所有消息的收发,以及渲染均囊括在此组件。

这里提一下 msgpackager.js、msgstorage.js、msgtype.js、pushStorage.js 几个 js 文件的作用。

msgpackager.js 主要为将收发的IM消息进行结构重组

msgstorage.js 将收发消息进行本地缓存

msgtype.js 消息类型以及聊天类型的常量文件

pushStorage.js 推送处理相关

迁入进去之后将开始针对大大小小十几个文件进行语法以及引入改造,另外其中个别文件还牵扯到使用的 uviewUI 那么则需要进行重写,最终经过改造以及剔除不再使用的组件以及音视频相关代码之后,结构如图:
image.png

有一点较为基础但是还是要强调注意的事项要提一下,在 components/chat 下的组件改造中经常出现父子组件的调用,那么父组件在使用子组件的方法的时候,由于 Vue3 中不能再通过类似$ref 直接去调用子组件中的方法或者值,子组件需要通过 defineExpose 主动进行暴露方可使用,这个需要进行注意。

迁移中发现 H5 的录音采用的 recorder-core.js 库,js 按需导入中有用到 require,那么需要改写为 import 导入,但是发现实例化时发现依然不是一个构造函数,通过改写从 window 下访问即正常使用,相关代码如下:

    /* 原代码片段 */
    handleRecording(e) {
      const sysInfo = uni.getSystemInfoSync();
      console.log("getSystemInfoSync", sysInfo);
      if (sysInfo.app === "alipay") {
        // https://forum.alipay.com/mini-app/post/7301031?ant_source=opendoc_recommend
        uni.showModal({
          content: "支付宝小程序不支持语音消息,请查看支付宝相关api了解详情"
        });
        return;
      }
      let me = this;
      me.recordClicked = true;
      // h5不支持uni.getRecorderManager, 需要单独处理
      if (sysInfo.uniPlatform === "web") {
        import("../../../../../recorderCore/src/recorder-core").then((Recorder) => {
          require("../../../../../recorderCore/src/engine/mp3");
          require("../../../../../recorderCore/src/engine/mp3-engine");
          if (me.recordClicked == true) {
            clearInterval(recordTimeInterval);
            me.initStartRecord(e);
            me.rec = new Recorder.default({
              type: "mp3"
            });
            me.rec.open(
              () => {
                me.saveRecordTime();
                me.rec.start();
              },
              (msg, isUserNotAllow) => {
                if (isUserNotAllow) {
                  uni.showToast({
                    title: "鉴权失败,请重试",
                    icon: "none"
                  });
                } else {
                  uni.showToast({
                    title: `开启失败,请重试`,
                    icon: "none"
                  });
                }
              }
            );
          }
        });
      } else {
        setTimeout(() => {
          if (me.recordClicked == true) {
            me.executeRecord(e);
          }
        }, 350);
      }
    }
    /* 调整后代码片段 */
    const handleRecording = async (e) => {
      const sysInfo = uni.getSystemInfoSync();
      console.log('getSystemInfoSync', sysInfo);
      if (sysInfo.app === 'alipay') {
        // https://forum.alipay.com/mini-app/post/7301031?ant_source=opendoc_recommend
        uni.showModal({
          content: '支付宝小程序不支持语音消息,请查看支付宝相关api了解详情',
        });
        return;
      }
      audioState.recordClicked = true;
      // h5不支持uni.getRecorderManager, 需要单独处理
      if (sysInfo.uniPlatform === 'web') {
        // console.log('>>>>>>进入了web层面注册页面');
        // #ifdef H5
        await import('@/recorderCore/src/recorder-core');
        await import('@/recorderCore/src/engine/mp3');
        await import('@/recorderCore/src/engine/mp3-engine');
        if (audioState.recordClicked == true) {
          clearInterval(recordTimeInterval);
          initStartRecord(e);
          audioState.rec = new window.Recorder({
            type: 'mp3',
          });
          audioState.rec.open(
            () => {
              saveRecordTime();
              audioState.rec.start();
            },
            (msg, isUserNotAllow) => {
              if (isUserNotAllow) {
                uni.showToast({
                  title: '鉴权失败,请重试',
                  icon: 'none',
                });
              } else {
                uni.showToast({
                  title: `开启失败,请重试`,
                  icon: 'none',
                });
              }
            }
          );
        }
        // #endif
      } else {
        setTimeout(() => {
          if (audioState.recordClicked == true) {
            executeRecord(e);
          }
        }, 350);
      }
};

3)启动进行后续调整测试

启动之后验证发现更多的是一些细节问题,同样边改边验证。

后续总结

在首期迁移 vue2 升级 vue3 的工作中其实难度并没有很大,主要的工作量集中在语法的修改变更上,好在 uni-app 中可以同步去写 vue2 与 vue3 两种语法代码,这样有助于在引入之后陆续进行语法变更,另外迁移之后开发体验启动速度确实快了很多,接下来就可以腾出手针对 uni-app-demo 源码代码进行整体质量提升,敬请期待…

此次升级后的源码地址:https://github.com/easemob/webim-uniapp-demo/tree/vue3

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

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

相关文章

MySQL高级第十四篇:锁机制分类详解(表锁,行锁,页锁,悲观锁和乐观锁)

MySQL高级第十四篇&#xff1a;锁机制分类详解&#xff08;表锁&#xff0c;行锁&#xff0c;页锁&#xff0c;悲观锁和乐观锁&#xff09; 一、概述二、MySQL并发事物访问相同记录的几种情况&#xff1f;1. 读--读情况2. 写--写情况3. 读--写情况&#xff08;写--读情况&#…

大数据项目实战之数据仓库:电商数据仓库系统——第8章 数仓开发之DIM层

文章目录 第8章 数仓开发之DIM层8.1 商品维度表8.2 优惠券维度表8.3 活动维度表8.4 地区维度表8.5 日期维度表8.6 用户维度表8.7 数据装载脚本8.7.1 首日装载脚本8.7.2 每日装载脚本 第8章 数仓开发之DIM层 DIM层设计要点&#xff1a; &#xff08;1&#xff09;DIM层的设计依…

解决 Microsoft Edge Dev 版本中右上角的 bing 按钮消失的问题 让 New Bing 还能阅读分析文档!

Microsoft Edge Dev 右上角的必应图标消失了&#xff0c;使得无法用 New Bing 阅读分析文档&#xff0c;到底什么原因呢&#xff1f; 针对 Microsoft Edge Dev 版本中右上角的发现按钮消失的问题&#xff0c;网上搜索解决方案。发现也有一些用户反馈在更新 Microsoft Edge Dev …

【C++STL精讲】优先级队列(priority_queue)与双端队列(deque)

文章目录 &#x1f490;专栏导读&#x1f490;文章导读&#x1f337;优先级队列——priority_queue&#x1f338;什么是优先级队列&#xff1f;&#x1f338;优先级队列的基本使用&#x1f338;什么是仿函数&#xff1f;&#x1f338;优先级队列的模拟实现 &#x1f337;双端队…

本地Pycharm连接远程服务器训练模型教程-yolov5为例

本篇文章解决的问题&#xff1a; 本地pycharm 与云服务器/实验室服务器进行远程连接跑实验训练、同步本地与云服务器的全部或者部分文件。 在这之前需要做的的工作&#xff1a; 1.服务器上已经创建好虚拟环境&#xff08;不会的可以看下篇文章&#xff09;&#xff1a;使用云…

git commit三种回退的方式

git commit 回退 弄清楚三个区 工作区&#xff08;working tree&#xff09;&#xff1a; 本地编辑器 暂存区&#xff08;index&#xff09;&#xff1a;git add操作后进入暂存区&#xff0c;可用git status查看 本地仓库&#xff08;repository&#xff09;&#xff1a;git …

C#上位机与三菱FX3UPLC实现异步伪实时串口通信机制(串口类通信可参考)

C#上位机与三菱FX3UPLC实现异步伪实时串口通信机制&#xff08;串口类通信可参考&#xff09; 一、串口通信概述1.1 串口通信1.2 串行通信1.2.1 串行同步通信1.2.2 串行异步通信1.2.2.1 异步通信的数据格式1.2.2.2 异步通信的数据发送过程1.2.2.3 异步通信的数据接收过程 1.3 串…

Redis如何保障缓存与数据库的数据一致性问题?

目录 一.最经典的数据库加缓存的双写双删模式 二. 高并发场景下的缓存数据库双写不一致问题分析与解决方案设计 三、上面高并发的场景下&#xff0c;该解决方案要注意的问题 一.最经典的数据库加缓存的双写双删模式 1.1 Cache Aside Pattern概念以及读写逻辑 &#xff08;…

redis非关系型数据库部署和使用(linux)

1.概念 NoSQL非关系型数据库是一种不使用关系模型来组织数据的数据库&#xff0c;通常用于存储非结构化或半结构化的数据&#xff0c;不支持或只部分支持SQL语言&#xff0c;满足最终一致性。非关系型数据库有多种类型&#xff0c;例如键值数据库、文档数据库、列式数据库、图形…

Shopee、Grab、Gojek 打造超级app已成为主流

超级App的概念在全球范围内逐渐被接受和采用。 超级App是指一种综合性的应用程序&#xff0c;允许用户在同一个平台上访问多个不同的服务&#xff0c;包括支付、社交媒体、出行、点餐等等。它的发源地是东南亚地区&#xff0c;如中国的微信、印度的Paytm和印尼的Gojek等应用&a…

Spring入门案例--bean的生命周期

bean的生命周期 关于bean的相关知识还有最后一个是bean的生命周期,对于生命周期&#xff0c;我们主要围绕着bean生命周期控 制 来讲解: 首先理解下什么是生命周期? 从创建到消亡的完整过程,例如人从出生到死亡的整个过程就是一个生命周期。 bean生命周期是什么? bean对…

C++ | 说说类中的static成员

【概念】&#xff1a;声明为static的类成员称为类的静态成员&#xff0c;用static修饰的成员变量&#xff0c;称之为静态成员变量&#xff1b;用static修饰的成员函数&#xff0c;称之为静态成员函数。静态成员变量一定要在类外进行初始化 文章目录 一、面试题引入二、static特…

5个实用的JavaScript原生API

本文带来5个难得一见的JavaScript原生API&#xff0c;为我们的前端开发带来意想不到的便利。 1. getBoundingClientRect() Element.getBoundingClientRect() 方法返回一个 DOMRect 对象&#xff0c;该对象提供有关元素大小及其相对于视口的位置的信息。 domRect element.ge…

Java笔记_11(常用API)

Java笔记_11 一、常用的API1.1、MathMath练习 1.2、System1.3、Runtime1.4、Object1.5、浅克隆、深克隆1.6、对象工具类的Objects1.7、BigInteger&#xff08;大整数&#xff09;1.8、BigDecimal&#xff08;大小数&#xff09; 二、正则表达式2.1、正则表达式基础知识2.2、正则…

关于WordPress的20个有趣事实

时值 2022 年&#xff0c;互联网格局和 WordPress 的流行发生了重大变化。COVID-19 流行几乎影响到人类生存的方方面面&#xff0c;包括我们的互联网习惯&#xff0c;这也不例外。 到 2022 年&#xff0c;我们在家工作的人数显着增加&#xff0c;下岗或发现自己有更多空闲时间…

Python基础实战3-Pycharm安装简介

Pycharm下载、安装与使用 1.打开pycharm官网&#xff1a;下载 PyCharm&#xff1a; Python IDE for Professional Developers by JetBrains 2.选择自己对应的操作系统&#xff0c;点击Download&#xff0c;默认是最新版本&#xff0c;想安装其他版本可以选择Other versions下载…

【iOS】—— Masonry源码学习(浅看,未完)

Masonry 文章目录 MasonryNSLayoutConstraint用法Masonry源码 Masonry在我们之前的学习中是一个非常有用的第三方库。 Masonry是一种基于Objective-C语言的轻量级布局框架&#xff0c;它可以简化iOS应用程序中的自动布局任务。Masonry提供了一个方便的API&#xff0c;可以编写更…

Kubernetes Service、Ingress

Service&#xff08;4层负载均衡器&#xff09; 1、K8S 可以保证任意 Pod 挂掉时自动从任意节点启动一个新的Pod进行代替&#xff0c;以及某个Pod超负载时动态对Pod进行扩容。每当 Pod 发生变化时其 IP地址也会发生变化&#xff0c;且Pod只有在K8S集群内部才可以被访问&#xf…

Flink高手之路4-Flink流批一体

文章目录 Flink高手之路4-Flink流批一体API开发一、流批一体相关的概念1.数据的时效性2.流处理和批处理1)批处理2)流处理3)两者对比 3.流批一体API4.流批一体的编程模型 二、Data Source1.预定义的Source1)基于集合的Sources(1)API(2)演示 2)基于文件的Source(1)API(2)演示 3)基…

2023.4.19 + 4.20

文章目录 String类1&#xff1a;介绍&#xff1a;2&#xff1a;String类实现了很多的接口&#xff1a;3&#xff1b;String类常用构造器4&#xff1a;不同方式创建String类对象的区别&#xff08;1&#xff09;直接赋值的方式&#xff08;2&#xff09;常规new的方式&#xff0…