【开箱即用】开发了一个基于环信IM聊天室的Vue3插件,从而快速实现仿直播间聊天窗功能

news2024/12/29 9:37:51

前言

由于看到有部分的需求为在页面层,快速的引入一个包,并且以简单的配置,就可以快速实现一个聊天窗口,因此尝试以 Vue3 插件的形式开发一个轻量的聊天窗口。

这次简单分享一下此插件的实现思路,以及实现过程,并描述一下本次插件发布 npm 的过程。

技术栈

  • Vue3
  • pnpm
  • Typescript
  • Vite

插件核心目录设计

📦 emchat-chatroom-widget
┣ 📂 build // 插件打包输出的目录
┣ 📂 demo // 验证插件demo相关目录
┣ 📂 scripts // 打包脚本目录
┣ 📂 src // 插件源代码
┃   ┣ 📂 components // 组件目录
┃   ┣ 📂 container // 容器组件目录
┃   ┣ 📂 EaseIM // 环信IM相关目录
┃   ┣ 📂 utils // 工具相关目录
┃   ┣ 📜 index.ts // 插件入口文件
┃   ┗ 📜 install.ts // 插件初始化文件
┣ 📜 package.json // 项目配置文件
┣ 📜 vite.config.ts // vite配置文件
┗ 📜 README.md // 项目说明文档
...

实现过程

确认功能范围

首先确认本次插件实现的功能范围,从而围绕要实现的功能着手进行开发准备。

  1. Vue3 框架使用
  2. 轻量配置、仅配置少量参数即可立即使用聊天功能
  3. 页面大小自适应,给定容器宽高,插件内部宽高自适应。
  4. 仅聊天室类型消息支持基础文本,表情,图片。
    暂时第一期仅支持这些功能范围。

着手开发

1、创建空白项目

pnpm create vite emchat-chatroom-widget --template vue-ts

2、配置eslint pretter 等代码校验、以及代码风格工具。

pnpm i eslint eslint-plugin-vue @typescript-eslint/eslint-plugin @typescript-eslint/parser -D
pnpm i prettier eslint-config-prettier eslint-plugin-prettier -D

同时也不要忘了创建对应的 .eslintrc.cjs.prettierrc.cjs

这里遇到了一个问题:

这几个文件以 cjs 结尾是因为 package.json 创建时设置了"type": "module" 后你的所有 js 文件默认使用 ESM 模块规范,不支持 commonjs 规范,所以必须显式的声明成 xxx.cjs 才能标识这个是用 commonjs 规范的,把你的配置都改成.cjs 后缀。

3、配置 scripts 打包脚本

目录下新建一个文件夹命名为scripts,新加一个 build.js 或者为.ts 文件。

在该文件中引入vite进行打包时的配置。由于本次插件编写时使用了jsx语法进行编写,因此 vite 打包时也需要引入 jsx 打包插件。
安装@vitejs/plugin-vue-jsx插件。

const BASE_VITE_CONFIG = defineConfig({
  publicDir: false, //暂不需要打包静态资源到public文件夹
  plugins: [
    vue(),
    vueJSX(),
    // visualizer({
    //   emitFile: true,
    //   filename: "stats.html"
    // }),
    dts({
      outputDir: './build/types',
      insertTypesEntry: true, // 插入TS 入口
      copyDtsFiles: true, // 是否将源码里的 .d.ts 文件复制到 outputDir
    }),
  ],
});

package.json中增加 build 脚本执行命令,

  "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build",
    "preview": "vite preview",
    "lint": "eslint src --fix",
    "build:widget": "node ./scripts/build.js"
  },

整体 build.js 代码由于篇幅关系,可以后面查看文末的源码地址。

4、 编写 Vue3 插件入口函数

import type { App } from 'vue';
import EasemobChatroom from './container';
import { initEMClient } from './EaseIM';
export interface IEeasemobOptions {
  appKey: string;
}

export default {
  install: (app: App, options: IEeasemobOptions) => {
    // 在这里编写插件代码
    console.log(app);
    console.log('options', options);
    if (options && options?.appKey) {
      initEMClient(options.appKey);
    } else {
      throw console.error('appKey不能为空');
    }
    app.component(EasemobChatroom.name, EasemobChatroom);
  },
};

5、聊天插件入口代码

聊天插件入口组件主要用来接收插件使用者所传递进来的一些必要参数,比如登录用户 id、密码、token、聊天室 id,以及针对初始化插件的初始状态。

