vue搭建一個h5頁面步驟,vue 使用 ueditor uparse_vue手把手教學~搭建web聊天室

 2023-12-10 阅读 18 评论 0

摘要:WebSocket簡介WebSocket是一種在單個TCP連接上進行全雙工通信的協議WebSocket使得客戶端和服務器之間的數據交換變得更加簡單,并且允許服務端主動向客戶端推送數據。(HTTP協議的缺陷:通信只能由客戶端發起)vue搭建一個h5頁面步驟,使用WbeSocket,瀏覽器

WebSocket簡介

WebSocket是一種在單個TCP連接上進行全雙工通信的協議

WebSocket使得客戶端和服務器之間的數據交換變得更加簡單,并且允許服務端主動向客戶端推送數據。(HTTP協議的缺陷:通信只能由客戶端發起)

vue搭建一個h5頁面步驟,使用WbeSocket,瀏覽器和服務器只需要完成一次握手,兩者之間就可以創建持久性連接(長連接),并進行雙向數據傳輸,并且能夠實時的進行通訊

聊天室通訊還可以采用輪詢的方式實現。所謂輪詢就是客戶端在特定時間間隔,由瀏覽器向服務器發送請求獲得最新數據,這樣會浪費很多帶寬等資源

特點:

  • 建立在 TCP 協議之上,服務器端的實現比較容易。

  • 與 HTTP 協議有著良好的兼容性。默認端口也是80和443,并且握手階段采用 HTTP 協議,因此握手時不容易屏蔽,能通過各種 HTTP 代理服務器。

  • vue實現一個聊天對話框。數據格式比較輕量,性能開銷小,通信高效。

  • 可以發送文本,也可以發送二進制數據。

  • 沒有同源限制,客戶端可以與任意服務器通信。

  • 協議標識符是ws(如果加密,則為wss),服務器網址就是 URL。

使用WebSocket()構造函數來構造一個WebSocket

//注意是ws協議,不存在跨域問題,可以在本地啟node服務戶進行測試,在需要的時候換上后端服務器地址即可
var?ws?=?new?WebSocket('ws://localhost:8080');

vue搭建項目步驟,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和name
this.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。

發送消息

//存this
let?_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>

效果圖大致如下:

cebc165967d8eaee017f4b76a8e34111.png
8b52090a203069d512477b2609b04c84.png

a8daf58efa8e57db7e3a95d7827c10cb.png

?相關推薦

WebSocket 全面知識補全

7個處理JavaScript值為undefined的技巧

immutablejs 是如何優化我們的代碼的?

Chrome 新功能嘗鮮!— CSS Overview

又一個布局利器, CSS 偽類 :placeholder-shown

封裝一個vue視頻播放器組件

對于組件的可重用性,大佬給出來6個建議

學習 TS 不要錯過的八個工具

Node 中的全鏈路式日志標記及處理

使用 Node 開發服務器項目時如何高效地打日志?

用TypeScript學設計模式(享元模式)

用TypeScript學設計模式(模板方法模式)

TypeScript 設計模式之適配器模式

用TypeScript學設計模式(觀察者模式)

用TypeScript學設計模式(單例模式)

5729a073ffb21f9af5bccd0879292ce6.png

4edf3d3383114ca3fa02df635bbed323.png點在看的人特別帥/美1520abbd1b700fbcd221b984c1ed8c20.gif

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/5/194375.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息