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>

效果图大致如下:



 相关推荐

WebSocket 全面知识补全

7个处理JavaScript值为undefined的技巧

immutablejs 是如何优化我们的代码的?

Chrome 新功能尝鲜!— CSS Overview

又一个布局利器, CSS 伪类 :placeholder-shown

封装一个vue视频播放器组件

对于组件的可重用性,大佬给出来6个建议

学习 TS 不要错过的八个工具

Node 中的全链路式日志标记及处理

使用 Node 开发服务器项目时如何高效地打日志?

用TypeScript学设计模式(享元模式)

用TypeScript学设计模式(模板方法模式)

TypeScript 设计模式之适配器模式

用TypeScript学设计模式(观察者模式)

用TypeScript学设计模式(单例模式)

点在看的人特别帅/美

vue 使用 ueditor uparse_vue手把手教学~搭建web聊天室相关推荐

  1. 从零开始搭建 web 聊天室(一)

    本篇将介绍如何快速.简便地使用 socket.io 库搭建一个 web 在线聊天室.前端并没有使用任何框架.后端使用 express 框架搭建简易的后端. socket.io 库本质上是基于 webs ...

  2. vue和socket.io开发简单web聊天室

    2019独角兽企业重金招聘Python工程师标准>>> 效果预览 https://www.wangchunjian.top/chat.html 需要用到的库 https://sock ...

  3. 使用nodejs搭建你自己的专属web聊天室

    前言 前断时间在学习nodejs,自己闲来无事,在网上搜索了一些资料自己搭建了一个属于自己的web聊天室项目.现在把自己的开发过程和心得和大家分享,希望其中涉及到的一些知识对你有用. 项目开源地址:h ...

  4. ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室(一) 之 基层数据搭建,让数据活起来(数据获取)...

    大家好,本篇是接上一篇 ASP.NET SignalR 与 LayIM2.0 配合轻松实现Web聊天室(零) 前言  ASP.NET SignalR WebIM系列第二篇.本篇会带领大家将 LayIM ...

  5. 2021-06-14 Socketio学习使用搭建一个聊天室

    Socketio搭建一个聊天室 前言 本次实验所用编程语言为HTML以及javascript和JQurey语言和Socketio框架,所用编辑文本工具为VS code. 注意事项 (1)前端编程注意H ...

  6. Spring和WebSocket整合并建立简单的Web聊天室

    Spring和WebSocket整合并建立简单的Web聊天室 官方主页 Spring WebSocket 一.概述 WebSocket 是一种网络通信协议.RFC6455 定义了它的通信标准. Web ...

  7. 【项目设计】基于WebSocket的Web聊天室

    文章目录 1. 项目简介 2. 数据库表的设计 3. 实体类以及工具类的设计 3.1 实体类model 3.1.1 lombok的使用 3.2 工具类util 3.2.1 DBUtil 3.2.2 W ...

  8. emqttd java 即时通讯_使用Emqttd搭建一个聊天室

    前言 由于项目需要,目前需要使用Emqttd搭建一个聊天室,自己写了个demo,特记录下来 代码 使用IDEA搭建一个Spring Boot工程 pom.xml文件,此处我只列出dependencie ...

  9. 基于阿里云用C/C++做了一个http协议与TCP协议的web聊天室的服务器——《干饭聊天室》

    基于阿里云用C/C++做了一个http协议与TCP协议的web聊天室的服务器--<干饭聊天室> 在这里首先感谢前端小伙伴飞鸟 前端技术请看一款基于React.C++,使用TCP/HTTP协 ...

  10. Django项目--web聊天室

    需求 做一个web聊天室,主要练习前端ajax与后台的交互: 一对一聊天和群组聊天 添加用户为好友 搜索并添加群组 管理员可以审批用户加群请求,群管理员可以有多个,群管理员可以删除,添加禁言群友 与聊 ...

最新文章

  1. python爬虫入门教程--快速理解HTTP协议(一)
  2. 华中科技大学计算机考研408,【21计算机考研】华中科技大学不改408了?燕山大学官宣408!...
  3. C# string.Empty
  4. Java的一维数组和二维数组的关系
  5. 快学Scala习题解答—第一章 基础
  6. matlab火焰测温源程序,一种火焰测温方法与流程
  7. JavaScript==比较的规则
  8. ES6字符串的扩展方法~超详细哦
  9. 功能强大的云打印组件-接口文档
  10. yii2设置session时间_YII2 设置session过期时间
  11. WPS Linux 2019领先的背后
  12. Java中int和byte的互相转换
  13. hibernate报错could not insert
  14. 【Office】Visio无响应问题(打开形状样式功能区即卡死)的解决方案
  15. 李天平:技术以外的功夫
  16. Flash常见问题与解答
  17. 32_mechanize使用实例
  18. 创建基础 protractor 项目
  19. 《圈外课程学习记录》1.1结构化的特征 1.2表达时主题先行
  20. 06.破解Windows7密码

热门文章

  1. [转载] 民兵葛二蛋——第10集
  2. 【转】golang 结构体和方法
  3. ES6 class继承
  4. 牛客网——F求最大值
  5. Redis学习笔记(二) Redis 数据类型
  6. selenium pydev环境配置中IEdriver server失败
  7. 网站日志分析工具:WebLog Expert Lite
  8. 陈丹琦NLP团队敢于挑战权威!谁说BERT只能Mask 15%?
  9. 【学术】让你的博士经历更加轻松愉快的10个tips
  10. 打开你的脑洞:NER如何进行数据增强 ?