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'jin
    let 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() || 0
    sockets.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" />
        <!-- 具体聊天信息 -->
        <rightChat
            v-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} = this
            if(this.isLoadMore) return
            let { page, limit, total } = this.pages
            if (this.chatInfo.chatList.length >= total) return
            // 找出第一条的id
            let chat_id = this.chatInfo.chatList[0]?._id || ''
            if (!chat_id) return
            this.isLoadMore = true
               
            get_chat_list({ room_id,chat_id,limit }).then(res=>{
                if (res.data && this.isArrayLength(res.data.data)) {
                    let { data } = res.data
                    this.chatInfo.chatList = [...data.reverse(),...this.chatInfo.chatList]

                    this.$refs.rightChatRef.reloadScrollPostion()
                }
            }).catch(()=>{}).finally(()=>{
                this.isLoadMore = false
            })
        },
        scrollToBottom() {
            let { rightChatRef } = this.$refs
            if (rightChatRef) {
                rightChatRef.scrollToBottom(this.isUserActive)
            }
        },
        async init() {
            // 获取房间信息
            let res = await get_room({ type: 1 }).catch(() => {})

            if (!res.data && !this.isArrayLength(res.data.data)) return

            Object.assign(this.chatInfo.roomInfo, { _id: res.data.data[0]._id })
            let { room_id } = this
            if (!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()
            }
            // 连接io
            this.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.data
                    Object.assign(this.chatInfo.roomInfo, data, {
                        roomMembers,
                    })
                    this.pages.total = roomSysCount
                    if (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 = 0
            this.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.scrollHeight
                await 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) return
            await 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.mfbz.cn/a/716651.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java健身私教服务师傅小程序APP源码(APP+小程序+公众号+H5)

私人定制的健身之旅 &#x1f3cb;️ 引言&#xff1a;探索私人健身新纪元 在现代都市的快节奏生活中&#xff0c;越来越多的人开始注重身体健康和健身塑形。然而&#xff0c;传统的健身房模式可能无法满足每个人的个性化需求。这时&#xff0c;一款名为“健身私教服务师傅”的…

Spring IoC【控制反转】DI【依赖注入】

文章目录 控制反转&#xff08;IoC&#xff09;依赖注入&#xff08;DI&#xff09;IoC原理及解耦IoC 容器的两种实现BeanFactoryApplicationContext IoC 是 Inversion of Control 的简写&#xff0c;译为“控制反转”&#xff0c;它不是一门技术&#xff0c;而是一种设计思想&…

centos7.9部署k8s的几种方式

文章目录 一、常见的k8s部署方式1、使用kubeadm工具部署2、基于二进制文件的部署方式3、云服务提供商的托管 Kubernetes 服务4、使用容器镜像部署或自动化部署工具 二、使用kubeadm工具部署1、硬件准备&#xff08;虚拟主机&#xff09;2、环境准备2.1、所有机器关闭防火墙2.2、…

Cisco Catalyst 9800 wireless Controller配置操作指引

一、控制器基本信息 外立面信息&#xff1a; 硬件规格如下&#xff1a; 序号 硬件规格满配能力1业务端口 4个1G/10G光口 2 冗余端口 1个GE电口或1G光口 3 最大管理AP数量 20004 最大接入客户端数量 320005 最大WLAN数量(SSID) 40966电源模块数量 2 7 最大吞吐量 40 …

云计算 | (四)基本云安全

文章目录 📚基本云安全🐇云安全背景🐇基本术语和概念⭐️风险(risk)⭐️安全需求🐇威胁作用者⭐️威胁作用者(threat agent)⭐️匿名攻击者(anonymous attacker)⭐️恶意服务作用者(malicious service agent)⭐️授信的攻击者(trusted attacker)⭐️恶意的内部人员(mal…

Neo4j Desktop界面认识以及数据库备份与还原

Neo4j Desktop界面认识以及数据库备份与还原 neo4j 版本信息&#xff1a;Neo4j Desktop Version 1.5.9&#xff1b;neo4j 5.12.0 系统信息&#xff1a;windows 11 Neo4j Desktop 界面 每个 Project 下可以有多个 DBMS&#xff0c;而每个 DBMS 中默认有 system 和 neo4j (def…

想要做好短视频?这5大关键点你知道吗?沈阳短视频剪辑培训

在新媒体运营中&#xff0c;短视频已成为抓住观众注意力的重要工具。制作成功的短视频需要细心规划和精确执行。今天小编就围绕做好短视频的五大关键点&#xff0c;为大家进行详细解析&#xff0c;帮助您提升视频的吸引力和效果。 做好短视频的5大关键点 01内容策划&#xff1…

新闻稿标题怎么写吸引人?建议收藏

一个好的标题&#xff0c;不仅能激发读者的好奇心&#xff0c;还能引导他们继续深入了解文章内容。本文伯乐网络传媒将为你揭秘新闻稿标题写作的十大技巧&#xff0c;让你轻松写出吸引人的标题。 1. 激发好奇心 a. 提出疑问&#xff1a;以问句的形式提出问题&#xff0c;让读者…

BetterZip 5软件安装包下载

BetterZip是一款功能强大的Mac解/压缩软件&#xff0c;可以满足用户对文件压缩、解压、加密和保护等方面的需求。以下是关于BetterZip软件的主要功能、特点和使用方法的详细介绍&#xff0c;以及对其用户友好度、稳定性和安全性的评价。 安 装 包 获 取 地 址: BetterZip 5-安…

PHP邮箱服务器搭建与配置教程?如何使用?

PHP邮箱服务器搭建的步骤&#xff1f;服务器搭建的注意事项&#xff1f; 在当今的数字化时代&#xff0c;电子邮件仍然是沟通和业务处理的重要工具之一。通过PHP搭建和配置一个邮箱服务器&#xff0c;您可以实现自主掌控邮件系统&#xff0c;确保数据的安全性和隐私性。AokSen…

英语学习笔记37——Making a bookcase

Making a bookcase 做书架 词汇 Vocabulary work v. 工作 ing形式&#xff1a;working 搭配&#xff1a;work on 工作 做……工作    work for 人 为……而工作 例句&#xff1a;我正在做我的家庭作业。    I am working on my homework.    我正在为Bobby工作。 …

使用 Cheerio 和 Node.js 进行网络搜刮 2024

Web scraping 是一种强大的技术&#xff0c;用于从网站提取数据&#xff0c;广泛应用于数据分析、市场研究和内容聚合。截至2024年&#xff0c;利用 Cheerio 和 Node.js 进行 web scraping 仍然是一种流行且高效的方法。本文将深入探讨使用 Cheerio 和 Node.js 进行 web scrapi…

机械师电脑文件丢失怎么办?6个恢复方法,希望能帮到您

机械师电脑作为高性能的计算机品牌&#xff0c;受到众多用户的青睐。然而&#xff0c;即便是品质卓越的电脑&#xff0c;也难免会遇到文件丢失的困扰。无论是由于误操作、系统故障还是硬盘损坏&#xff0c;文件丢失都可能给用户带来不小的麻烦。当您发现机械师电脑上的文件突然…

win10能用微信、QQ,不能打开网页

今天上班&#xff0c;打开电脑&#xff0c;突然遇到一个问题&#xff0c;发现QQ、微信可以登录&#xff0c;但是任何网页都打不开&#xff0c;尝试了重启电脑和路由器都不行&#xff0c;最终解决了电脑可以访问网页的问题&#xff0c;步骤如下&#xff1a; 1、打开电脑的网络设…

python之对接有道翻译API接口实现批量翻译

内容将会持续更新&#xff0c;有错误的地方欢迎指正&#xff0c;谢谢! python之对接有道翻译API接口实现批量翻译 TechX 坚持将创新的科技带给世界&#xff01; 拥有更好的学习体验 —— 不断努力&#xff0c;不断进步&#xff0c;不断探索 TechX —— 心探索、心进取&…

使用 C# 进行面向对象编程:第 10 部分

封装和抽象之间的区别 对于 OOP 初学者来说&#xff0c;封装和抽象之间存在非常基本的区别。他们可能会对此感到困惑。但如果你详细了解这两个主题&#xff0c;就会发现它们之间存在巨大差异。 抽象意味着向用户隐藏不必要的数据。用户只需要所需的功能或根据其需求的输出。例…

k8s学习--OpenKruise详细解释以及原地升级及全链路灰度发布方案

文章目录 OpenKruise简介OpenKruise来源OpenKruise是什么&#xff1f;核心组件有什么&#xff1f;有什么特性和优势&#xff1f;适用于什么场景&#xff1f; 什么是OpenKruise的原地升级原地升级的关键特性使用原地升级的组件原地升级的工作原理 应用环境一、OpenKruise部署1.安…

融资融券有哪些交易技巧,两融利率现在最低多少?4.0%!

融资融券交易技巧 授信额度技巧 当我们账户净资产有显著增长时&#xff0c;最好主动申请增加信用额度&#xff0c;这样在后面行情好转入资金需要进行更多融资融券交易时就不会受限于授信额度&#xff0c;避免因为临时申请增加额度而错过交易机会。 买入委托技巧 现金的折算率…

开发一个python工具,pdf转图片,并且截成单个图片,然后修整没用的白边及循环遍历文件夹全量压缩图片

今天推荐一键款本人开发的pdf转单张图片并截取没有用的白边工具 一、开发背景&#xff1a; 业务需要将一个pdf文件展示在前端显示&#xff0c;但是基于各种原因&#xff0c;放弃了h5使用插件展示 原因有多个&#xff0c;文件资源太大加载太慢、pdf展示兼容性问题、pdf展示效…

100V宽电压H62410A恒压芯片 24V降压5V 24V降压12V电源IC

H62410A是一款宽电压100V 内置MOS管的降压恒压芯片&#xff0c;适用于24V降压至5V或12V的应用场景。其内置100V耐压MOS和宽压8V-90V的输入范围&#xff0c;使得它能够在多种电压条件下稳定工作。同时&#xff0c;支持输出电压可调至3.3V&#xff0c;为不同设备提供了灵活的电源…