express+vue在线im实现【二】

express+vue在线im实现【一】

在线体验

本期完成了:

1、心跳检测

2、支持发送表情与图片【这个目前还需要优化下,当图片上传后会被默认选中,需要点击一下旁边,使之失去选中效果,才能正常,留待下期优化吧】

3、新增了一些细节,消息固定位置,是否显示呢称,新消息来的闪烁提示等

4、将整个模块独立了出来,在博客页新增了全局挂载

5、如何处理图片的加载无法正常获取到准确高度,导致无法滚动到准确位置(这个是这期本人觉得最复杂的,等待图片加载完再获取高度,体验太差;设置固定高度,又无法兼容到小图片,大的图片有看不清;具体解决方案在下方)

下期功能

1、文件发送

2、在线语音

感兴趣的,可以私聊我,也可以点个收藏,关注,以下是核心代码示例

心跳检测

为何需要做这个,长连接不稳定,会自动断开,需要我们手动来做在线检测和重连

js 复制代码
        // 轮询心跳检测
        setHeartBeat() {
            let { room_id } = this
            clearTimeout(this.timer)
            this.timer = setTimeout(() => {
                im_heart({ room_id })
                    .then((res) => {
                        if (res && this.isObject(res.data) && this.isObject(res.data.data)) {
                            let { status } = res.data.data
                            if (status == 2) {
                                console.log('您已掉线,开始重新加入')
                                this.socket.emit('join_room', {
                                    room_id,
                                    user_id: this.userdata._id,
                                })
                            }
                            this.setHeartBeat()
                        }
                    })
                    .catch(()=>{})
            }, this.heartTime)
        },

支持发送表情与图片

这个使用了高级css3属性来完成

html 复制代码
    <div
                :id="myInputId"
                class="im-input kl-contenteditable-input flex-1 no-select f-14"
                contenteditable="true"
                @paste="pasteEvent($event)"
                @blur="blurEvent"
            ></div>
js 复制代码
  async pasteEvent(event) {
            // 尝试从 event.clipboardData 获取粘贴的项
            if (event.clipboardData && event.clipboardData.items) {
                for (let index in event.clipboardData.items) {
                    const item = event.clipboardData.items[index]

                    if (item.kind === 'file') {
                        event.preventDefault()
                        // 文件类型,将数据收集为fil
                        let file = item.getAsFile()
                        try {
                            let {
                                file: miniFile,
                                newWidth,
                                newHeight,
                            } = await this.compressImg(file, 0.85)
                            const formData = new FormData()
                            formData.append('file', miniFile)
                            const  devicePixelRatioa = window.devicePixelRatio || 1

                            // 上传图片,同时需要上传图片的宽高
                            upload_imgs_im(formData, {
                                type: this.isIm ? 'im' : 'sys',
                                devicePixelRatioa,
                                width: Math.floor(newWidth / devicePixelRatioa),
                                height: Math.floor(newHeight / devicePixelRatioa),
                            }).then((res) => {
                                if (res.code != 200) {
                                    return this.$message.error(res.msg || '请重新上传')
                                }
                                // 将返回的图片链接替换到输入框中
                                let imgUrl = baseURL + this.filePath + res.data[0]?.filename
                                this.textContent = `<img src="${imgUrl}" class="contenteditable-unpload-img" />`
                                this.insertHtmlAtCaret(this.textContent)
                            })
                        } catch (err) {
                            this.$message.warning('请重新上传')
                        }
                    }
                }
            }
        },

        insertHtmlAtCaret(html, element = document.querySelector('.my-input')) {
            // 获取当前元素的选中范围
            let range, selection
            if (window.getSelection) {
                // 大多数浏览器,包括IE9+
                selection = window.getSelection()
                if (selection.rangeCount > 0) {
                    range = selection.getRangeAt(0)
                } else {
                    // 如果没有选中范围,则创建一个新的范围
                    range = document.createRange()
                    range.selectNodeContents(element)
                    range.collapse(false) // 将范围设置在元素内容的末尾
                    selection.addRange(range)
                }
            } else if (document.selection && document.selection.createRange) {
                // 旧版本的IE
                range = document.selection.createRange()
            }

            // 删除选中范围的内容(如果有的话)
            if (range) {
                range.deleteContents()
                // 创建一个临时元素来保存HTML
                const tempEl = document.createElement('div')
                tempEl.innerHTML = html

                // 将临时元素的内容复制到范围中
                while (tempEl.firstChild) {
                    range.insertNode(tempEl.firstChild)
                }
            }
        },

如何解决图片高度问题

前端部分

上传:可以看到我们在上传图片时同时上传了图片的高度与宽度