import { defineComponent, onMounted } from "vue"
import { EMClient } from "../EaseIM"
import { useManageChatroom } from "../EaseIM/mangeChatroom"
import { manageEasemobApis } from "../EaseIM/imApis"
import "./style/index.css"
/* components */
import MessageContainer from "./message"
import InputBarContainer from "./inputbar"
console.log("EMClient", EMClient)
export default defineComponent({
  name: "EasemobChatroom",
  props: {
    username: {
      type: String,
      default: "",
      required: true
    },
    password: {
      type: String,
      default: ""
    },
    accessToken: {
      type: String,
      default: ""
    },
    chatroomId: {
      type: String,
      default: "",
      required: true
    }
  },
  setup(props) {
    const { setCurrentChatroomId } = useManageChatroom()
    const { loginIMWithPassword, loginIMWithAccessToken } = manageEasemobApis()
    const loginIM = async (): Promise<void> => {
      if (!EMClient) return
      try {
        if (props.accessToken) {
          await loginIMWithAccessToken(props.username, props.accessToken)
        } else {
          await loginIMWithPassword(props.username, props.password)
        }
      } catch (error: any) {
        throw `${error.data.message}`
      }
    }
    const closeIM = async (): Promise<void> => {
      console.log(">>>>>断开连接")
      //   EMClient.close()
    }
    onMounted(() => {
      loginIM()
      if (props.chatroomId) {
        setCurrentChatroomId(props.chatroomId)
      }
    })
    return {
      loginIM,
      closeIM
    }
  },
  render() {
    return (
      <>
        <div class={"easemob_chatroom_container"}>
          <MessageContainer />
          <InputBarContainer />
        </div>
      </>
    )
  }
})

6、输入框组件代码

主要处理插件输入框功能,实现消息文本内容,图片内容的发送。

import { defineComponent, ref } from "vue"
import { EasemobChat } from "easemob-websdk"
import { EMClient } from "../EaseIM"
import { useManageChatroom } from "../EaseIM/mangeChatroom"
/* compoents */
import InputEmojiComponent from "../components/InputEmojiComponent"
import UploadImageComponent from "../components/UploadImageComponent"
import "./style/inputbar.css"
export enum PLACE_HOLDER_TEXT {
  TEXT = "Enter 发送输入的内容..."
}
export default defineComponent({
  name: "InputBarContainer",
  setup() {
    //基础文本发送
    const inputContent = ref("")
    const setInputContent = (event: Event) => {
      inputContent.value = (event.target as HTMLInputElement).value
    }
    const { currentChatroomId, loginUserInfo, sendDisplayMessage } =
      useManageChatroom()
    const sendMessage = async (event: KeyboardEvent) => {
      if (inputContent.value.match(/^\s*$/)) return
      if (event.code === "Enter" && !event.shiftKey) {
        event.preventDefault()
        console.log(">>>>>>调用发送方法")
        const param: EasemobChat.CreateTextMsgParameters = {
          chatType: "chatRoom",
          type: "txt",
          to: currentChatroomId.value,
          msg: inputContent.value,
          from: EMClient.user,
          ext: {
            nickname: loginUserInfo.nickname
          }
        }
        try {
          await sendDisplayMessage(param)
          inputContent.value = ""
        } catch (error) {
          console.log(">>>>>消息发送失败", error)
        }
      }
    }
    const appendEmojitoInput = (emoji: string) => {
      inputContent.value = inputContent.value + emoji
    }
    return () => (
      <>
        <div class={"input_bar_container"}>
          <div class={"control_strip_container"}>
            <InputEmojiComponent onAppendEmojitoInput={appendEmojitoInput} />
            <UploadImageComponent />
          </div>

          <div class={"message_content_input_box"}>
            <input
              class={"message_content_input"}
              type="text"
              value={inputContent.value}
              onInput={setInputContent}
              placeholder={PLACE_HOLDER_TEXT.TEXT}
              onKeyup={sendMessage}
            />
          </div>
        </div>
      </>
    )
  }
})

7、消息列表组件代码

渲染聊天室内收发的消息代码,以及列表滚动。

import { defineComponent, nextTick, watch } from 'vue';
import { useManageChatroom } from '../EaseIM/mangeChatroom';
import { scrollBottom } from '../utils';
import './style/message.css';
import { EasemobChat } from 'easemob-websdk';
const { messageCollect } = useManageChatroom();

