從 0 到 1 開發一個聊天通訊服務覆盤總結

前言

在上個月初,接到一個需求,要開發一個 聊天通訊 模塊 並且 集成到 項目中的多個 入口,實現業務數據的記錄追蹤.

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

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

技術棧

本項目是 以 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+=` @ ${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 聊天工具箱」

QQ 聊天工具箱

介紹了市場上熱門聊天的工具箱有哪些工具,迴歸正題:我們的聊天工具箱  有哪些功能呢, 其實有哪些功能根據 業務來定,後期工具箱可以不斷擴充。「我們的工具箱基本上滿足日常聊天需求」

下面我就來將比較幾個重要的功能:文件上傳  和 截屏 , 其它功能都很簡單。

「文件上傳」

上傳組件我採用的是 Element el-upload 組件,由於我業務 要求上傳文件支持大文件, 採用的 分片續傳 方式來實現。

「分片續傳思路」

  1. 我們上傳也是採用的 websoket 上傳,首次發送時,必須發送一些必要的文件基本信息

  1. 首次發送完文件的基本信息後,開始發送分片文件信息,首先將文件分片後,然後依次讀取片文件流,發送時攜帶文件流,等文件分片循環結束後,發送一個結束標識告訴後臺發送完畢了 [這塊你可以和後端商量設計數據格式]

「示例代碼演示」

<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 有哪些屬性吧 !

oGBpt8

「注意」

不允許簡寫爲 <label contenteditable>Example Label</label>

正確的用法是 <label contenteditable="true">Example Label</label>

「瀏覽器支持情況」

「使用」

<div
     class="inputContent"
     contenteditable="true"
     ref="inputConents">
</div>

「效果展示」

「2. 截屏」

由於採用的是 可編輯 ,那麼就可以隨意從外部 copy , 哈哈,有意思的來了,支持 Windows 自帶的截屏  + PC 第三方 截屏......

💥快捷操作方法:

站在巨人的肩膀上, 直接起飛。😄 ,  不過確實站在用戶角度想,這點確實有點不好😘。

** 實際效果演示 **

「2.1 微信截屏 show time」

**2.2 QQ 截屏 **

功能 4:發送功能

這個功能貫穿這個聊天項目,項目採用的是 websoket  實現的通信服務,全雙工通信 ,  發送聊天內容時,需要攜帶一些很業務相關的數據,來實現業務跟蹤分析。下面,來簡單複習過一下 websoket , 對沒有使用過websoket 同學也時學習。

WebSoket

WebSocket是一種在單個 TCP 連接上進行全雙工通信的協議。WebSocket使得客戶端和服務器之間的數據交換變得更加簡單,允許服務端主動向客戶端推送數據。在WebSocket API中,瀏覽器和服務器只需要完成一次握手,兩者之間就直接可以創建持久性的連接,並進行雙向數據傳輸。

WebSoket 特點」

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. @  推送全局 Notification 通知  和  聊天內部推送 設計

  2. 區分數據類型的字段,這樣前端在接收到推送的消息時,知道在頁面中該如何顯示,例如(該顯示圖片樣式還是文本樣式)

  3. 區分發送消息顯示左右的字段, 前端通過接收到推送的消息時, 會首先判斷是否爲自己,不是的話顯示在左邊樣式

  4. 區分 系統的推送字段,  根據這個字段顯示對應的樣式。

  5. ........... 更多字段屬性 需要根據你實際業務而來定

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

總結

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

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/X4vTTaeqL2tjR8kg1kT1zw