express+vue实现一个在线im

在线体验地址

需要用邮箱注册一个账号
在线链接

目前实现的功能

1、在线聊天(群聊)
2、实时监控成员状态
3、历史聊天,下拉加载
4、有新消息,自动滚动到最新消息,如果自己在查看历史记录,不会强行滚动

exprees部分

创建几个表来记录相关数据

  • 下面这些都不是必须的,如果你只是生成一个临时聊天室,这些表也可以直接用个对象直接保存
    AuthorInfo 成员详细信息
    ImRoom 聊天房间
    ImRoomSys 聊天信息
    ImRoomMember 聊天成员

简单启动一个服务器,并创建基本的事件监听

socket.js

const express = require("express");
const app = express(); //创建网站服务器
const server = require("http").createServer(app);
const io = require('socket.io')(server,{ cors: true });
module.exports = {app,io,express,server,
};

具体的事件

const { io } = require("../../tool/socket.js");
const { AuthorInfo } = require("../../mod/author/author_info");
const { ImRoom } = require("../../mod/game/im_room.js");
const { ImRoomSys } = require("../../mod/game/im_room_sys.js");
const { ImRoomMember } = require("../../mod/game/im_room_member.js");
const { Game } = require("../../mod/game/game.js");
const { GameList } = require("../../mod/game/game_list.js");let allSocket = {};// 监听客户端的连接
io.on("connection", function (socket) {allSocket[socket.id] = socket;// 监听用户掉线socket.on("disconnect", async () => {// 更新用户状态let user = await ImRoomMember.findOneAndUpdate({ socket_id: socket.id },{ status: "2" });if (user) {delete allSocket[user.im_room_id];// 向房间的用户同步信息sendMsgToRoom(user.im_room_id);}});// 监听加入房间socket.on("join_room", async (data) => {if (!global.isObject(data)) {resMsg("加入房间参数错误", 400);return;}let { user_id, room_id } = data;if (!user_id) {resMsg("用户id不能为空", 400);return;}let user = await AuthorInfo.findOne({ _id: user_id });if (!user) {resMsg("用户不存在", 400);return;}if (!room_id) {resMsg("房间id不能为空", 400);return;}let room = await ImRoom.findOne({ _id: room_id, status: "1" });if (!room) {resMsg("房间不存在", 400);return;}let { max, status } = room;if (+status !== 1) {resMsg("房间未开放", 300);return;}// 查找所有加入该房间,并且状态为在线的用户let members = await ImRoomMember.find({im_room_id: room_id,status: 1,}).countDocuments();if (members >= max) {resMsg("房间已满", 300);return;}// 查找用户是否yin'jinlet oldUser = await ImRoomMember.findOne({im_room_id: room_id,author_id: user_id,});if (!oldUser) {let res = await new ImRoomMember({im_room_id: room_id,author_id: user_id,author_type: 2,created_time: getCurrentTimer(),updated_time: getCurrentTimer(),status: 1,socket_id: socket.id,}).save();if (!res) {resMsg("加入房间失败", 400);return;}} else {await ImRoomMember.updateOne({ im_room_id: room_id, author_id: user_id },{ socket_id: socket.id, status: 1 });}// 房间信息改变,向房间内所有在线用户推送房间信息sendMsgToRoom(room_id);});// 主动推出登录socket.on("live_room", async (data) => {let { room_id, user_id } = data;// 更新用户状态let user = await ImRoomMember.findOneAndUpdate({ im_room_id: room_id, author_id: user_id },{ status: "2" });if (user) {delete allSocket[user.socket_id];// 向房间的用户同步信息sendMsgToRoom(room_id);}});// 发送消息socket.on("send_msg", async (data) => {if (!global.isObject(data)) return;let { room_id, author_id, content } = data;// 判断用户是否存在if (!author_id) {resMsg("用户id不能为空", 400);return;}let user = await AuthorInfo.findOne({ _id: author_id });if (!user) {resMsg("用户id不能为空", 400);return;}// 判断房间是否存在if (!room_id) {resMsg("房间id不能为空", 400);return;}let room = await ImRoom({ _id: room_id, status: "1" });if (!room) {resMsg("房间未开放", 400);return;}if (!content) {resMsg("消息内容不能为空", 400);return;}// 保存消息let params = {im_room_id: room_id,author_id: author_id,content: content,created_time: getCurrentTimer(),updated_time: getCurrentTimer(),};let room_sys = await new ImRoomSys(params).save();if (!room_sys) {resMsg("保存消息失败", 400);return;}// 找出对应的成员信息let userinfo = await AuthorInfo.findOne({ _id: author_id },{username: 1,header_img: 1,});if (!userinfo) {resMsg("用户信息不存在", 400);return;}room_sys.author_id = userinfo;sendMsgToRoom(room_id, room_sys);});// 向一个房间内的所有在线用户推送房间的基本信息async function sendMsgToRoom(room_id, row = null) {if (!room_id) return;let members = await ImRoomMember.find({im_room_id: room_id,status: 1,},{ socket_id: 1 });if (!members || members.length === 0) return;let sockets = members.map((item) => item.socket_id);// 查出房间的基本信息// 额外查在线人数let room = (await ImRoom.findOne({ _id: room_id, status: "1" })) || {};let roomMembers = await ImRoomMember.find({ im_room_id: room_id },{ author_id: 1, status: 1 }).populate("author_id", "username").exec();// 查找出当前房间的总消息数let roomSysCount = await ImRoomSys.find({im_room_id: room_id}).countDocuments() || 0sockets.forEach((item) => {let socket = allSocket[item];let res = {data: room,roomMembers,roomSysCount,msg: "房间信息已更新",};if (global.isObject(row)) {res.content = row;}if (socket) {resMsg(res, 200, "room_baseinfo", socket);}});}// 获取当前时间戳function getCurrentTimer() {return Date.now();}// 统一返回消息function resMsg(msg, code = 400, name = "err", _socket) {let obj = {code,};if (code === 200) {obj.msg = "操作成功";obj.data = msg;} else {obj.msg = msg;}socket = _socket ? _socket : socket;socket.emit(name, obj);}
});

