express+vue在线im实现【一】
在线体验
本期完成了:
1、心跳检测
2、支持发送表情与图片【这个目前还需要优化下,当图片上传后会被默认选中,需要点击一下旁边,使之失去选中效果,才能正常,留待下期优化吧】
3、新增了一些细节,消息固定位置,是否显示呢称,新消息来的闪烁提示等
4、将整个模块独立了出来,在博客页新增了全局挂载
5、如何处理图片的加载无法正常获取到准确高度,导致无法滚动到准确位置(这个是这期本人觉得最复杂的,等待图片加载完再获取高度,体验太差;设置固定高度,又无法兼容到小图片,大的图片有看不清;具体解决方案在下方)
下期功能
1、文件发送
2、在线语音
感兴趣的,可以私聊我,也可以点个收藏,关注,以下是核心代码示例
心跳检测
为何需要做这个,长连接不稳定,会自动断开,需要我们手动来做在线检测和重连
// 轮询心跳检测setHeartBeat() {let { room_id } = thisclearTimeout(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.dataif (status == 2) {console.log('您已掉线,开始重新加入')this.socket.emit('join_room', {room_id,user_id: this.userdata._id,})}this.setHeartBeat()}}).catch(()=>{})}, this.heartTime)},
支持发送表情与图片
这个使用了高级css3属性来完成
<div:id="myInputId"class="im-input kl-contenteditable-input flex-1 no-select f-14"contenteditable="true"@paste="pasteEvent($event)"@blur="blurEvent"></div>
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()// 文件类型,将数据收集为fillet 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]?.filenamethis.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, selectionif (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) {// 旧版本的IErange = document.selection.createRange()}// 删除选中范围的内容(如果有的话)if (range) {range.deleteContents()// 创建一个临时元素来保存HTMLconst tempEl = document.createElement('div')tempEl.innerHTML = html// 将临时元素的内容复制到范围中while (tempEl.firstChild) {range.insertNode(tempEl.firstChild)}}},
如何解决图片高度问题
前端部分
上传:可以看到我们在上传图片时同时上传了图片的高度与宽度
// 上传图片,同时需要上传图片的宽高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]?.filenamethis.textContent = `<img src="${imgUrl}" class="contenteditable-unpload-img" />`this.insertHtmlAtCaret(this.textContent)})} catch (err) {this.$message.warning('请重新上传')}
回显:直接读取链接上的宽高,来计算出需要呈现的最终宽高,这样就可以不用等到图片加载完毕,就能自动滚动到准确位置
mounted() {let { chatItemClassName,maxWidth } = thislet 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.lengthif (len === 3) {let width = +arr[1]let height = +arr[2]if (isNaN(width) || isNaN(height)) returnif (width > maxWidth) {let scale = maxWidth / widthwidth = maxWidthheight = height * scale}$(item).css({width: width + 'px',height: height + 'px',})}}}}},
express的上传代码
这边我们需要接收宽高,并将宽高信息放到文件名上
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也必须 都是 filelet result = upload.array("file", limit);return result;
};
本期示例