个人技术分享

在这里插入图片描述

在线体验地址

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

目前实现的功能

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>

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