前言
在上個月初,接到一個需求,要開發一個 聊天通訊
模塊 並且 集成到 項目中的多個 入口,實現業務數據的記錄追蹤
.
接到需求后,還挺開心,這是我第一次 搞 通訊
類的需求,之前一直是 B 端
的業務需求,不過現在也是在做這個方向,感覺 B
端 方向 挺有意思,管理着項目的整個項目上游和下游,然后服務於 內部人員 和 外部人員 使用,感覺挺自豪的。
下面就就跟着我來看看 如何 開發一個 聊天通訊 服務吧 ! (主要站在前端的角度來講如何開發設計 )
技術棧
Vue 2.x
Websoket
Vuex
Element
vue-at
本項目是 以 Vue
技術棧生態開發的,其實不管用什么語言
, 思路是關鍵 !
知道每一步需要干什么, 然后將每一步操作 整合起來 , 最終服務就跑起來了.
當中的每一步需要干什么
就是 編程 中的 function
功能,根據這個功能然后在細化分析需要有到哪些技術點
。在開發的過程中,你不可能對整個鏈路的所有技術點
熟悉,這就需要遇到啥困難,臨時學習就可以了。
開始分析需求
首先,我們要等待 UI
設計師 的設計稿 畫出來, 然后根據 UI
設計師的 設計稿分析整體 聊天通訊 的結構,從view
結構 來 划分 應該 大體 包括哪些 component
, 每個component
中 又包括哪些小的 component
, 這樣從 大 到 小
的方向將 設計稿
轉化為 程序員視角的 component
.
確立了有哪些component
, 接下來 就是 確定 每個 小的 component
又有哪些 功能了。 現在 UI
設計師們,一般畫完界面后,會通過第三方軟件 / 平台
來將效果圖 轉化成網頁,並且可以通過 URL
可以直接訪問,當光標放到頁面中的某個元素時,可以獲取到當前元素的 css style
, 不過,我建議不之 copy
,有時和自己寫的布局代碼會沖突,按需copy
.
效果圖
真實效果圖,我就在這里不放出來了,為了保密性,只把整體結構,列出來,然后帶着大家分析結構和功能,如何進行編碼設計和組件設計。
功能分析圖
根據效果圖,在進行組件划分時,我要記住這個原則:高內聚,低耦合
, 組件職責單一性
我們將組件划分為:
聯系人組件
聊天組件
---- 包括了歷史記錄組件
功能根據 UI
設計師 提供的 URL
網頁來看交互效果來定,並和組長
/ 產品經理
交流需求,確定需求,以及砍掉不合理需求。
需求確定后,就是梳理組件部分的功能了。
組件構成
在分析組件之前,我們需要先了解一下Vue Component
,使用Vue
的 朋友應該很熟悉了,一個組件的構成由以下組成:
-
data
組件內部狀態 -
computed
計算屬性,監聽data
變化來實現對應的業務邏輯需求 -
watch
監聽state
變化 -
method
組將的功能編寫區 -
props
組件接受父組件
傳遞來的值,進行約束類型等 -
lifecycle
組件的生命周期, 可以在組件創建到銷毀的過程中執行對應的業務邏輯
聯系人組件
這個組件主要是用來在聊天的時候,可以通過分組快速的找到某個人聯系它,功能相對簡單。
功能:
- 查找聯系人
- 有通知某人操作
功能分析
功能1: 查找聯系人
通過現有聯系人json
數據來 查找輸入的聯系人進行匹配。 (簡單)
功能2: 通知某人
當用戶點擊到某個聯系人時,將點擊的人 放到輸入框里 顯示 @xxx
[ 經過格式化處理 ] , 並將選中的聯系人信息加入到發送消息的 json
對象中。
有多種實現方案,當用戶點擊了某聯系人時,將觸發事件,攜帶值傳遞給父組件[聊天組件的入口 index.vue ] 接收
,然后將值傳遞給 聊天主體組件
,通過 在 聊天主體組件
中 通過 $refs
進行傳遞值。
下面只提供示例代碼
從聯系人列表獲取選中聯系人
//聯系人組件 concat.vue
getLogname(val){
this.$emit('toParent',{tag:'add',logname:val})
},
聊天框顯示選中的聯系人
在聊天入口組件 接收 子向父
組件傳遞 選中聯系人數據,然后給 聊天主體
組件綁定 ref
, 通過refs
來將聯系人數據傳遞到 聊天主體
組件顯示。 [這塊 數據傳遞有多種方法,例如 Vuex
]
//聊天組件入口 index.vue 它包括 聯系人組件 聊天主體組件 歷史記錄組件
//聯系人組件
<Concat @toParent='innerHtmlToChat'/>
//聊天主體組件
<ChatRoom @fullScreen="getFullStatus" @closeWindow="close" ref="chatRoom"/>
// 接受
innerHtmlToChat(data){
this.$refs.chatRoom.$refs.inputConents.innerHTML+=` @ ${data.logname}` //拼接到聊天輸入框里
},
效果展示
從聯系人列表選中人員,發送消息
@人 接收到推送消息
聊天主體組件
這個組件就負責的功能就多了,這塊我主要把關鍵的功能帶大家來分析過一遍
關鍵功能;
@
好友功能,實現推送通知(在線通知 / 離線-上線通知)- 聊天工具 [
支持表情
支持大文件上傳
] - 發送消息 [
這塊就可以跟業務掛鈎了,發送信息時,並攜帶一些符合你項目需求的數據
]
功能分析
功能1 : @ 實現
vue-at
文檔 : https://github.com/von7750/vue-at
它的功能和 微信
和 QQ
@ 功能一樣,在聊天輸入框里,當你 輸入 @
鍵時, 彈出好友列表,然后從中選擇聯系人進行聊天。
@
功能必須包括以下3個關鍵功能;
- 可以彈出聯系人列表
- 可以監聽輸入字符內容進行過濾顯示對應數據
- 刪除 @ 聯系人
- .......
一開始, 我是 自己造了個 @
功能 輪子 搞了搞,后來才發現市場上有相應的輪子,直接用第三方了,挺不錯的 vue-at
。
下面來跟着我,來捋一下思路如何實現這個輪子,此處就不放實現代碼了。
先來分析一波:
當在編輯區,輸入 @ 時, 彈出框
- 我們可以在
mounted
生命周期中監聽 按鍵code
= 50 / 229 (中文/英文) 時,做出處理- 由於我們這塊采用的
div 可編輯屬性
,那么就獲取到 可編輯屬性的光標位置- 然后通過光標位置 動態來改變 彈出框聯系人列表的樣式
top
left
, 實現跟着光標的 位置顯示聯系人列表。- 然后 從列表中選擇 聯系人進行聊天,並將
聯系人列表彈框
隱藏掉。上面就實現了基本的
選中聯系人功能
。刪除選中的聯系人
由於這塊是采用的可編輯屬性, 我們可以獲取選中的人,但無法直接判斷是刪除的哪個人,這時,只能通過判斷
innerHTML
中是否包含某聯系人,來進行刪除已保存的聯系人。這時,已經基本滿足了業務需求實現了。
第三方插件已經的夠好了,我們就沒必要再造輪子,浪費時間了, 但 實現思路 必須的懂。 下面,我就來演示如何使用 第三方插件vue-at
實現 @
功能
1. 安裝插件
npm i vue-at@2.x
2.組件 內部導入插件組件
import At from "vue-at";
3.注冊插件組件
components: {
At
},
4. 頁面中使用
At
組件 必須包括 可編輯
輸入內容區域, 這樣,當輸入 @
時,會彈出聯系人列表框。
members
: 數據源filter-match
: 過濾數據deleteMatch
:刪除的聯系人
insert
: 獲取聯系人
<At
:members="filtercontactListContainer"
:filter-match="filterMatch"
:deleteMatch="deleteMatch"
@insert="getValue"
>
<template slot="item" slot-scope="s">
<div v-text="s.item" style="width:100%"></div>
</template>
<div
class="inputContent"
contenteditable="true"
ref="inputConents"
></div>
</At>
// 過濾聯系人
filterMatch(name, chunk) {
return name.toLowerCase().indexOf(chunk.toLowerCase()) === 0;
},
// 刪除聯系人
deleteMatch(name, chunk, suffix) {
this.contactList = this.contactList.filter(
item => item.logname != chunk.trim()
);
return chunk === name + suffix;
},
// 獲取聯系人
getValue(val) {
this.contactList.push({ logname: val });
},
功能2:聊天工具箱
聊天軟件除了普通文字聊天,還有一些輔助服務來增加聊天的豐富性,例如: 表情 , 文件上傳, 截圖上傳 .... 功能
我們先來看看 市場 熱門聊天軟件它們有哪些 聊天工具。
微信聊天工具箱
表情
文件上傳
截屏
聊天記錄
視頻聊天 / 語音聊天
QQ
聊天工具箱
表情
GIF 動圖
截屏
文件上傳
騰訊文檔
圖片發送
..... 騰訊業務相關功能
介紹了市場上熱門聊天的工具箱有哪些工具,回歸正題: 我們的聊天工具箱
有哪些功能呢, 其實有哪些功能根據 業務來定,后期工具箱可以不斷擴充。 我們的工具箱基本上滿足日常聊天需求
表情
文件上傳
支持大文件 ( 幾個G 都可以)截屏
Ctrl + Alt + A
歷史記錄
下面我就來將比較幾個重要的功能: 文件上傳
和 截屏
, 其它功能都很簡單。
文件上傳
上傳組件我采用的是 Element el-upload
組件,由於我業務 要求上傳文件支持大文件, 采用的 分片續傳
方式來實現。
分片續傳思路
-
我們上傳也是采用的
websoket
上傳,首次發送時,必須發送一些必要的文件基本信息- 文件名
- 文件大小
- 發送者
- 一些跟業務相關的字段數據
- 時間
- 文件分片大小
- 文件分片片數
- 上傳進度標識
-
首次發送完文件的基本信息后,開始發送分片文件信息,首先將文件分片后,然后依次讀取片文件流,發送時攜帶文件流,等文件分片循環結束后,發送一個結束標識告訴后台發送完畢了 [
這塊你可以和后端商量設計數據格式
]
示例代碼演示
<el-upload
ref="upload"
class="upload-demo"
drag
:auto-upload="false"
:file-list="fileList"
:http-request="httpRequest"
style="width:200px"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text" trigger>
<em> 將文件拖到此處然后點擊上傳文件</em>
</div>
</el-upload>
覆蓋掉 Element
默認上傳方式,改用自定義上傳方式。
開始分片上傳
// 上傳文件
httpRequest(options) {
let that = this;
//每個文件切片大小
const bytesPerPiece = 1024 * 2048;
// 文件必要的信息
const { name, size } = options.file;
// 文件分割片數
const chunkCount = Math.ceil(size / bytesPerPiece);
// 獲取到文件后,發送文件的基本信息
const fileBaseInfo = {
fileName: name,
fileSize: size,
segments: "historymessage",
loginName: localStorage.getItem("usrname"),
time: new Date().toLocaleString(),
chunkSize: bytesPerPiece,
chunkCount: chunkCount,
messagetype: "bufferfile",
process: "begin",
... 一些跟業務掛鈎的 字段
};
that.$websoketGlobal.ws.send(JSON.stringify(fileBaseInfo));
let start = 0;
// 進行分片
var blob = options.file.slice(start, start + bytesPerPiece);
//創建`FileReader`
var reader = new FileReader();
//開始讀取指定的 Blob中的內容, 一旦完成, result 屬性中保存的將是被讀取文件的 ArrayBuffer 數據對象.
reader.readAsArrayBuffer(blob);
//讀取操作完成時自動觸發。
reader.onload = function(e) {
// 發送文件流
that.$websoketGlobal.ws.send(reader.result);
start += bytesPerPiece;
if (start < size) {
var blob = options.file.slice(start, start + bytesPerPiece);
reader.readAsArrayBuffer(blob);
} else {
fileBaseInfo.process = "end";
// 發送上傳文件結束 標識
that.$websoketGlobal.ws.send(JSON.stringify(fileBaseInfo));
}
that.uploadStatus = false;
that.fileList = [];
};
},
效果演示
功能3: 截屏功能
在 PC
中,這是一個很重要的業務,通過這種技術可以從網上截取下自己感興趣的文章圖片供自己使用觀看,可以幫助人們更好的去理解使用知識。
由於我們的輸入內容區域采用的 可編輯
區域,此處可以插入任意內容,也可以使用外部 的截圖功能,粘貼到輸入框區域,這塊就沒必要的造輪子了。
1. 可編輯區域
我們給 div
加上 該屬性 contenteditable
就可以控制 div
中可輸入哪些內容,外部復制過來內容也可以直接顯示,還可以顯示其帶的css
效果。我們先來看看 contenteditable
有哪些屬性吧 !
值 | 描述 |
---|---|
inherit |
默認值繼承自父元素 |
true |
或空字符串,表示元素是可編輯的; |
false |
表示元素不是可編輯的。 |
plaintext-only |
純文本 |
caret |
符號 |
events |
注意
不允許簡寫為 <label contenteditable>Example Label</label>
正確的用法是 <label contenteditable="true">Example Label</label>
。
瀏覽器支持情況
使用
<div
class="inputContent"
contenteditable="true"
ref="inputConents">
</div>
效果展示
2. 截屏
由於采用的是 可編輯
,那么就可以隨意從外部 copy
, 哈哈,有意思的來了,支持 Windows 自帶的截屏 + PC 第三方 截屏......
💥快捷操作方法:
-
windows
自帶的的截屏快捷鍵截取整個屏幕
Print Screen
截取當前活動屏幕
Alt+Print Screen
-
QQ
截屏功能,支持個性化操作截圖Ctrl + Alt + A
-
微信
截屏功能, 支持個性化操作截圖Alt + A
-
專門的截屏工具....
站在巨人的肩膀上, 直接起飛。😄 , 不過確實站在用戶角度想,這點確實有點不好😘。
實際效果演示
2.1 微信截屏 show time
2.2 QQ
截屏
功能4: 發送功能
這個功能貫穿這個聊天項目,項目采用的是 websoket
實現的通信服務,全雙工通信
, 發送聊天內容時,需要攜帶一些很業務相關的數據,來實現業務跟蹤分析。下面,來簡單復習過一下 websoket
, 對沒有使用過websoket
同學也時學習。
WebSoket
WebSocket
是一種在單個TCP連接上進行全雙工通信的協議。 WebSocket
使得客戶端和服務器之間的數據交換變得更加簡單,允許服務端主動向客戶端推送數據。在WebSocket API
中,瀏覽器和服務器只需要完成一次握手,兩者之間就直接可以創建持久性的連接,並進行雙向數據傳輸。
WebSoket
特點
- 服務器可以主動向客戶端推送信息,客戶端也可以主動向服務器發送信息,是真正的雙向平等對話。
- 屬於服務器推送技術的一種。
- 與 HTTP 協議有着良好的兼容性。默認端口也是80和443,並且握手階段采用 HTTP 協議.
- 數據格式比較輕量,性能開銷小,通信高效。
- 可以發送文本,也可以發送二進制數據。
- 沒有同源限制,客戶端可以與任意服務器通信。
- 協議標識符是
ws
(如果加密,則為wss
),服務器網址就是 URL。
WebSoket
操作 API
創建Websoket連接🔗
let socket = new WebSocket("ws://域名/服務路徑")
連接 Websoket 成功觸發
open()
方法在連接成功時,觸發
socket.onopen = function() {
console.log("websocket連接成功");
};
發送消息
send()
方法並傳入一個字符串
、ArrayBuffer
或 Blob
.
socket.send("公眾號: 前端自學社區")
接收服務端返回的數據
message
事件會在 WebSocket
接收到新消息時被觸發。
socket.onmessage = function(res) {
console.log(res.data)
}
關閉 WebSoket 連接
WebSocket.close()
方法關閉 WebSocke
連接或連接嘗試(如果有的話)。 如果連接已經關閉,則此方法不執行任何操作。
socket.onclose = function() {
// 關閉 websocket
console.log("連接已關閉...");
//斷線重新連接
setTimeout(() => {
that.initWebsoket();
}, 2000);
};
WebSoket 錯誤處理
當websocket
的連接由於一些錯誤事件的發生 (例如無法發送一些數據)而被關閉時,一個error
事件將被引發.
// 監聽可能發生的錯誤
socket.addEventListener('error', function (event) {
console.log('WebSocket error: ', event);
});
通過上面我們了解了 Websoket
如何使用,接下來就是 實操了,下面走起!
項目采用的是 Vue
技術棧,更多寫法偏向於 Vue
。 由於 WebSoket
貫穿整個項目,而且需要實時推送 @
, 我們將 Websoket
盡量放在全局入口,接收信息onmessage
事件也放在 入口文件中,這樣全局都能接收到數據,接收到的數據 利用 Vuex
進行管理聊天的數據 [ 歷史數據 推送數據 發送數據 ] 。
1. 新建 一個 websoket
文件,用於全局使用
export default {
ws: {},
setWs: function(wsUrl) {
this.ws = wsUrl
}
}
2. 在Vue
入口文件index.js
中 全局注冊
import Vue from 'vue'
import websoketGlobal from './utils/websoket'
Vue.prototype.$websoketGlobal = websoketGlobal
3. 在 App.vue 中 接收 Websoket 推送的消息
這塊的設計很關鍵,決定了聊天數據的存儲和設計,過多細節代碼就不放了。
大體思路我說說一下:
-
傳輸格式上定了,那么接收的數據結構也就定了,更多的就是在數據結構上下文章了, 前后端需要約束好字段屬性。
從聊天頁面顯示狀態來看:
- 區分數據類型的
字段
,這樣前端在接收到推送的消息時,知道在頁面中該如何顯示,例如(該顯示圖片樣式還是文本樣式) - 區分發送消息顯示左右的字段, 前端通過接收到推送的消息時, 會首先判斷是否為自己,不是的話顯示在左邊樣式
- 區分 系統的推送字段, 根據這個字段顯示對應的樣式。
- ........... 更多字段屬性 需要根據你實際業務而來定
是
是
從信息推送狀態來看:
-
@
推送全局Notification
通知 和 聊天內部推送 設計@
推送 根據指定字段類型判斷 ,然后實現全局 推送聊天內容推送
: 由於它和具體某個聊天有關系,它也屬於歷史聊天數據,在聊天中根據內容數據類型
來確定如何顯示
- 區分數據類型的
mounted(){
this.$websoketGlobal.ws.onmessage = res => {
const result = JSON.parse(res.data);
// 推送數據
//聊天歷史數據 新增加發送的數據
// 獲取聊天歷史數據
//聊天歷史數據 新增加發送的數據
};
}
4. 在聊天組件中使用 Websoket
在聊天組件中,其實使用的就是 發送功能 和 獲取 歷史記錄 功能,還有就是根據 推送的消息內容字段來決定頁面中數據如何顯示。下面聊天的樣式代碼就不放了,主要放一下 發送消息的 示例代碼 。
send() {
let that = this;
// 定義數據結構: 傳遞什么內容是 前提 前端和后端商量好的
const obj = {
messageId: Number(
Math.random()
.toString()
.substr(3, length) + Date.now()
).toString(36),
//文件類型
messagetype: "textmessage",
//@ 聯系熱
call: that.contactList,
//聊天輸入內容
inputConent: that.$refs.inputConents.innerHTML ,
// 當前時間
time: currentDate,
..... 再定義一些符合你業務的字段
};
// 發送消息
that.$websoketGlobal.ws.send(JSON.stringify(obj));
that.$refs.inputConents.innerHTML = "";
that.contactList = []
}
},
在每次進入聊天組件時,需要首先獲取聊天的歷史記錄,聊天入口根據你的業務來定,傳遞必須參數.
mounted(){
this.$websoketGlobal.ws.send(
JSON.stringify({
id: 1
messagetype: "historymessage"
})
);
}
功能5: 離線 / 在線推送
這個相當於 微信
/ QQ
在線 和 上線 收到的消息。 當 A 用戶 @ 了 B 用戶 (此時 B 用戶 不在線
),當 B 用戶 上線時,它會收到 一條信息。這個是怎么實現呢?
我就結合項目來大體說一下思路,具體實現就不說了,實現主要在后端。 當時,向后端大佬同時還特意請教了一下。
當 A用戶 登錄了 系統,此時就會和Websoket
建立連接,后端會記錄起來,該用戶的標識,狀態為登錄。當 A 用戶 @ 了 B 用戶 ,正常邏輯會推送給B用戶一條信息,B 不在線,就不推給他?
怎么知道B 用戶是否在線呢?
前面也說到了,登錄系統就會建立連接,后端會暫時存儲起來在線的用戶,當A 用戶 向 B 用戶發送的消息后,后端看
在線用戶列表
里沒有B 用戶,那么他就不會推送。當B用戶上線了,會自動推送,前端接收,直接提醒用戶。
聊天室入口組件
聊天室入口組件包括: 聯系人組件
+ 聊天主體組件
, 它做的事情其實很簡單了。
- 如何打開聊天室 ?
- 如何給聊天室傳遞歷史數據?
如何打開聊天室?
外部可能通過多個入口來打開聊天室,通過一個狀態來控制顯示聊天室,傳遞類型為Boolean
如何給聊天室傳遞歷史數據?
外部通過給聊天室組件傳遞必要數據,這些必要數據然后在聯系人組件
和 聊天主體組件
內部消耗,獲取各自需要的數據,這樣聊天室入口組件的職責單一,很好進行管理。
下面來看看聊天室的入口組件:
<template>
<div>
<transition name="el-fade-in-linear" :duration="4000">
<div
class="chat-container"
>
<div
class="left-concat"
>
//聯系人組件
<Concat @toParent="innerHtmlToChat" />
</div>
<div
class="right-chatRoom"
>
// 聊天室主體組件
<ChatRoom
ref="chatRoom"
/>
</div>
</div>
</transition>
</div>
</template>
內部的通信主要是由 Vuex
來進行管理, 由於聊天室在全局都需要喚醒,可以將聊天入口組件
放到全局入口文件,這樣,不管項目需要多少個入口,只需要傳遞喚醒聊天入口組件的狀態
和 入口組件需要的必要參數
來獲取歷史聊天數據。
<Chat
// 控制是否顯示聊天室
v-if="$store.state.chatStore.roomStatus"
//聊天室需要的必要數據
:orderInfo="$store.state.chatStore"
/>
這樣,當項目其它模塊需要 聊天室
這個功能,只需要 一行代碼 即可 接入,作為插槽接入。
<template slot="note" slot-scope="props">
<i class="el-icon-chat-dot-square" @click="openChatRoome(props.data.row)"></i>
</template>
openChat(row){
this.$store.commit("Chat", { status: true, data: row });
},
總結
在開發這個 聊天服務
中也遇到了很多難點和坑,不過一個一個踩過來了,越往后做思路越開。 開發完這個 聊天服務
對技術理解又有更深的認知了,在你感覺某個功能很難困難,不知道怎么實現,你先行動起來,按照自己的思路一步一步推理,推理的過程就會思路打開了,會有多種方式來實現了。
最后
聊天服務開發了一個月,寫文章寫了一個周左右,寫作不易,如果文章學到了,點個贊👍👍👍關注,支持一下!
