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;
};

本期示例

相关推荐
燃先生._.3 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖4 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
black^sugar5 小时前
纯前端实现更新检测
开发语言·前端·javascript
2401_857600956 小时前
SSM 与 Vue 共筑电脑测评系统:精准洞察电脑世界
前端·javascript·vue.js
2401_857600956 小时前
数字时代的医疗挂号变革:SSM+Vue 系统设计与实现之道
前端·javascript·vue.js
GDAL6 小时前
vue入门教程:组件透传 Attributes
前端·javascript·vue.js
小白学大数据6 小时前
如何使用Selenium处理JavaScript动态加载的内容?
大数据·javascript·爬虫·selenium·测试工具
轻口味6 小时前
Vue.js 核心概念:模板、指令、数据绑定
vue.js
2402_857583496 小时前
基于 SSM 框架的 Vue 电脑测评系统:照亮电脑品质之路
前端·javascript·vue.js