Introduction of WebSocket

WebSocket is a protocol for full duplex communication over a single TCP connection

WebSocket makes it easier to exchange data between the client and the server, and allows the server to actively push data to the client. (HTTP protocol flaw: communication can only be initiated by the client)

With WbeSocket, the browser and server only need to complete a handshake to create a persistent connection (long connection), two-way data transfer, and real-time communication between the two

Chat room communication can also be implemented by polling. Polling means that the client sends a request to the server to obtain the latest data at a specific interval, which wastes a lot of bandwidth and other resources

Features:

  • Based on TCP protocol, the implementation of the server side is relatively easy.

  • It has good compatibility with HTTP protocol. The default ports are also 80 and 443, and the handshake phase uses HTTP protocol, so it is not easy to mask the handshake and can pass various HTTP proxy servers.

  • The data format is relatively light, with low performance overhead and high communication efficiency.

  • You can send text or binary data.

  • There are no same-origin restrictions, and clients can communicate with any server.

  • The protocol identifier is WS (or WSS if encrypted), and the server URL is the URL.

Use the WebSocket() constructor to construct a WebSocket

// Note that it is the WS protocol, there is no cross-domain problem, you can start the Node server account locally for testing, when necessary to change the back-end server address

var ws = new WebSocket('ws://localhost:8080');

Copy the code

API (common) :

[WebSocket.onclose]

Used to specify the callback function after the connection is closed.

[WebSocket.onerror]

Used to specify the callback function if the connection fails.

[WebSocket.onmessage]

Used to specify the callback function when information is received from the server.

[WebSocket.onopen]

Used to specify the callback function after a successful connection.

[WebSocket.close([code[, reason\]])]

Close the current link.

Code and Reason are optional

Code Status code Reason A readable character string explaining the reason for disabling the function

[WebSocket.send(data)]

Queue data to be transmitted.

SocketIO

To be compatible with all browsers, SocketIO encapsulates Websockets, AJAX, and other communication methods into a unified communication interface

Socket.IO consists of two parts:

  • A server to integrate (or mount) to the Node.js HTTP server:socket.io
  • A client loaded into a browser:socket.io-client

Introducing socket. IO -client allows you to create a global instance that is easy to use in all files

I personally think the biggest advantage of socket. IO is the ability to customize events

Send messages via emit and listen for events via ON

// Introduce the HTTP standard module,CommonJS module

const http = require("http");

const fs = require("fs");

const ws = require("socket.io");



// Create a Web service

const server  = http.createServer(function(request,response){

  response.writeHead(200, {

    "Content-type":"text/html; charset=UTF-8"

  })

  // Read the file

  const html = fs.readFileSync("index.html")

  response.end(html);

})

// Start the socket instance based on the created service

const io = ws(server)



// Check connection events

io.on("connection".function(socket){

  let nmae = ' ';

  // Join a group chat

  socket.on("join".function(message){

    console.log(message)

    name = message.name

    // Broadcast to other clients (boradcast, everyone but yourself)

    socket.broadcast.emit('joinNoticeOther', {

      name:name,

      action:'Joined a group chat'.

      count:count

    })

  })

  

  // Receive the message sent by the client

  socket.on("message".function(message){

    console.log(message)

    // Broadcast the message to all clients

    io.emit("message",message)

  })

  // Listen for broken links

  socket.on("disconnect".function(){

    count--

    A user has left the group chat

    io.emit("disconnection", {

      name:name,

      count:count

    })

  })

})

Copy the code

Chat room setup

This demo uses VUE +WebSocket + Java for development

Create an instance

// Retrieve the user id and name from store

this.userId = this.$store.getters.userInfo.userId;

this.name = this.$store.getters.userInfo.realName;

// Establish a long connection according to the user id

this.ws = new WebSocket(

    Ws: / / "192.168.0.87:12137 / websocket/" + this.userId

);

