Vue開發多人聊天室 覆盤總結

小阿鑫發表於2021-09-16

前言

在上個月初,接到一個需求,要開發一個 聊天通訊 模組 並且 整合到 專案中的多個 入口,實現業務資料的記錄追蹤.

接到需求後,還挺開心,這是我第一次 搞 通訊 類的需求,之前一直是 B 端 的業務需求,不過現在也是在做這個方向,感覺 B 端 方向 挺有意思,管理著專案的整個專案上游和下游,然後服務於 內部人員 和 外部人員 使用,感覺挺自豪的。

下面就就跟著我來看看 如何 開發一個 聊天通訊 服務吧 ! (主要站在前端的角度來講如何開發設計 )

技術棧

徽章.png

  • 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 的 朋友應該很熟悉了,一個元件的構成由以下組成:

  1. data 元件內部狀態

  2. computed 計算屬性,監聽data 變化來實現對應的業務邏輯需求

  3. watch 監聽state 變化

  4. method 組將的功能編寫區

  5. props 元件接受父元件 傳遞來的值,進行約束型別等

  6. lifecycle 元件的生命週期, 可以在元件建立到銷燬的過程中執行對應的業務邏輯

聯絡人元件

這個元件主要是用來在聊天的時候,可以通過分組快速的找到某個人聯絡它,功能相對簡單。

功能:

  1. 查詢聯絡人
  2. 有通知某人操作

功能分析

功能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+=`&nbsp;@&nbsp;${data.logname}`  //拼接到聊天輸入框裡
},     
​

效果展示

從聯絡人列表選中人員,傳送訊息

@人 接收到推送訊息

聊天主體元件

這個元件就負責的功能就多了,這塊我主要把關鍵的功能帶大家來分析過一遍

關鍵功能;

  1. @ 好友功能,實現推送通知(線上通知 / 離線-上線通知)
  2. 聊天工具 [ 支援表情 支援大檔案上傳 ]
  3. 傳送訊息 [ 這塊就可以跟業務掛鉤了,傳送資訊時,並攜帶一些符合你專案需求的資料]

功能分析

功能1 : @ 實現

vue-at 文件 : https://github.com/von7750/vue-at

它的功能和 微信QQ @ 功能一樣,在聊天輸入框裡,當你 輸入 @ 鍵時, 彈出好友列表,然後從中選擇聯絡人進行聊天。

@ 功能必須包括以下3個關鍵功能;

  • 可以彈出聯絡人列表
  • 可以監聽輸入字元內容進行過濾顯示對應資料
  • 刪除 @ 聯絡人
  • .......

一開始, 我是 自己造了個 @ 功能 輪子 搞了搞,後來才發現市場上有相應的輪子,直接用第三方了,挺不錯的 vue-at

下面來跟著我,來捋一下思路如何實現這個輪子,此處就不放實現程式碼了。

先來分析一波:

當在編輯區,輸入 @ 時, 彈出框

  1. 我們可以在 mounted 生命週期中監聽 按鍵 code = 50 / 229 (中文/英文) 時,做出處理
  2. 由於我們這塊採用的 div 可編輯屬性 ,那麼就獲取到 可編輯屬性的游標位置
  3. 然後通過游標位置 動態來改變 彈出框聯絡人列表的樣式 top left , 實現跟著游標的 位置顯示聯絡人列表。
  4. 然後 從列表中選擇 聯絡人進行聊天,並將 聯絡人列表彈框 隱藏掉。

上面就實現了基本的 選中聯絡人功能

刪除選中的聯絡人

由於這塊是採用的可編輯屬性, 我們可以獲取選中的人,但無法直接判斷是刪除的哪個人,這時,只能通過判斷 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 元件,由於我業務 要求上傳檔案支援大檔案, 採用的 分片續傳 方式來實現。

分片續傳思路

  1. 我們上傳也是採用的 websoket 上傳,首次傳送時,必須傳送一些必要的檔案基本資訊

    • 檔名
    • 檔案大小
    • 傳送者
    • 一些跟業務相關的欄位資料
    • 時間
    • 檔案分片大小
    • 檔案分片片數
    • 上傳進度標識
  2. 首次傳送完檔案的基本資訊後,開始傳送分片檔案資訊,首先將檔案分片後,然後依次讀取片檔案流,傳送時攜帶檔案流,等檔案分片迴圈結束後,傳送一個結束標識告訴後臺傳送完畢了 [這塊你可以和後端商量設計資料格式]

示例程式碼演示

<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()方法並傳入一個字串ArrayBufferBlob .

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 推送的訊息

這塊的設計很關鍵,決定了聊天資料的儲存和設計,過多細節程式碼就不放了

大體思路我說說一下:

  • 傳輸格式上定了,那麼接收的資料結構也就定了,更多的就是在資料結構上下文章了, 前後端需要約束好欄位屬性。

    從聊天頁面顯示狀態來看:

    1. 區分資料型別的欄位,這樣前端在接收到推送的訊息時,知道在頁面中該如何顯示,例如(該顯示圖片樣式還是文字樣式)
    2. 區分傳送訊息顯示左右的欄位, 前端通過接收到推送的訊息時, 會首先判斷是否為自己,不是的話顯示在左邊樣式
    3. 區分 系統的推送欄位, 根據這個欄位顯示對應的樣式。
    4. ........... 更多欄位屬性 需要根據你實際業務而來定

    從資訊推送狀態來看:

    1. @ 推送全域性 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使用者上線了,會自動推送,前端接收,直接提醒使用者。

聊天室入口元件

聊天室入口元件包括: 聯絡人元件 + 聊天主體元件 , 它做的事情其實很簡單了。

  1. 如何開啟聊天室 ?
  2. 如何給聊天室傳遞歷史資料?

如何開啟聊天室?

外部可能通過多個入口來開啟聊天室,通過一個狀態來控制顯示聊天室,傳遞型別為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 });
},

總結

在開發這個 聊天服務 中也遇到了很多難點和坑,不過一個一個踩過來了,越往後做思路越開。 開發完這個 聊天服務 對技術理解又有更深的認知了,在你感覺某個功能很難困難,不知道怎麼實現,你先行動起來,按照自己的思路一步一步推理,推理的過程就會思路開啟了,會有多種方式來實現了。

最後

聊天服務開發了一個月,寫文章寫了一個周左右,寫作不易,如果文章學到了,點個贊???關注,支援一下!

Vue開發多人聊天室 覆盤總結

相關文章