const MessageList = () => {
  const downloadSourceImage = (message: EasemobChat.MessageBody) => {
    if (message.type === 'img') {
      window.open(message.url);
    }
  };
  return (
    <>
      {messageCollect.length > 0 &&
        messageCollect.map((msgItem) => {
          return (
            <div class={'message_item_box'} key={msgItem.id}>
              <div class={'message_item_nickname'}>
                {msgItem?.ext?.nickname || msgItem.from}
              </div>
              {msgItem.type === 'txt' && (
                <p class={'message_item_textmsg'}>{msgItem.msg}</p>
              )}
              {msgItem.type === 'img' && (
                <img
                  style={'cursor: pointer;'}
                  onClick={() => {
                    downloadSourceImage(msgItem);
                  }}
                  src={msgItem.thumb}
                />
              )}
            </div>
          );
        })}
    </>
  );
};
export default defineComponent({
  name: 'MessageContainer',
  setup() {
    watch(messageCollect, () => {
      console.log('>>>>>>监听到消息列表改变');
      nextTick(() => {
        const messageContainer = document.querySelector('.message_container');
        setTimeout(() => {
          messageContainer && scrollBottom(messageContainer);
        }, 300);
      });
    });

    return () => {
      return (
        <>
          <div class='message_container'>
            <MessageList />
          </div>
        </>
      );
    };
  },
});

8、聊天室内核心方法

聊天室内部分状态管理

import { EasemobChat } from "easemob-websdk"
import { reactive, ref } from "vue"
import { DisplayMessageType, ILoginUserInfo } from "../types/index"
import { manageEasemobApis } from "../imApis/"
const messageCollect = reactive<DisplayMessageType[]>([])
const loginUserInfo: ILoginUserInfo = {
  loginUserId: "",
  nickname: ""
}
const currentChatroomId = ref("")
export const useManageChatroom = () => {
  const setCurrentChatroomId = (roomId: string) => {
    currentChatroomId.value = roomId
  }
  const setLoginUserInfo = async (loginUserId: string) => {
    const { fetchLoginUserNickname } = manageEasemobApis()
    loginUserInfo.loginUserId = loginUserId
    try {
      const res = await fetchLoginUserNickname(loginUserId)
      loginUserInfo.nickname = res[loginUserId].nickname
      console.log(">>>>>>获取到用户属性", loginUserInfo.nickname)
    } catch (error) {
      console.log(">>>>>>获取失败")
    }
  }
  const pushMessageToList = (message: DisplayMessageType) => {
    messageCollect.push(message)
  }
  const sendDisplayMessage = async (payload: EasemobChat.CreateMsgType) => {
    const { sendTextMessage, sendImageMessage } = manageEasemobApis()
    return new Promise((resolve, reject) => {
      if (payload.type === "txt") {
        sendTextMessage(payload)
          .then(res => {
            messageCollect.push(res as unknown as EasemobChat.TextMsgBody)
            resolve(res)
          })
          .catch(err => {
            reject(err)
          })
      }
      if (payload.type === "img") {
        sendImageMessage(payload)
          .then(res => {
            messageCollect.push(res as unknown as EasemobChat.ImgMsgBody)
            resolve(res)
          })
          .catch(err => {
            reject(err)
          })
      }
    })
  }

  return {
    messageCollect,
    currentChatroomId,
    loginUserInfo,
    setCurrentChatroomId,
    sendDisplayMessage,
    pushMessageToList,
    setLoginUserInfo
  }
}

实例化 IM SDK

import EaseSDK, { EasemobChat } from "easemob-websdk"
import { mountEaseIMListener } from "./listener"
export let EMClient = {} as EasemobChat.Connection
export const EMCreateMessage = EaseSDK.message.create
export const initEMClient = (appKey: string) => {
  EMClient = new EaseSDK.connection({
    appKey: appKey
  })
  mountEaseIMListener(EMClient)
  return EMClient
}

挂载聊天室相关监听监听

import { EasemobChat } from 'easemob-websdk';
import { useManageChatroom } from '../mangeChatroom';
import { manageEasemobApis } from '../imApis';
export const mountEaseIMListener = (EMClient: EasemobChat.Connection) => {
  const { pushMessageToList, setLoginUserInfo, currentChatroomId } =
    useManageChatroom();
  const { joinChatroom } = manageEasemobApis();
  console.log('>>>mountEaseIMListener');
  EMClient.addEventHandler('connection', {
    onConnected: () => {
      console.log('>>>>>onConnected');
      joinChatroom();
      setLoginUserInfo(EMClient.user);
    },
    onDisconnected: () => {
      console.log('>>>>>Disconnected');
    },
    onError: (error: any) => {
      console.log('>>>>>>Error', error);
    },
  });
  EMClient.addEventHandler('message', {
    onTextMessage(msg) {
      if (msg.chatType === 'chatRoom' && msg.to === currentChatroomId.value) {
        pushMessageToList(msg);
      }
    },
    onImageMessage(msg) {
      if (msg.chatType === 'chatRoom' && msg.to === currentChatroomId.value) {
        pushMessageToList(msg);
      }
    },
  });
  EMClient.addEventHandler('chatroomEvent', {
    onChatroomEvent(eventData) {
      console.log('>>>>chatroomEvent', eventData);
    },
  });
};