this.ws.onopen = function (evt{

    // Bind the connection event

    if (evt.isTrusted) {

        // Get the current number of people

        CountRoom().then((res) = >{

            $("#count").text(res);

        })

    }

    console.log("Connection open ...");

};

    var _this = this;

    this.scrollToBottom();





// Scroll to the bottom

scrollToBottom() {

    this.$nextTick((a)= > {

        $(".chat-container").scrollTop($(".chat-container") [0].scrollHeight);

    });

},

Copy the code

disconnect

A dialog box is displayed indicating whether to reconnect. Disconnect the device manually before reconnecting the device

When the sent file is wrong or too large, it may cause disconnection

Manual disconnection is required when leaving the current route and components are destroyed

// Disconnect callback event

_this.ws.onclose = function (evt{

    CountRoom().then((res) = >{

        $("#count").text(res);

    })

    if (evt.code === 1009) {

        _this.tipText = "Sent picture or file is too large, please choose again!";

    }

    _this.dialogVisible = true;

};

// Callback after connection failure

 _this.ws.onerror = function (evt{

     console.log("Connection error.");

     if (evt.code === 1009) {

         _this.tipText = "Failed to connect. Click OK to try to reconnect.";

     }

     _this.dialogVisible = true;

 };

// Click the ok button in the pop-up box

 handleOK() {

     this.dialogVisible = false;

     this.tipText = "Unknown error occurred. Please click ok to try reconnection.";

     this.reconnet = true;

     let _this = this;

     if (this.reconnet) {

         // window.location.reload(); You can do this by refreshing the page, but the experience is poor

         this.ws.close();// Manually close and reconnect

         this.init(); // Reconnection is in init

         _this.reconnet = false;

     }

 },

// Disconnect the component when it is destroyed

destroyed(){

    this.ws.close();

    console.log("Disconnect")

}

Copy the code

Rich text chat box

There are many rich text editor plug-ins including TinyMCE, Ckeditor, UEditor (Baidu), wangEditor, etc

This project does not need to use too many functions, so choose to implement a simple rich text editor yourself

You can paste text or pictures to compress the pictures in the text box. The displayed pictures are not compressed

Select File to send, click on file to get the URL, download or preview

Traditional input fields are created using

with the contenteditable=”true” property set
<div class="editor" :contenteditable="editFlag" Default: true ref="editor" id=" MSG "@keyUp ="getCursor" @keydown.enter.prevent="submit" @paste. Prevent ="onPaste" @click="getCursor" ></div>Copy the code

Handling paste Events

Anything copied using Copy or Control + C (including screenshots) is stored on the clipboard and can be listened to in the onPaste event of the input box as it is pasted.

The contents of the clipboard are stored in the DataTransferItemList object, which can be accessed via e.clipboardData.items:

// Define the paste function

const onPaste = (e, type) = > {

  // If the clipboard has no data, it returns directly

  if(! (e.clipboardData && e.clipboardData.items)) {

    return;

  }

  // Use the Promise wrapper for future use

  return new Promise((resolve, reject) = > {

    // Copy the contents of the clipboard position is uncertain, so through traversal to ensure that the data is accurate

    for (let i = 0, len = e.clipboardData.items.length; i < len; i++) {

      const item = e.clipboardData.items[i];

      // Text formatting content processing

      if (item.kind === "string") {

        item.getAsString((str) = > {

          resolve({ compressedDataUrl: str });

        });

      // File format content processing

      } else if (item.kind === "file") {

        const pasteFile = item.getAsFile();

        const imgEvent = {

          target: {

            files: [pasteFile],

          },

        };

        chooseImg(imgEvent, (url) => {

          resolve(url);

        });

      } else {

        reject(new Error("Pasting this type is not supported"));

      }

    }

  });

};

Copy the code

ChooseImg takes the pasted image or selected image and converts it to a Base64 string

The canvas toDataURL method can only save img/ PNG or IMG/JPEG format, if the format is not converted to IMG/PNG by default

I started thinking about replacing the default img/ PNG format with img/ GIF to display giFs but it didn’t work because toDataURL only converted one frame

I haven’t thought of a good way to convert GIF images into Base64

/ * *

* Preview function

 *

* @param {*} dataUrl base64 A character string

@param {*} cb callback function

* /


function toPreviewer(dataUrl, cb{

  cb && cb(dataUrl);

}



/ * *

* Picture compression function

 *

* @param {*} img Image object

* @param {*} fileType Indicates the image type

* @param {*} maxWidth Maximum image width

* @returns base64 A character string

* /


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;

  // Compress the image according to type

  if (type) {

    // Compressed for display in the input box

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



  / / replace

  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;

  }

}



/ * *

* Select the picture function

 *

* @param {*} e input.onchange event object

@param {*} cb callback function

* @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("Picture format wrong!");

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

}

Copy the code

Gets the cursor and sets the cursor position to facilitate insertion of content

/ * *

* Gets the cursor position

* @param {DOMElement} Element input box dom node

* @return {Number} Cursor position

* /


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;

};





/ * *

* Set the cursor position

* @param {DOMElement} Element input box dom node

* @param {Number} cursorPosition specifies the value of the 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);

};



    // In vue's methods

    // Paste the content into the text box

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

        }

    },

    // Get the cursor position

    getCursor() {

        this.cursorPosition = getCursorPosition(this.editor);

    },

