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
<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 = {
id: this.userId,
name: this.name,
msg: value,
elImg: imgValue,
type: this.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.