使用方式

npm install emchat-chatroom-widget
import EMChatroom from "emchat-chatroom-widget/emchat-chatroom-widget.esm.js"
//引入插件内部样式
import "emchat-chatroom-widget/style.css"
//appKey 需从环信申请
createApp(App)
  .use(EMChatroom, {
    appKey: "easemob#XXX"
  })
  .mount("#app")

  //模版组件内使用
  /**
   * @param {username} string
   * @param {password} string
   * @param {accessToken} string
   * @param {chatroomId} string
   */
    <EasemobChatroom
      :username="'hfp'"
      :password="'1'"
      :chatroomId="'208712152186885'"
    >
    </EasemobChatroom>

最终效果

image.png

相关代码

Github 源码地址

npm 相关包地址

参考资料

注册环信

环信官方 Web 端相关文档

【前端工程化-组件库】从 0-1 构建 Vue3 组件库(组件开发)

使用 TSX 编写 Vue3 组件

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

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

相关文章

Elasticsearch近实时架构

1 Elasticsearch 与 Lucene 的结构理解 一个Elasticsearch索引由一个或多个分片&#xff08;shards&#xff09;组成。这些分片可以是主分片&#xff08;primary shard&#xff09;或副本分片&#xff08;replica shard&#xff09;。每个分片都是一个独立的Lucene索引&#xf…

什么是Docker和Docker-Compose?

Docker的构成 Docker仓库&#xff1a;https://hub.docker.com Docker自身组件 Docker Client&#xff1a;Docker的客户端 Docker Server&#xff1a;Docker daemon的主要组成部分&#xff0c;接受用户通过Docker Client发出的请求&#xff0c;并按照相应的路由规则实现路由分发…

Shell命令切换root用户、管理配置文件、检查硬件

Shell命令切换root用户、管理配置文件、检查硬件 切换root用户 两种方法 su命令详细介绍 sudo命令详细介绍 /etc/passwd文件 /etc/passwd文件里为什么有乱七八糟的用户&#xff1f; /etc/shadow文件 管理配置文件 检查硬件命令 查看CPU 查看GPU 与其他基于UNIX的系统…

【Java基础篇 | 面向对象】—— 继承

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【JavaSE_primary】 本专栏旨在分享学习JavaSE的一点学习心得&#xff0c;欢迎大家在评论区讨论&#x1f48c; 继承允许一个类继承另一个…

buuctf crypto 【[HDCTF2019]basic rsa】解题记录

1.打开文件 2.套用脚本 import random from binascii import a2b_hex,b2a_hex p 262248800182277040650192055439906580479 q 262854994239322828547925595487519915551 n p * q def multiplicative_inversr(a,b): x 0 y 1 lx 1 ly 0 oa a ob…

Python项目开发:Flask基于Python的天气数据可视化平台

目录 步骤一&#xff1a;数据获取 步骤二&#xff1a;设置Flask应用程序 步骤三&#xff1a;处理用户输入和数据可视化 步骤四&#xff1a;渲染HTML模板 总结 在这个数字化时代&#xff0c;数据可视化已经成为我们理解和解释信息的重要手段。在这个项目中&#xff0c;我们…

干了三年的功能测试,让我女朋友跑了,太难受了...

简单概括一下 先说一下自己的情况&#xff0c;普通本科&#xff0c;19年通过校招进入深圳某软件公司&#xff0c;干了3年多的功能测试&#xff0c;21年的那会&#xff0c;因为大环境不好&#xff0c;我整个人心惊胆战的&#xff0c;怕自己卷铺盖走人了&#xff0c;我感觉自己不…

解疑答惑:独立部署类型的商城系统有哪些优势?

数字化时代&#xff0c;电子商务已成为企业蓬勃发展的重要组成部分。 为了满足不断增长的在线购物需求&#xff0c;企业需要一个强大而可靠的商城系统。独立部署类型的商城系统因其独特的优势而备受青睐。下面就独立部署商城系统的优势作一些简单介绍&#xff0c;并解释为什么它…

Python中使用item()方法遍历字典的例子

前言 嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 这篇文章主要介绍了Python中使用item()方法遍历字典的例子, for…in这种是Python中最常用的遍历字典的方法了,需要的朋友可以参考下 Python字典的遍历方法有好几种&#xff0c;其中一种是for…in&#xff0c;这个我就…

修改图片尺寸的几个简单方法