前端部分

需要用到下面这个插件

https://cdn.socket.io/3.1.2/socket.io.js

具体代码
index.vue

<template><div class="flex-wrap"><!-- 用户个人信息 --><leftUserInfo v-if="isShowLeft" /><!-- 聊天列表 --><centerList v-if="isShowCenter" :chatRoomList="chatInfo.chatList" /><!-- 具体聊天信息 --><rightChatv-loading="isLoading"ref="rightChatRef"v-if="isShowRight":isLoading="isLoading":socket="socket":chatInfo="chatInfo"@loadPrev="loadPrev"@postComment="postComment"/></div>
</template><script>
import { baseURL } from '@/plugins/config.js'
import { get_room, get_chat_list } from '@/api/data.js'
const plugins = [{js: 'https://cdn.socket.io/3.1.2/socket.io.js',},
]
import leftUserInfo from '@/views/blog/im/leftUserInfo.vue'
import centerList from '@/views/blog/im/centerList.vue'
import rightChat from '@/views/blog/im/rightChat.vue'
export default {components: {leftUserInfo,centerList,rightChat,},props: {isShowLeft: {type: Boolean,default: true,},isShowCenter: {type: Boolean,default: false,},isShowRight: {type: Boolean,default: true,},},data() {return {isUserActive:false,isLoadMore:false,isLoading: true,socket: {},chatInfo: {roomInfo: {roomMembers: [],im_name: '',},chatList: [],},pages: {page: 1,limit: 10,total: 0,},}},computed: {...Vuex.mapState(['userdata', 'userTags']),...Vuex.mapGetters(['isAdmin']),islogin() {return this.isLogin()},room_id() {return this.chatInfo?.roomInfo?._id || ''},baseParams() {return {user_id: this.userdata._id,room_id: this.room_id,}},},created() {this.getIndexDBJS(plugins).finally((res) => {this.init()})},beforeDestroy() {this.delPageScript(plugins)this.socket.emit('live_room', this.baseParams)},methods: {// 主动发送消息postComment(val = false){this.isUserActive = val},// 拉取上一页loadPrev() {let {room_id} = thisif(this.isLoadMore) returnlet { page, limit, total } = this.pagesif (this.chatInfo.chatList.length >= total) return// 找出第一条的idlet chat_id = this.chatInfo.chatList[0]?._id || ''if (!chat_id) returnthis.isLoadMore = trueget_chat_list({ room_id,chat_id,limit }).then(res=>{if (res.data && this.isArrayLength(res.data.data)) {let { data } = res.datathis.chatInfo.chatList = [...data.reverse(),...this.chatInfo.chatList]this.$refs.rightChatRef.reloadScrollPostion()}}).catch(()=>{}).finally(()=>{this.isLoadMore = false})},scrollToBottom() {let { rightChatRef } = this.$refsif (rightChatRef) {rightChatRef.scrollToBottom(this.isUserActive)}},async init() {// 获取房间信息let res = await get_room({ type: 1 }).catch(() => {})if (!res.data && !this.isArrayLength(res.data.data)) returnObject.assign(this.chatInfo.roomInfo, { _id: res.data.data[0]._id })let { room_id } = thisif (!room_id) return// 拉取最近的10条聊天记录let syss = await get_chat_list({ room_id })if (syss.data && this.isArrayLength(syss.data.data)) {// 倒序this.chatInfo.chatList = syss.data.data.reverse()}// 连接iothis.socket = io.connect(baseURL)// 加入房间this.socket.emit('join_room', { room_id, user_id: this.userdata._id })// 监听错误事件this.socket.on('err', (err) => {console.log('err', err)})// 监听房间基本信息this.socket.on('room_baseinfo', (res) => {console.log('room_baseinfo', res.data)if (res.data) {let { data, content, roomMembers,roomSysCount } = res.dataObject.assign(this.chatInfo.roomInfo, data, {roomMembers,})this.pages.total = roomSysCountif (content) {this.chatInfo.chatList.push(content)}// 将房间消息滚动到底部this.scrollToBottom()this.isLoading = false}})},},
}
</script><style lang="scss" scoped></style>

rightChat.vue

<template><div class="flex-1 flex-column-wrap"><!-- 聊天室信息 --><div class="flex-justify-between flex-wrap flex-center-wrap h-80 p-l-20 p-r-20 b-b-1"><div>{{ chatInfo.roomInfo.im_name || '默认聊天室' }}<span v-if="onLine"> ({{ onLine }}) </span></div><i class="el-icon-s-tools f-24"></i></div><!-- 聊天信息列表 --><div:class="['room-container p-l-20 p-r-20 b-b-1 p-t-10 p-b-10',pageClass,isLoading ? 'op-0' : '',]"@scroll="scrollEvent"><template v-if="chatInfo.chatList.length"><chatItem:class="[index !== 0 ? 'm-t-20' : '']"v-for="(item, index) in chatInfo.chatList":key="item._id":row="item":roomMembers="chatInfo.roomInfo.roomMembers"></chatItem></template><div v-else>暂无</div><div class="tip-new" v-if="false">有新消息</div></div><!-- 底部发送消息区域 --><div class="im-send-continer h-100 p-l-20 flex-center-wrap p-r-20"><kl-emoji ref="pushCommentRef" type="2" @postComment="postComment" /></div></div>
</template><script>
import chatItem from '@/views/blog/im/chatItem.vue'
export default {components: {chatItem,},props: {isLoading: {type: Boolean,default: true,},socket: {type: Object,default: () => {return {}},},chatInfo: {type: Object,default: () => {return {roomInfo: {roomMembers: [],},chatList: [],}},},},data() {return {pageClass: this.createId(),isBottom: true,}},computed: {...Vuex.mapState(['userdata']),onLine() {let count = 0this.chatInfo.roomInfo.roomMembers.forEach((item) => {if (item.status == 1) {count++}})return count},},methods: {// 重新定位async reloadScrollPostion() {let el = document.querySelector(`.${this.pageClass}`)if (el) {let oldHeight = el.scrollHeightawait this.$nextTick()let newHeight = document.querySelector(`.${this.pageClass}`).scrollHeight// 计算出滚动的高度let scrollHeight = newHeight - oldHeight// 滚动到原来的位置el.scrollTop = scrollHeight}},// 监听滚动,判断用户是否在底部scrollEvent(e) {let el = $(`.${this.pageClass}`)if (el) {this.isBottom = el.scrollTop() + el.innerHeight() >= el[0].scrollHeight - 50// 判断是否触顶,加载上一页if (el.scrollTop() <= 50) {this.$emit('loadPrev')}}},// 滚动规则: 1、第一次进入 2、新消息来之前,用户就是停在底部 3、用户主动发送了消息async scrollToBottom(val) {if (!this.isBottom && !val) returnawait this.$nextTick()let el = $(`.${this.pageClass}`)if (el) {el.scrollTop(el[0].scrollHeight)}this.$emit('postComment',false)},postComment(content) {this.$emit('postComment',true)this.socket.emit('send_msg', {room_id: this.chatInfo.roomInfo._id,author_id: this.userdata._id,content,})},},
}
</script><style lang="scss" scoped>
.room-container {height: calc(100vh - 80px - 100px);overflow-y: auto;
}
.b-b-1 {border-bottom: 1px solid #aaa;
}
</style>

如果有道友有需要的也可以私聊我,我看到会回的

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

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

相关文章

《永生之后》读后

文章以2120年背景创作&#xff0c;人类进入永生之年&#xff0c;发现了延长寿命的药物。停滞的死亡&#xff0c;新生的继续造生了人口大爆炸&#xff0c;于是分成两个阵营-长生区&#xff08;不再繁衍后代&#xff09;与生死区&#xff08;不服用药物&#xff0c;仍然生老病死&…

单片机中的四种通信总线:UART、I2C、SPI、CAN

1. UART&#xff08;Universal Asynchronous Receiver/Transmitter&#xff09; 特点&#xff1a; 通信方式&#xff1a;异步串行通信连接线数&#xff1a;两条&#xff08;TX传输线&#xff0c;RX接收线&#xff09;时钟信号&#xff1a;不需要&#xff0c;双方通过预设波特…

PySpark教程(001):基础准备与数据输入

PySpark 学习目标 了解什么是Spark、PySpark了解为什么学习PySpark了解如何和大数据开发方向进行衔接 Spark是什么&#xff1f; Apache Spark是用于大规模数据处理的统一分析引擎。 简单来说&#xff0c;Spark是一款分布式的计算框架&#xff0c;用于调度成百上千的服务器…

MyBatis总结(2)- MyBatis实现原理(一)

Mybatis实现原理&#xff1a; 概括一句话&#xff1a;约定配置参数mybatis-config.xml&#xff0c;映射关系JavaBean-mapper.xml&#xff0c;用SqlSessionFactoryBuilder构建应用程序运行期间需要的SqlSessionFactory实例对象&#xff0c;当请求或方法需要执行CURD操作时&…

初识volatile

volatile&#xff1a;可见性、不能保证原子性(数据不安全)、禁止指令重排 可见性&#xff1a;多线程修改共享内存的变量的时候&#xff0c;修改后会通知其他线程修改后的值&#xff0c;此时其他线程可以读取到修改后变量的值。 指令重排&#xff1a;源代码的代码顺序与编译后字…

基于STM32开发的智能空气质量监控系统

⬇帮大家整理了单片机的资料 包括stm32的项目合集【源码开发文档】 点击下方蓝字即可领取&#xff0c;感谢支持&#xff01;⬇ 点击领取更多嵌入式详细资料 问题讨论&#xff0c;stm32的资料领取可以私信&#xff01; 目录 引言环境准备智能空气质量监控系统基础代码实现&…

三十七篇:大数据架构革命:Lambda与Kappa的深度剖析

大数据架构革命:Lambda与Kappa的深度剖析 1. 引言 在这个数据驱动的时代,我们面临着前所未有的挑战和机遇。随着数据量的爆炸性增长,传统的数据处理方法已无法满足现代业务的需求。大数据处理不仅涉及数据量的增加,还包括数据类型的多样化、数据来源的广泛性以及对实时数据…

Policy-Based Reinforcement Learning(1)

之前提到过Discount Return&#xff1a; Action-value Function &#xff1a; State-value Function: &#xff08;这里将action A积分掉&#xff09;这里如果策略函数很好&#xff0c;就会很大&#xff1b;反之策略函数不好&#xff0c;就会很小。 对于离散类型&#xff1a; …

java的反射和python的鸭子类型

Java的反射&#xff08;Reflection&#xff09;和Python的鸭子类型&#xff08;Duck Typing&#xff09;感觉相似但又说不出具体的细节&#xff0c;本文借助kimi试图给出总结。 相似之处&#xff1a; 动态性&#xff1a;Java的反射允许程序在运行时查询、创建和修改类和对象的…

深度学习之文本分类模型-基于transformer

1、transformer transformer就是大名鼎鼎的论文《Attention Is All You Need》[1]&#xff0c;其在一些翻译任务上获得了SOTA的效果。其模型整体结构如下图所示 encoder和decoder 其整体结构由encoder和decoder组成&#xff0c;其中encoder由6个相同的block组成&#xff0c;…

【设计模式】结构型-桥接模式

当抽象与实现&#xff0c;各自独立&#xff0c; 桥接模式&#xff0c;如彩虹桥&#xff0c;连接两岸。 文章目录 一、类爆炸与代码重复二、桥接模式三、桥接模式的核心组成四、运用桥接模式五、桥接模式的应用场景六、小结推荐阅读 一、类爆炸与代码重复 场景假设&#xff1a…

单片机嵌入式计算器(带程序EXE)

单片机嵌入式计算器 主要功能&#xff1a;完成PWM占空比计算&#xff0c;T溢出时间&#xff08;延时&#xff09;&#xff1b; [!NOTE] 两个程序EXE&#xff1b; [!CAUTION] 百度网盘链接&#xff1a;链接&#xff1a;https://pan.baidu.com/s/1VJ0G7W5AEQw8_MiagM7g8A?pwdg8…

881救生艇

给定数组 people 。people[i]表示第 i 个人的体重 &#xff0c;船的数量不限&#xff0c;每艘船可以承载的最大重量为 limit。 每艘船最多可同时载两人&#xff0c;但条件是这些人的重量之和最多为 limit。 返回 承载所有人所需的最小船数 。 分析&#xff1a; 贪心策略&…

numpy数组不是连续存储时可能出现的问题

numpy数组在进行涉及到内存拷贝的操作时&#xff0c;一定要注意将其转换成连续的&#xff0c;也就是数组的元素在内存中是按顺序存储的&#xff0c;不然可能会无法得到预期的结果&#xff1b; 判断数组是否连续 你可以使用 .flags 属性中的 C_CONTIGUOUS 标志来判断一个数组是否…

代码随想录算法训练营第五十四 | ● 392.判断子序列 ● 115.不同的子序列

392.判断子序列 https://programmercarl.com/0392.%E5%88%A4%E6%96%AD%E5%AD%90%E5%BA%8F%E5%88%97.html class Solution { public:bool isSubsequence(string s, string t) {if(s.size()0 )return true;if(t.size()0)return false;vector<vector<int>> dp(s.size(…

为什么选择海外服务器?

如何选择跨境电商服务器&#xff1a;详细指南 选择合适的服务器是跨境电商企业成功的基础。服务器的性能和稳定性直接影响着网站的访问速度、用户体验和安全性&#xff0c;进而影响着企业的销量和利润。那么&#xff0c;跨境电商企业该如何选择服务器呢&#xff1f; ​​​​​…

Jenkins构建 Maven项目(微服务)并自动发布

前面讲了docker 安装Jenkins和gitlab代码管理工具&#xff0c;接下来我们讲一下Jenkins怎么构建 Maven项目。 1. 首先Jenkins配置下面3中工具类 首先是在本地安装三个jenkins自动配置相关的工具 1.1 JDK 由于我们使用docker来启动jenkins&#xff0c;其自带有jdk&#xff0c;…

python记录之字典

一、字典简介 字典&#xff08;Dictionary&#xff09;是Python中一个非常强大且灵活的数据结构&#xff0c;它允许我们存储键值对&#xff08;key-value pairs&#xff09;的集合。与列表和元组不同&#xff0c;字典中的元素是通过键&#xff08;key&#xff09;来访问的&…

oracle 12.1 rac to rac adg(maa)搭建保姆级教程

目录 资源配置 一、主库集群操作 1.主库增加standbylog 2.主库开启force logging及归档 3.主库配置参数 4.生成参数文件并将参数文件、密码文件拷贝至备库 4.1参数文件处理 4.2密码文件处理 二、备库操作 1.备库修改参数文件 1.1创建adump目录并在参数文件修改&#…

vscode Run Code输出出现中文乱码情况问题解决方案

主要解决方案是通过修改计算机默认的编码格式,来完成的。 chcp 是 Windows 操作系统中的一个命令,用于显示或设置控制台的代码页(code page)。代码页决定了控制台如何解释和显示字符,特别是非 ASCII 字符(例如 Unicode 字符)。 使用方法 显示当前代码页: 输入 chcp 而…