Copy the code

Here’s a look at the Document. execCommand API

When an HTML document switches to design mode, the document exposes the execCommand method, which allows commands to be run to manipulate elements in the editable content area.

Parameters:

ACommandName: a DOMString, the name of the command. For example, insertImage in code is for inserting images, and insertText is for inserting text

AShowDefaultUI: A Boolean indicating whether to display the user interface. Mozilla doesn’t implement it.

AValueArgument: Some commands (such as insertImage) require additional arguments (insertImage needs to provide the URL to insert the image), which defaults to null.

Send a message

/ / this

let _this = this;

this.ws.onmessage = function (message{

    console.log(message);

    // console.log(_this.name);

    var data = message.data;

    // When the first connection is successful, the data sent by the background is a string

    if(data ! = ="Connection successful") {

        var result = JSON.parse(data);

    }

    let html = "";

    let answer = "";

    let date = new Date(a);

    let nowTime = date.getHours() + ":" + date.getMinutes();

    // Push the desired data into an array and render it by traversing the array on the page

    if (result) {

        _this.messageList.push({

            nowTime: nowTime,

            name: result.name, 

            msg: result.msg,

            id: result.id,

            elImg: result.elImg,// The image identifier

            type: result.type,// Messages are divided into three types: text, image, and file

            url: result.url,// Address of the file

        });

        _this.scrollToBottom();

    }

};



// Send a message

 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 ! = ="") {// It is a picture and the input field is not empty

         imgValue = this.resultOfBase64.replace(/[\n\r]$/."");

         this.type = 2;

     } else if (value && url) {// Use the url to distinguish between files and text

         this.type = 3;

     } else if (value) {

         this.type = 1;

     }

     if (value) {

         const message = {

             idthis.userId,

             namethis.name,

             msg: value,

             elImg: imgValue,

             typethis.type, //1-- text 2-- picture 3-- file

             url: url,

         };

         // console.log(JSON.stringify(message));

         // Send messages through the 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;

     }

 },

Copy the code

Choose picture

 <div class="sendFile">

    <i class="el-icon-picture"></i>

    <input

    type="file"

    id="file"

    title="Select picture"

    accept="image/png, image/jpeg, image/gif, image/jpg"

    @change="getFile"

    @click="getFocus"

    />


// Compress the image

    chooseFile(e) {

      return new Promise((resolve, reject) => {

        const pasteFile = e.target.files[0];

        const imgEvent = {

          target: {

            files: [pasteFile],

          },

        };

        chooseImg(imgEvent, (url) => {

          resolve(url);

        });

      });

    },

// Select the image file

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

        }

      });

    },

Copy the code

Select the file

The file box is a div and style written by myself. Putting it directly in the input box will cause input dislocation, so I choose to call submit method to send it directly

 

    <el-upload

    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>



 // Automatically get focus

    getFocus() {

      document.getElementById("msg").focus();

    },

    // Select the onchange event for the file

    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.

          ` <div class="fileBox">

              <div class = "imgcover"></div>

              <div>The ${this.fileName}</div>

            </div>`


        );

        this.editFlag = true;

        var edit = document.getElementById("msg");

        // Call the submit method and send it directly without displaying the input box

        this.submit(edit.innerHTML, this.fileUrl);

      } else if (e.status == "fail") {

        this.$message.error("Failed to send file, please try again!");

      }

    },

    // File preview or download

    PreviewFile(url) {

      //TOOD(window.open...)

      console.log(url);

    }

Copy the code

Determine the current file type by type and render it in a different way

Text is directly parsed using V-HTML

Images are rendered using El-Image in elementUI. Click to preview the uncompressed image, which is the original image

The file is also rendered in V-HTML with click events

  <div 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-image

                      style="width: 300px; height: 200px"

                      :src="item.elImg"

                      :preview-src-list="[item.elImg]"

                      :lazy="true"

                    >
</el-image>

                  </div>

                  <div

                    class="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>

Copy the code

The effect diagram is roughly as follows:

I am Monkeysoft, your [three] is the biggest power of monkeysoft creation, if this blog has any mistakes and suggestions, welcome to leave a comment!

The article continues to be updated, you can search wechat [little monkey’s Web growth path] follow the public number for the first time to read.