修改图片尺寸的几个简单方法~~图片&#xff0c;是我们常用的文件格式&#xff0c;也是日常生活与工作中重要的文件。图片记录了非常多的元素和内容&#xff0c;其中不乏有工作上的内容&#xff0c;也有对一些日常生活的记录。所以说&#xff0c;图片文件对我们来说是非常重要的…

使用轻薄款电子价签,有什么样的改价体验?

在数字化的潮流之中&#xff0c;「轻薄」逐渐成为ESL电子标签的强大优势&#xff0c;让商品管理更高效&#xff0c;货架保持统一高端的形象。云里物里最新发布了一款纤薄ESL电子标签&#xff0c;DS029厚度仅有9.8mm&#xff0c;在36g的轻巧身躯上融入了强大功能&#xff0c;将为…

FirmAFL

FirmAFL使用并改进了Firmdyne模拟方式&#xff0c;并利用AFL对IoT固件实施高通量灰盒Fuzzing。 一、项目简介 FIRM-AFL 是 第一个针对物联网固件的高吞吐量灰盒模糊测试器。 支持mipsel、mipseb和armel三种CPU架构 &#xff0c;涵盖Firmadyne数据库中90.2%的固件。 FIRM-AFL 解…

学习笔记-配置备份静态路由及优先级

上一个笔记&#xff1a;学习笔记-静态路由配置有来无回导致无法访问目标IP 拓扑图&#xff1a; 书接上回。 模拟R2至R3之间的链路中断&#xff0c;配置备份路由通过R1访问R3。 shutdown掉R2的gi0/0/2端口&#xff0c;模拟链路中断。pingR3的gi0/0/0和R3的loopback0&#xff…

想修复Windows 10屏幕分辨率问题?这里有5种方法供你选择!

​一般来说,如果你愿意,你可以很容易地更改Windows 10计算机的屏幕分辨率。如果你发现你无法在Windows 10中更改分辨率,你可以查看下面的解决方案来解决这个问题。 检查和更改Windows 10屏幕分辨率上 一、你可以右键单击桌面的黑色空间,然后选择“显示设置”。单击“分辨…

循环购模式:美业的新机遇和新挑战

美业是一个高频消费的行业&#xff0c;每个人都想要拥有美丽的容颜和健康的身体。但是&#xff0c;美业的消费门槛往往较高&#xff0c;很多人会觉得美容美发等服务太贵&#xff0c;不敢轻易尝试。如果有一种模式&#xff0c;能够让消费者在享受美业服务的同时&#xff0c;还能…

pdf.js 微信公众号不显示问题

问题1&#xff1a; 在浏览器中能够正常显示&#xff0c; 但是在微信浏览器中不行&#xff01;解决&#xff1a; 这个是pdf.js 版本问题&#xff0c; 用2.4版本&#xff0c;微信打开就没问题了 问题2&#xff1a; 如何关闭侧边栏&#xff1f; 修改这个地方&#xff0c; 将 -1 改…

Macronix MX25L25645G NOR Flash无法擦除问题分析

1. 问题现象描述 处理器使用的 SAM9X60, 使用的内核版本是 5.10.80&#xff0c;在调试 Macronix MX25L25645G NOR Flash时&#xff0c;发现flash驱动加载成功后&#xff0c;使用 mtd_debug 工具 erase flash时&#xff0c;擦除一整片flash区域时&#xff0c;命令执行速度很快&a…

十 动手学深度学习v2 ——卷积神经网络之NiN + GoogLeNet

文章目录 网络中的网络&#xff08;NiN&#xff09;InceptionGoogLeNet总结&#xff1a; 网络中的网络&#xff08;NiN&#xff09; NiN块使用卷积层加两个1x1卷积层 后者对每个像素增加了非线性性 NiN使用全局平均池化层来替代VGG和AlexNet中的全连接层 不容易过拟合&#xf…

win10CPU占用率高达100%怎么办

很多小伙伴在打开任务管理器的时候会发现win10CPU的占用率高达了100%&#xff0c;这使得我们的电脑用起来十分的卡顿&#xff0c;那么这个问题该怎么解决呢&#xff0c;这里小编就给大家带来win10CPU占用率高达100%的解决方法&#xff0c;有需要的小伙伴快来看看吧。 win10CPU占…

电子行业云MES解决方案

电子行业MES解决方案主要是针对目前电子生产制造企业面临的产品迭代升级中多品种小批量混线生产、存呆滞问题多;质量检查标准多、售后问题难追溯&#xff1b;生产进度难追踪、车间物料难管控、实际成本难计算等问题&#xff0c;提出的一种切实可行且能降低成本、提高效率的有效…