js 复制代码
  // 上传图片,同时需要上传图片的宽高
                            upload_imgs_im(formData, {
                                type: this.isIm ? 'im' : 'sys',
                                devicePixelRatioa,
                                width: Math.floor(newWidth / devicePixelRatioa),
                                height: Math.floor(newHeight / devicePixelRatioa),
                            }).then((res) => {
                                if (res.code != 200) {
                                    return this.$message.error(res.msg || '请重新上传')
                                }
                                // 将返回的图片链接替换到输入框中
                                let imgUrl = baseURL + this.filePath + res.data[0]?.filename
                                this.textContent = `<img src="${imgUrl}" class="contenteditable-unpload-img" />`
                                this.insertHtmlAtCaret(this.textContent)
                            })
                        } catch (err) {
                            this.$message.warning('请重新上传')
                        }

回显:直接读取链接上的宽高,来计算出需要呈现的最终宽高,这样就可以不用等到图片加载完毕,就能自动滚动到准确位置

js 复制代码
    mounted() {
        let { chatItemClassName,maxWidth } = this
        let imgs = document.querySelectorAll(`.${chatItemClassName} .contenteditable-unpload-img`)
        if (imgs && imgs.length > 0) {
            for (let i = 0; i < imgs.length; i++) {
                const item = imgs[i]
                item.onclick = () => {
                    this.prevewImg(item)
                }

                // 重新设置图片的宽高
                const src = $(item).attr('src')
                let arr = src.split('~')
                arr = arr.filter((item) => !isNaN(+item))
                if (Array.isArray(arr)) {
                    let len = arr.length
                    if (len === 3) {
                        let width = +arr[1]
                        let height = +arr[2]
                        if (isNaN(width) || isNaN(height)) return
                        if (width > maxWidth) {
                            let scale = maxWidth / width
                            width = maxWidth
                            height = height * scale
                        }
                        $(item).css({
                            width: width + 'px',
                            height: height + 'px',
                        })
                    }
                }
            }
        }
    },

express的上传代码

这边我们需要接收宽高,并将宽高信息放到文件名上

js 复制代码
const path = require("path");
const multer = require("multer");
module.exports = (limit = 1, file_type_name = "blog") => {
  let storage = multer.diskStorage({
    destination: function (req, file, cb) {
      let { type } = req.query;
      if (type) {
        file_type_name = type;
      }
      const file_path = path.resolve(
        __dirname,
        "../../public/",
        file_type_name
      );
      cb(null, file_path);
    },
    filename: function (req, file, cb) {
      let { user_id, devicePixelRatioa = 1, width = 0, height = 0 } = req.query;
      let fileOption = {
        author_id: user_id,
        netdisk_url: "",
        netdisk_name: "",
        netdisk_save_name: "",
        netdisk_size: "",
        netdisk_create_time: "",
      };
      let fileFormat = file.originalname.split(".");
      let old_name = "";
      fileFormat.forEach((item, index) => {
        if (index < fileFormat.length - 1) {
          old_name += item;
        }
      });

      let file_type = fileFormat[fileFormat.length - 1];
      let netdisk_save_name = `${old_name}-${Date.now()}~${devicePixelRatioa}~`;
      if (width && height) {
        netdisk_save_name += `${width}~${height}~`;
      }
      netdisk_save_name += `.${file_type}`;
      // 存储相关数据到自定义项
      fileOption.netdisk_url = file_type_name + "/" + netdisk_save_name;
      fileOption.netdisk_name = file.originalname || "";
      fileOption.netdisk_save_name = netdisk_save_name || "";
      fileOption.netdisk_size = file.size || 0;
      fileOption.netdisk_create_time = Date.now();
      req.fileOption = fileOption;
      cb(null, netdisk_save_name);
    },
  });
  let upload = multer({ storage: storage });
  // file 前端上传key也必须 都是 file
  let result = upload.array("file", limit);
  return result;
};

本期示例

相关推荐
小曲曲14 分钟前
接口上传视频和oss直传视频到阿里云组件
javascript·阿里云·音视频
学不会•1 小时前
css数据不固定情况下,循环加不同背景颜色
前端·javascript·html
EasyNTS2 小时前
H.264/H.265播放器EasyPlayer.js视频流媒体播放器关于websocket1006的异常断连
javascript·h.265·h.264
活宝小娜4 小时前
vue不刷新浏览器更新页面的方法
前端·javascript·vue.js
程序视点4 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
coldriversnow4 小时前
在Vue中,vue document.onkeydown 无效
前端·javascript·vue.js
我开心就好o4 小时前
uniapp点左上角返回键, 重复来回跳转的问题 解决方案
前端·javascript·uni-app
刚刚好ā5 小时前
js作用域超全介绍--全局作用域、局部作用、块级作用域
前端·javascript·vue.js·vue
yqcoder6 小时前
reactflow 中 useNodesState 模块作用
开发语言·前端·javascript
会发光的猪。7 小时前
css使用弹性盒,让每个子元素平均等分父元素的4/1大小
前端·javascript·vue.js