vue 文字无缝滚动_手把手教你搭建 Vue 聊天室
WebSocket简介
WebSocket是一种在单个TCP连接上进行全双工通信的协议
WebSocket使得客户端和服务器之间的数据交换变得更加简单,并且允许服务端主动向客户端推送数据。(HTTP协议的缺陷:通信只能由客户端发起)
使用WbeSocket,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性连接(长连接),并进行双向数据传输,并且能够实时的进行通讯
聊天室通讯还可以采用轮询的方式实现。所谓轮询就是客户端在特定时间间隔,由浏览器向服务器发送请求获得最新数据,这样会浪费很多带宽等资源
特点:
建立在 TCP 协议之上,服务器端的实现比较容易。
与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
数据格式比较轻量,性能开销小,通信高效。
可以发送文本,也可以发送二进制数据。
没有同源限制,客户端可以与任意服务器通信。
协议标识符是
ws
(如果加密,则为wss
),服务器网址就是 URL。
使用WebSocket()
构造函数来构造一个WebSocket
//注意是ws协议,不存在跨域问题,可以在本地启node服务户进行测试,在需要的时候换上后端服务器地址即可var ws = new WebSocket('ws://localhost:8080');
API(常用):
[WebSocket.onclose
]
用于指定连接关闭后的回调函数。
[WebSocket.onerror
]
用于指定连接失败后的回调函数。
[WebSocket.onmessage
]
用于指定当从服务器接受到信息时的回调函数。
[WebSocket.onopen
]
用于指定连接成功后的回调函数。
[WebSocket.close([code[, reason\]])
]
关闭当前链接。
code和reason可选
code状态码 reason可读字符串,解释关闭原因
[WebSocket.send(data)
]
对要传输的数据进行排队。
SocketIO
为了兼容所有浏览器,SocketIO将WebSocket、AJAX和其它的通信方式全部封装成了统一的通信接口
Socket.IO 由两部分组成:
- 一个服务端用于集成 (或挂载) 到 Node.JS HTTP 服务器:
socket.io
- 一个加载到浏览器中的客户端:
socket.io-client
引入socket.io-client
,可以创建一个全局的实例,便于在所有文件中使用
我个人认为socket.io的最大优点就在于可以自定义事件
通过emit发送消息,通过on监听事件
//引入http标准模块,CommonJS模块const http = require("http");const fs = require("fs");const ws = require("socket.io");
//创建一个web服务const server = http.createServer(function(request,response){ response.writeHead(200,{ "Content-type":"text/html;charset=UTF-8" }) // 读取文件 const html = fs.readFileSync("index.html") response.end(html);})//基于创建的服务开启socket实例const io = ws(server)
//检测连接事件io.on("connection",function(socket){ let nmae = ''; //加入群聊 socket.on("join",function(message){ console.log(message) name = message.name //广播给其它客户端看(boradcast,除了自己以外的所有人) socket.broadcast.emit('joinNoticeOther',{ name:name, action:'加入了群聊', count:count }) })
//接收客户端所发送的消息 socket.on("message",function(message){ console.log(message) //向所有客户端广播该消息 io.emit("message",message) }) //监听到断开链接 socket.on("disconnect",function(){ count-- //发送广播 某用户离开了群聊 io.emit("disconnection",{ name:name, count:count }) })})
聊天室搭建
本次demo采用vue+WebSocket +java进行开发
创建实例
//从store中取出用户的id和namethis.userId = this.$store.getters.userInfo.userId;this.name = this.$store.getters.userInfo.realName;//根据用户的id建立各自的长连接this.ws = new WebSocket( "ws://192.168.0.87:12137/websocket/" + this.userId);this.ws.onopen = function (evt) { //绑定连接事件 if (evt.isTrusted) { //获取当前人数 CountRoom().then((res)=>{ $("#count").text(res); }) } console.log("Connection open ...");}; var _this = this; this.scrollToBottom();
//滚动到底部scrollToBottom() { this.$nextTick(() => { $(".chat-container").scrollTop($(".chat-container")[0].scrollHeight); });},
断开连接
弹框提示,选择是否重连。重连时需要先手动断开连接
当发送的文件出错或者过大,可能会导致断开连接
当离开当前路由,组件销毁的时候,需要手动断开连接
// 断开连接回调事件_this.ws.onclose = function (evt) { CountRoom().then((res)=>{ $("#count").text(res); }) if (evt.code === 1009) { _this.tipText = "发送的图片或者文件过大,请重新选择!"; } _this.dialogVisible = true;};//连接失败后的回调 _this.ws.onerror = function (evt) { console.log("Connection error."); if (evt.code === 1009) { _this.tipText = "连接失败,点击确定按钮尝试重连"; } _this.dialogVisible = true; };//点击弹出框确定按钮后 handleOK() { this.dialogVisible = false; this.tipText = "出现未知错误,请点击确定按钮尝试重连"; this.reconnet = true; let _this = this; if (this.reconnet) { // window.location.reload(); 可以通过刷新页面来实现,但是体验很差 this.ws.close();//手动关闭后再重新连接 this.init(); //重连方法在init里 _this.reconnet = false; } },//组件销毁时,需要断开连接destroyed(){ this.ws.close(); console.log("断开连接")}
富文本聊天框
有很多富文本编辑器插件包括TinyMCE、Ckeditor、UEditor(百度)、wangEditor等
本项目中不需要用到太多功能,所有选择自己实现一个简单的富文本编辑器
可以粘贴文字或图片,对文本框中的图片进行压缩,展示的图片不压缩
选择文件发送,点击文件可以获取url,可以下载或是预览
传统的输入框都是使用 来制作的,它的优势是非常简单,但最大的缺陷却是无法展示图片。为了能够让输入框能够展示图片(富文本化),我们可以采用设置了 contenteditable="true"
属性的
来实现这里面的功能
:contenteditable="editFlag" //有时需要输入框处于不可编辑状态,采用标识,默认为true ref="editor" id="msg" @keyup="getCursor" @keydown.enter.prevent="submit" @paste.prevent="onPaste" @click="getCursor" >
处理粘贴事件
任何通过“复制”或者 control + c
所复制的内容(包括屏幕截图)都会储存在剪贴板,在粘贴的时候可以在输入框的 onpaste
事件里面监听到。
而剪贴板的的内容则存放在 DataTransferItemList
对象中,可以通过 e.clipboardData.items
访问到:
//定义粘贴函数const onPaste = (e, type) => { // 如果剪贴板没有数据则直接返回 if (!(e.clipboardData && e.clipboardData.items)) { return; } // 用Promise封装便于将来使用 return new Promise((resolve, reject) => { // 复制的内容在剪贴板里位置不确定,所以通过遍历来保证数据准确 for (let i = 0, len = e.clipboardData.items.length; i const item = e.clipboardData.items[i]; // 文本格式内容处理 if (item.kind === "string") { item.getAsString((str) => { resolve({ compressedDataUrl: str }); }); // 文件格式内容处理 } else if (item.kind === "file") { const pasteFile = item.getAsFile(); const imgEvent = { target: { files: [pasteFile], }, }; chooseImg(imgEvent, (url) => { resolve(url); }); } else { reject(new Error("不支持粘贴该类型")); } } });};
chooseImg对粘贴的图片或选择的图片进行处理,将其转化为base64字符串
canvas的toDataURL的方法只能保存img/png或者img/jpeg格式的,如果格式不对话默认转成img/png
我开始想着把默认格式的img/png替换成img/gif,来展示gif图 但实际上不行,因为toDataURL只转换了一帧
暂时没想到好的办法将gif图转成base64
/** * 预览函数 * * @param {*} dataUrl base64字符串 * @param {*} cb 回调函数 */function toPreviewer(dataUrl, cb) { cb && cb(dataUrl);}
/** * 图片压缩函数 * * @param {*} img 图片对象 * @param {*} fileType 图片类型 * @param {*} maxWidth 图片最大宽度 * @returns base64字符串 */function compress(img, fileType, maxWidth, type) { let canvas = document.createElement("canvas"); let ctx = canvas.getContext("2d");
const proportion = img.width / img.height; let width = img.width; let height = img.height; //根据type来判断,是否对图片进行压缩 if (type) { //压缩后用于展示于输入框中 width = maxWidth; height = maxWidth / proportion; } canvas.width = width; canvas.height = height;
ctx.fillStyle = "#fff"; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0, width, height);
const base64data = canvas.toDataURL(fileType, 0.75);
//替换 if (fileType === "image/gif") { let regx = /(?<=data:image).*?(?=;base64)/; let base64dataGif = base64data.replace(regx, "/gif");
canvas = ctx = null;
return base64dataGif; } else { canvas = ctx = null;
return base64data; }}
/** * 选择图片函数 * * @param {*} e input.onchange事件对象 * @param {*} cb 回调函数 * @param {number} [maxsize=200 * 1024] 图片最大体积 */function chooseImg(e, cb, maxsize = 300 * 1024) { const file = e.target.files[0];
if (!file || !/\/(?:jpeg|jpg|png|gif)/i.test(file.type)) { console.log("图片格式错误!"); return; }
const reader = new FileReader(); reader.onload = function () { const result = this.result; let img = new Image();
img.onload = function () { const compressedDataUrl = compress(img, file.type, maxsize / 1024, true); const noCompressRes = compress(img, file.type, maxsize / 1024, false); toPreviewer({ compressedDataUrl, noCompressRes }, cb); img = null; }; img.src = result; };
reader.readAsDataURL(file);}
获取光标和设置光标的位置,便于插入内容
/** * 获取光标位置 * @param {DOMElement} element 输入框的dom节点 * @return {Number} 光标位置 */const getCursorPosition = (element) => { let caretOffset = 0; const doc = element.ownerDocument || element.document; const win = doc.defaultView || doc.parentWindow; const sel = win.getSelection(); if (sel.rangeCount > 0) { const range = win.getSelection().getRangeAt(0); const preCaretRange = range.cloneRange(); preCaretRange.selectNodeContents(element); preCaretRange.setEnd(range.endContainer, range.endOffset); caretOffset = preCaretRange.toString().length; } return caretOffset;};
/** * 设置光标位置 * @param {DOMElement} element 输入框的dom节点 * @param {Number} cursorPosition 光标位置的值 */const setCursorPosition = (element, cursorPosition) => { const range = document.createRange(); range.setStart(element.firstChild, cursorPosition); range.setEnd(element.firstChild, cursorPosition); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range);};
//在vue的methods中 //粘贴内容至文本框 async onPaste(e) { const result = await onPaste(e, true); this.resultOfBase64 = result.noCompressRes; const imgRegx = /^data:image\/png|jpg|jpeg|gif;base64,/; if (imgRegx.test(result.compressedDataUrl)) { document.execCommand("insertImage", false, result.compressedDataUrl); } else { document.execCommand("insertText", false, result.compressedDataUrl); } }, //获取光标位置 getCursor() { this.cursorPosition = getCursorPosition(this.editor); },
这里来了解一下document.execCommand这个API
当一个HTML文档切换到设计模式时,document
暴露 execCommand
方法,该方法允许运行命令来操纵可编辑内容区域
的元素。
参数:
aCommandName:一个 DOMString
,命令的名称,比如代码中的insertImage就是代表插入图片,insertText就是代表插入文本
aShowDefaultUI:一个 Boolean
, 是否展示用户界面,一般为 false。Mozilla 没有实现。
aValueArgument:一些命令(例如insertImage)需要额外的参数(insertImage需要提供插入image的url),默认为null。
发送消息
//存thislet _this = this;this.ws.onmessage = function (message) { console.log(message); // console.log(_this.name); var data = message.data; //第一次连接成功的时候,后台发送的数据是字符串 if (data !== "连接成功") { var result = JSON.parse(data); } let html = ""; let answer = ""; let date = new Date(); let nowTime = date.getHours() + ":" + date.getMinutes(); //将需要的数据,push到一个数组里,在页面上通过遍历数组渲染 if (result) { _this.messageList.push({ nowTime: nowTime, name: result.name, msg: result.msg, id: result.id, elImg: result.elImg,//图片标识 type: result.type,//消息分为三种类型,文本、图片、文件 url: result.url,//文件的地址 }); _this.scrollToBottom(); }};
//发送消息 submit(e, url) { const value = typeof e === "string" ? e.replace(/[\n\r]$/, "") : e.target.innerHTML.replace(/[\n\r]$/, ""); const imgRegx = /^data:image\/png|jpg|jpeg|gif;base64,/; const imgFlag = imgRegx.test(this.resultOfBase64); // console.log("resultOfBase64:" + this.resultOfBase64) let imgValue = ""; if (imgFlag && value !== "") {//判断是图片并且输入框中内容不为空 imgValue = this.resultOfBase64.replace(/[\n\r]$/, ""); this.type = 2; } else if (value && url) {//通过url来区分是文件还是文本 this.type = 3; } else if (value) { this.type = 1; } if (value) { const message = { id: this.userId, name: this.name, msg: value, elImg: imgValue, type: this.type, //1--文本 2--图片 3--文件 url: url, }; // console.log(JSON.stringify(message)); // 通过socket发送消息 this.ws.send(JSON.stringify(message)); if (typeof e === "string") { document.getElementById("msg").innerHTML = ""; document.getElementById("msg").innerText = ""; } else { e.target.innerText = ""; e.target.innerHTML = ""; } this.resultOfBase64 = ""; this.editFlag = true; } },
选择图片
class="sendFile"><i class="el-icon-picture">i><inputtype="file"id="file"title="选择图片"accept="image/png, image/jpeg, image/gif, image/jpg"
@change="getFile"
@click="getFocus"
/>
//压缩图片
chooseFile(e) {
return new Promise((resolve, reject) => {
const pasteFile = e.target.files[0];
const imgEvent = {
target: {
files: [pasteFile],
},
};
chooseImg(imgEvent, (url) => {
resolve(url);
});
});
},
//选择图片类文件
getFile(e) {
// const result = this.chooseFile(e)
this.chooseFile(e).then((res) => {
const result = res;
this.resultOfBase64 = result.noCompressRes;
const imgRegx = /^data:image\/png|jpg|jpeg|gif;base64,/;
if (imgRegx.test(result.compressedDataUrl)) {
document.execCommand("insertImage", false, result.compressedDataUrl);
} else {
document.execCommand("insertText", false, result.compressedDataUrl);
}
});
},
选择文件
文件框是自己写的div和样式,直接放在输入框中会导致输入错位,所以选择直接调用submit方法发送
class="upload-demo chooseFile" action="http://192.168.0.232:9001/zuul/web/file/simpleUpload" multiple :on-change="onChange" > <i class="el-icon-folder-opened">i> el-upload>
//自动获取焦点 getFocus() { document.getElementById("msg").focus(); }, //选择文件的onchange事件 onChange(e) { if (e.status == "success") { this.fileName = e.response.data.name; this.fileUrl = "uploadBaseUrl" + e.response.data.url; this.getCursor(); this.getFocus(); document.execCommand( "insertHTML", false, `
${this.fileName}
`
);
this.editFlag = true;
var edit = document.getElementById("msg");
//调用submit方法直接发送,不显示再输入框中
this.submit(edit.innerHTML, this.fileUrl);
} else if (e.status == "fail") {
this.$message.error("发送文件失败,请重试!");
}
},
//文件预览或下载
PreviewFile(url) {
//TOOD(window.open...)
console.log(url);
}
通过type判断,当前的文件类型,用不同的方式进行渲染
文本直接采用v-html解析
图片采用elementUI中的el-image渲染,点击可以预览没压缩的图片,也就是初始图片
文件也采用v-html渲染,加入点击事件
class="chat-container"><div class="userMessage" v-for="(item,index) in messageList" :key="index"><div class="time">{{item.nowTime}}div><div :class="userId === item.id ? 'message-self':'message-other'"><div class="message-container"><div class="icon" v-if="userId !== item.id"><img :src="userIcon" />div><div class="message-content"><div class="speaker-name">{{item.name}}div><div class="message" v-if="item.type===1" v-html="item.msg">div><div class="message" v-else-if="item.type === 2 "><el-imagestyle="width: 300px; height: 200px":src="item.elImg":preview-src-list="[item.elImg]":lazy="true"
>el-image>div><divclass="message PreviewFile"v-else-if="item.type===3"v-html="item.msg"
@click="PreviewFile(item.url)"
>div>div><div class="icon" v-if="userId === item.id"><img :src="userIcon" />div>div>div>div>div>
效果图大致如下:
vue 文字无缝滚动_手把手教你搭建 Vue 聊天室相关推荐
- python numpy安装教程_手把手教你搭建机器学习开发环境—Python与NumPy的超简安装教程...
手把手教你搭建机器学习开发环境Python语言是机器学习的基础,所以,想要入门机器学习,配置好Python的开发环境是第一步.本文就手把手的教你配置好基于Python的机器学习开发环境.超简单!第一步 ...
- 云服务器架设网站教程_手把手教你搭建腾讯云服务器入门(图文教程)
本文由博主 威威喵 原创 博客主页:https://blog.csdn.net/smile_running 背景 暑假期间,愁着无聊但也不能荒废学业吧,毕竟以后想靠技术混口饭吃!为了实施自己的计划,特 ...
- pls-00302: 必须声明 组件_手把手教你开发vue组件库
前言 Vue是一套用于构建用户界面的渐进式框架,目前有越来越多的开发者在学习和使用.在笔者写完 从0到1教你搭建前端团队的组件系统 之后很多朋友希望了解一下如何搭建基于vue的组件系统,所以作为这篇文 ...
- vue超详细教程,手把手教你完成vue项目
Vue 一. Vue简介 Vue是于2013年(与React框架同年发布)推出的一个渐进式.自底向上的前端框架,它的作者叫尤雨溪.那么什么叫做渐进式框架呢?比较官方的说法就是:以Vue内核作为核心 ...
- vue 属性是变量_手把手教你如何在生产环境检查 Vue 应用程序
本已经过原作者 Damian Mullins 授权翻译. 在开发环境中,Vue devtools 是很有用. 但是,一旦部署到生产环境,它就不再可以访问我们所编写的代码. 那么发布到生产环境时,我们 ...
- 抽奖随机滚动_手把手教你制作EXCEL抽奖器,只需两步轻松搞定
[例一]利用CHOOSE函数和RANDBETWEEN函数进行抽奖设置.如下图: 目的:在B列随机生成1-50的随机整数,取号码末尾数值,对应奖品,当末尾数值大于5时,为空奖. 操作: 第一步:在B2单 ...
- vue代码生成器可视化界面_手把手教你基于SqlSugar4编写一个可视化代码生成器(生成实体,以SqlServer为例,文末附源码)...
在开发过程中免不了创建实体类,字段少的表可以手动编写,但是字段多还用手动创建的话不免有些浪费时间,假如一张表有100多个字段,手写有些不现实. 这时我们会借助一些工具,如:动软代码生成器.各种ORM框 ...
- 手把手教你搭建 vue 环境
第一步 node环境安装 1.1 如果本机没有安装node运行环境,请下载node 安装包进行安装 1.2 如果本机已经安装node的运行换,请更新至最新的node 版本 下载地址:https://n ...
- 我的世界java无法安装包_手把手教你搭建java环境
前文 由于一些历史原因,开发java程序需要技术人员自行搭建环境,而搭建环境对于新手来说并不友好,不像其他语言那般方便,现如今,为帮助想入门java却无法顺利搭建的同学,编写该教程,如果觉得本文有用, ...
最新文章
- linux虚拟用户创建目录权限不足,在CentOs中安装vsFtpd并创建多个虚拟用户,且不同的用户拥有不同的权限以及指向不同的文件夹...
- BZOJ 2301 [HAOI2011]Problem b
- android o 小米note 3,小米 Note 3 MIUI 10 安卓 8.0 内测开启
- 常见Java面试题 – 第三部分:重载(overloading)与重写(overriding)
- lsass.exe 当试图更新密码时_“驱动人生”下载器木马再度更新-你应该注意什么?...
- linux 查找某目录下包含关键字内容的文件(文件内容、grep)
- Java讲课笔记22:Set接口及其实现类
- Oracle ora-15070,查询字段过多触发了Oracle的BUG?【ORA-01465: 无效的十六进制数字】...
- IOS学习笔记-UINavgationController
- 弹性地基梁板法计算原理_地基计算模型
- delphi 之 override overload
- MPI + OpenMP实现快速排序
- 无锡学python_无锡python基础编程好学吗
- 2018.06.25 一个不知道叫什么好的U盘启动工具集
- 超导量子计算机最新报道,量子效应的量子计算机,在高温超导体加持下,或将迎来重大突破!...
- PPT保存pps演示文档时,在另一个电脑中字体显示不正常!(已解决)
- 51单片机智能远程遥控温控PWM电风扇系统红外遥控温度速度定时关机
- Mp3Player VS Diskman(1)
- matlab:导入txt数据
- 顺序问题,母版页和内容页
热门文章
- JSR 303约束规则
- python输出去空格_Python3基础 print(,end=) 输出内容的末尾加入空格
- 【设计模式】迪米特法则和六种原则的总结
- Java8 Stream 数据流,大数据量下的性能效率怎么样?
- IntelliJ IDEA 超实用技巧分享,不能再全了!
- Java基础提升篇:equals()与hashCode()方法详解
- 【小练习02】CSS--网易产品
- Java中IO流的总结
- 【JavaSE04】Java中循环语句for,while,do···while-练习
- Android分享功能,微博、QQ、QQ空间等社交平台分享之入门与进阶