1. 程式人生 > >小程式即時通訊聊天控制元件(一)

小程式即時通訊聊天控制元件(一)

小程式即時通訊(一)輸入元件及使用WebSocket通訊

最新更新日誌

2018-09-18

優化:現在app.js中的有關IM的所有業務統一交由app-im-delegate管理
優化:現在im-factory以單例模式提供唯一的IMHandler例項

IM模板功能:

  • 目前專案中已使用webSocket,實現了IM的通訊功能!目前包括會話列表頁面、會話頁面及好友頁面。支援使用nodejs開啟本地WebSocket服務。詳見下方文件。
  • 支援傳送文字、圖片、語音,支援輸入法的emoji表情
  • 支援拍照,相簿選擇圖片、圖片預覽
  • 支援切換到文字輸入時,顯示傳送按鈕。
  • 支援語音播放及播放動畫。
  • 支援配置錄製語音的最短及最長時間。
  • 支援配置自定義事件。
  • 支援聊天訊息按時間排序。
  • 支援傳送訊息後,頁面回彈到最底部。
  • 使用了最新的語音播放介面,同時相容了低版本的語音播放介面。
  • 訊息傳送中、傳送成功、傳送失敗的狀態更新
  • 支援訊息傳送失敗情況下,點選重發按鈕重新發送
  • 優化時間氣泡顯示邏輯,相鄰資訊大於5分鐘顯示後者資訊的時間
  • 在頁面最上方增加了會話狀態的UI展示
  • 自定義功能,顯示自定義氣泡。
  • 通過解析語音或圖片訊息資訊,優先讀取本地檔案。
  • 實現了檔案儲存演算法,保證10M儲存空間內的語音和圖片檔案均為最新。
  • 最低支援微信基礎庫版本1.4.0
  • 各訊息型別和各功能均已模組化,讓你在瀏覽程式碼時愉悅輕鬆。(其實這算不上元件特性。。。)

IM模板不支援的功能:

  • 如果要使用群聊,目前的UI中,頭像旁並沒有展示成員暱稱。
  • 本地沒有儲存歷史聊天訊息。這個原因請看文章結尾。
  • 目前WebSocket所有功能僅供學習和參考,若想商用,請自行開發。
  • 目前不支援以外掛方式使用。

整體效果圖(載入有些慢)

我們先來看下效果 (因錄製軟體問題,圖中的一些按鈕的變色了,線條也少了很多畫素。。。)

  • 傳送圖片和圖片預覽
    傳送圖片和圖片預覽
  • 訊息重發和傳送自定義訊息
    訊息重發和傳送自定義訊息
  • 傳送語音訊息
    傳送語音訊息

聊天輸入元件

近期一直在做微信小程式,業務上要求在小程式裡實現即時通訊的功能。這部分功能需要用到文字和語音輸入及一些語音相關的手勢操作。所以我寫了一個控制元件來處理這些操作。
聊天輸入元件和會話頁面元件是兩個不同的元件,分別處理不同的業務。

控制元件樣式

控制元件樣式

功能

  • 切換輸入方式(文字或語音)
  • 獲取輸入的文字資訊
  • 語音輸入及取消語音輸入
  • 語音訊息錄製時長過短過長的判斷
  • 支援傳送圖片(拍照或選擇相簿圖片)和其他自定義拓展內容

注意:SDK僅支援微信基礎庫1.4.0及以上版本。

輸入元件這部分內容我會從整合、編寫控制元件兩個部分來講解。畢竟大部分人都是想盡快整合來著,所以先說說整合部分。

輸入元件的整合

一、匯入SDK相關檔案

輸入元件相關檔案在modules/chat-inputimage資料夾下,示例頁面是pages/chat-input/chat-input

聊天輸入元件和會話頁面元件所有你需要整合的檔案,打包後大小在65kb左右,已經很小了。需要注意的是,專案中的原有.gif資料夾已經遷移到了別的倉庫,image資料夾中有兩張用於測試的使用者頭像,也可以刪除掉。

二、整合到會話頁面

1. 在會話頁面中匯入chat-input檔案
  • 在聊天頁的js檔案中匯入 let chatInput = require('../../modules/chat-input/chat-input');
  • 在聊天頁的wxml檔案中引入佈局
<import src="../../modules/chat-input/chat-input.wxml"/> 
<template is="chat-input" data="{{inputObj:inputObj,textMessage:textMessage,showVoicePart:true}}"/>
  • 在聊天頁的wxss檔案中引入樣式表@import "../../modules/chat-input/chat-input.wxss";
    根據你的路徑來匯入這些內容</>
2. 初始化chatInput
chatInput.init(page, 
        {
			systemInfo: wx.getSystemInfoSync(),
			minVoiceTime: 1,//秒,最小錄音時長,小於該值則彈出‘說話時間太短’
            maxVoiceTime: 60,//秒,最大錄音時長,大於該值則按60秒處理
            startTimeDown: 56,//秒,開始倒計時時間,錄音時長達到該值時彈窗介面更新為倒計時彈窗
            format:'mp3',//錄音格式,有效值:mp3或aac,僅在基礎庫1.6.0及以上生效,低版本不生效
            sendButtonBgColor: 'mediumseagreen',//傳送按鈕的背景色
            sendButtonTextColor: 'white',//傳送按鈕的文字顏色
            extraArr: [{
                picName: 'choose_picture',
                description: '照片'
            }, {
                picName: 'take_photos',
                description: '拍攝'
            }, {
                picName: 'close_chat',
                description: '自定義功能'
            }],
            tabbarHeigth: 48
        });
  • page:這個是指當前的page。
  • systemInfo必填。手機的系統資訊,用於控制元件的適配。
  • minVoiceTime: 最小錄音時長,秒,小於該值則彈出‘說話時間太短’
  • maxVoiceTime: 最大錄音時長,秒,填寫的數值大於該值則按60秒處理。錄音時長如果超過該值,則會儲存最大時長的錄音,並彈出‘說話時間超時’並終止錄音
  • startTimeDown: 開始倒計時時間,秒,錄音時長達到該值時彈窗介面更新為倒計時彈窗
  • extraArr:非必填。點選右側加號時,顯示的自定義功能。picName元素的名字就是對應image/chat/extra資料夾下的png格式的圖片名稱,用於展示自定義功能的圖片。description元素用於展示自定義功能的文字說明。
  • tabbarHeight:非必填。這個也是用於適配。如果你的小程式有tabbar,那麼需要填寫這個欄位,填48就行。如果你的小程式沒有tabbar,那麼就不要填寫這個欄位。原因我會在第二篇講到。
  • format: 錄音格式,有效值:mp3或aac,僅在基礎庫1.6.0及以上生效,低版本不生效,當然也不會報錯。
  • sendButtonBgColor: 傳送按鈕的背景色
  • sendButtonTextColor: 傳送按鈕的文字顏色
3. 監聽獲取輸入的資訊

在初始化控制元件之後,監聽資訊的輸入,即可獲取到指定型別的資訊

文字資訊:
//文字資訊的輸入監聽
chatInput.setTextMessageListener(function (e) {
            let content = e.detail.value;//輸入的文字資訊
           
        });  
語音資訊:
//獲取錄音之後的音訊臨時檔案
chatInput.recordVoiceListener(function (res, duration) {
         let tempFilePath = res.tempFilePath;//語音臨時檔案的路徑
         let vDuration = duration;//錄音時長
     });
//監聽錄音狀態
 chatInput.setVoiceRecordStatusListener(function (status) {
            switch (status) {
                case chatInput.VRStatus.START://開始錄音

                    break;
                case chatInput.VRStatus.SUCCESS://錄音成功

                    break;
                case chatInput.VRStatus.CANCEL://取消錄音

                    break;
                case chatInput.VRStatus.SHORT://錄音時長太短

                    break;
                case chatInput.VRStatus.UNAUTH://未授權錄音功能

                    break;
                case chatInput.VRStatus.FAIL://錄音失敗(已經授權了)

                    break;
            }
        })
自定義功能:
//收起自定義功能視窗
chatInput.closeExtraView();

//自定義功能點選事件
chatInput.clickExtraListener(function (e) {
            let itemIndex = parseInt(e.currentTarget.dataset.index);//點選的自定義功能索引
            if (itemIndex === 2) {
                that.myFun();//其他的自定義功能
                return;
            }
            //選擇圖片或拍照
            wx.chooseImage({
                count: 1, // 預設9
                sizeType: ['compressed'],
                sourceType: itemIndex === 0 ? ['album'] : ['camera'],
                success: function (res) {
                    let tempFilePath = res.tempFilePaths[0];
                }
            });
        });
//新增右下角加號button點選事件
chatInput.setExtraButtonClickListener(function (dismiss) {
            console.log('Extra彈窗是否消失', dismiss);
        })

至此,輸入元件SDK的整合就完成了!

客戶端WebSocket功能及會話頁面

效果圖

  • 會話列表

  • 會話頁面

  • 好友頁面

會話頁面,我將UI封裝成了多個template,最後使用chat-item.wxml即可,UI相關的程式碼都放到了chat-page資料夾中;載入方面的UI放到了loading資料夾中;image資料夾中也新增了幾張圖片。

對於即時通訊方面的sdk,我是用的WebSocket,當然這部分內容僅供參考。你可以引入常見的sdk,比如騰訊的、網易的。

目前實現了三個頁面。會話列表頁面、好友列表頁面、會話頁面。

會話列表頁面功能:

  • 顯示好友頭像、暱稱、最新一條訊息內容及時間、未讀計數展示。
  • 實時更新好友最新一條訊息及時間,實時更新未讀計數。
  • 從會話頁面回退到會話列表頁面後,會重新整理列表頁面。

好友頁面功能:

  • 顯示好友頭像暱稱。

好友頁面未實現功能:

並未實現發起聊天功能,僅供演示,如有需要,請自行實現。

會話頁面功能:

  • 支援傳送文字、語音、圖片及自定義型別訊息。因傳送檔案型別的訊息需要上傳檔案的伺服器,所以目前僅支援傳送文字訊息和自定義型別訊息。如果你自己配置好了上傳檔案的伺服器,那麼就可以傳送語音和圖片訊息了。

重要功能模組的流程圖

有網友建議畫個流程圖,梳理下專案中的各部分關係。

這部分的東西包括WebSocket寫完之後發現,並沒有很多難點,所以我只說下使用時要注意的幾點。

會話列表頁面

示例頁面是pages/chat-list/chat-list

  • IM連線和會話列表頁收發訊息流程圖:

IM連線和收發訊息流程圖

程式碼非常簡單。

// pages/chat-list/chat-list.js

/**
 * 會話列表頁面
 */
Page({

    /**
     * 頁面的初始資料
     */
    data: {
        conversations: []
    },

    /**
     * 生命週期函式--監聽頁面載入
     */
    onLoad: function (options) {

    },

    toChat: function (e) {
        let item = e.currentTarget.dataset.item;
        delete item.latestMsg;
        delete item.unread;
        wx.navigateTo({
            url: `../chat/chat?friend=${JSON.stringify(item)}`
        });
    },
    /**
     * 生命週期函式--監聽頁面顯示
     */
    onShow: function () {

        getApp().getIMHandler().setOnReceiveMessageListener({
            listener: (msg) => {
                console.log('會話列表', msg);
                msg.type === 'get-conversations' && this.setData({conversations: msg.conversations.map(item => this.getConversationsItem(item))})
            }
        });
        getApp().getIMHandler().sendMsg({
            content: {
                type: 'get-conversations',
                userId: getApp().globalData.userInfo.userId//這裡獲取的userInfo,就是在建立webSocket連線時伺服器返回的,作為你的身份資訊。
            }, success: () => {
                console.log('獲取會話列表訊息傳送成功');
            },
            fail: (res) => {
                console.log('獲取會話列表失敗', res);
            }
        });
    },
    getConversationsItem(item) {
        let {latestMsg, ...msg} = item;
        return Object.assign(msg, JSON.parse(latestMsg));
    }
});

就這些程式碼。結合流程圖來看的話,相信你肯定能看懂。看不懂也沒事,能理解思想就行,就是 註冊監聽、發起請求、回撥監聽、渲染頁面。

好友列表頁面

示例頁面是pages/friends/friends

同會話列表頁面,沒什麼好說的。

會話頁面

示例頁面是pages/chat/chat

  • 會話頁面傳送訊息流程圖:

會話頁面傳送訊息

chat資料夾下,我封裝了多個類,用於管理訊息型別的收發和展示。

MsgManager是一個收發訊息的工廠類,用於統一管理所有訊息型別的收發。見msg-manager.js

import VoiceManager from "./msg-type/voice-manager";//語音訊息的收發和展示相關類
import TextManager from "./msg-type/text-manager";//文字型別訊息的收發和展示相關類
import ImageManager from "./msg-type/image-manager";//圖片型別訊息的收發和展示相關類
import CustomManager from "./msg-type/custom-manager";//自定義型別訊息的收發和展示相關類
import IMOperator from "./im-operator";//im行為管理類

export default class MsgManager {
    constructor(page) {
        this.voiceManager = new VoiceManager(page);
        this.textManager = new TextManager(page);
        this.imageManager = new ImageManager(page);
        this.customManager = new CustomManager(page);
    }

    showMsg({msg}) {
        let tempManager = null;
        switch (msg.type) {
            case IMOperator.VoiceType:
                tempManager = this.voiceManager;
                break;
            case IMOperator.ImageType:
                tempManager = this.imageManager;
                break;
            case IMOperator.TextType:
            case IMOperator.CustomType:
                tempManager = this.textManager;
        }
        tempManager.showMsg({msg});
    }

    sendMsg({type = IMOperator.TextType, content, duration}) {
        let tempManager = null;
        switch (type) {
            case IMOperator.VoiceType:
                tempManager = this.voiceManager;
                break;
            case IMOperator.ImageType:
                tempManager = this.imageManager;
                break;
            case IMOperator.CustomType:
                tempManager = this.customManager;
                break;
            case IMOperator.TextType:
                tempManager = this.textManager;
        }
        tempManager.sendOneMsg(content, duration);
    }

    stopAllVoice() {
        this.voiceManager.stopAllVoicePlay();
    }
}

小程式基礎庫1.6.0以後不再維護的語音播放和錄製介面,我進行了相容處理。

篇幅問題,各型別訊息的相關類程式碼我就不貼了。想看的去下載吧。

快取機制和檔案型別訊息的展示機制

  • 快取和展示機制:在展示語音或圖片型別的訊息時,我會優先載入已經儲存在本地的檔案。在檔案型別訊息(如語音、圖片訊息)的showMsg()方法中先是取訊息的本地路徑const localVoicePath = FileManager.get(msg),如果沒有獲取到本地路徑,就會按照訊息中的檔案路徑資訊去下載該檔案,並存儲下來。這裡程式碼沒有貼出來,大家理解意思就行。
    那取到的值是什麼時候設定的呢?是在傳送或接收訊息成功後(此時檔案已下載成功),以訊息的saveKey為key,儲存成功返回的savedFilePath為data,建立訊息和本地儲存路徑的對映關係,如:FileManager.set(msg, savedFilePath);
    這裡的saveKey是以訊息id msgId 和好友id friendId,拼接而成的。

  • 儲存溢位演算法:在儲存檔案時,我參考了Android LruCache的思想編寫了演算法,保證在小程式10M儲存限制的前提下,儲存新的檔案,如果溢位了,就移除最舊的檔案(跟LruCache溢位時去除不常用檔案的思想不太一樣)。演算法具體內容見小程式效能優化——檔案的本地儲存10M優化演算法

const MAX_SIZE = 10400000;
let wholeSize = 0;
setTimeout(() => {
    wx.getSavedFileList({
        success: savedFileInfo => {
            let {fileList} = savedFileInfo;
            !!fileList && fileList.forEach(item => {
                wholeSize += item.size;
            });
        }
    });
});

function saveFileRule(tempFilePath, cbOk, cbError) {
    wx.getFileInfo({
        filePath: tempFilePath,
        success: tempFailInfo => {
            let tempFileSize = tempFailInfo.size;
            // console.log('本地臨時檔案大小', tempFileSize);
            if (tempFileSize > MAX_SIZE) {
                typeof cbError === "function" && cbError('檔案過大');
                return;
            }
            wx.getSavedFileList({
                success: savedFileInfo => {
                    let {fileList} = savedFileInfo;
                    if (!fileList) {
                        typeof cbError === "function" && cbError('獲取到的fileList為空,請檢查你的wx.getSavedFileList()函式的success返回值');
                        return;
                    }
                    //這裡計算需要移除的總檔案大小
                    let sizeNeedRemove = wholeSize + tempFileSize - MAX_SIZE;
                    if (sizeNeedRemove >= 0) {
                        //按時間戳排序,方便後續移除檔案
                        fileList.sort(function (item1, item2) {
                            return item1.createTime - item2.createTime;
                        });
                        let sizeCount = 0;
                        for (let i = 0, len = fileList.length; i < len; i++) {
                            if ((sizeCount += fileList[i].size) >= sizeNeedRemove) {
                                for (let j = 0; j < i; j++) {
                                    wx.removeSavedFile({
                                        filePath: fileList[j].filePath,
                                        success: function () {
                                            wholeSize -= fileList[j].size;
                                        }
                                    });
                                }
                                break;
                            }
                        }
                    }

                    wx.saveFile({
                        tempFilePath: tempFilePath,
                        success: res => {
                            wholeSize += tempFileSize;
                            typeof cbOk === "function" && cbOk(res.savedFilePath);
                        },
                        fail: cbError
                    });
                },
                fail: cbError
            });
        }
    });
}

module.exports = {
    saveFileRule
};

IIMHandler抽象類 i-im-handler.js

這個類是IM-SDK的介面規範類。你可以通過繼承這個類,然後在子類中實現細節,這樣的話可以很方便的接入其他的第三方IM-SDK。

/**
 * 由於JavaScript沒有介面的概念,所以我編寫了這個IM基類
 * 將你自己的IM的實現類繼承這個類就可以了
 * 我把IM通訊的常用方法封裝在這裡,
 * 有些實現了具體細節,但有些沒實現,是作為抽象函式,由子類去實現細節,這點是大家需要注意的
 */
export default class IIMHandler {
    constructor() {
        this._isLogin = false;
        this._msgQueue = [];
        this._receiveListener = null;
    }

    /**
     * 建立IM連線
     * @param options 傳入你建立連線時需要的配置資訊,比如url
     */
    createConnection({options}) {
        // 作為抽象函式
    }

    /**
     * 傳送訊息
     * @param content 需要傳送的訊息,是一個物件,如{type:'text',content:'abc'}
     * @param success 傳送成功回撥
     * @param fail 傳送失敗回撥
     */
    sendMsg({content, success, fail}) {
        if (this._isLogin) {
            this._sendMsgImp({content, success, fail});
        } else {
            this._msgQueue.push(content);
        }
    }

    /**
     * 訊息接收監聽函式
     * @param listener
     */
    setOnReceiveMessageListener({listener}) {
        this._receiveListener = listener;
    }

    closeConnection() {
        // 作為抽象函式
    }

    _sendMsgImp({content, success, fail}) {
        // 作為抽象函式
    }
}

IIMHandler實現類 web-socket-handler-imp.js

這個檔案位於modules資料夾下。

這個是IM-SDK的WebSocket實現類,所有的WebSocket基本操作都封裝到了這個類中。我截取了重要的幾處程式碼。

import IIMHandler from "../interface/i-im-handler";

export default class WebSocketHandlerImp extends IIMHandler{
    constructor() {
        super();
        this._onSocketMessage();
    }

    /**
     * 建立WebSocket連線
     * 如:this.imWebSocket = new IMWebSocket();
     *    this.imWebSocket.createSocket({url: 'ws://10.4.97.87:8001'});
     * 如果你使用本地伺服器來測試,那麼這裡的url需要用ws,而不是wss,因為用wss無法成功連線到本地伺服器
     * @param options 建立連線時需要的配置資訊,這裡是傳入的url,即你的服務端地址,埠號不是必需的。
     */
    createConnection({options}) {
        !this._isLogin && wx.connectSocket({
            url: options.url,
            header: {
                'content-type': 'application/json'
            },
            method: 'GET'
        });
    }


    _sendMsgImp({content, success, fail}) {
        wx.sendSocketMessage({
            data: JSON.stringify(content), success: () => {
                success && success(content);
            },
            fail: (res) => {
                fail && fail(res);
            }
        });
    }


    /**
     * 關閉webSocket
     */
    closeConnection() {
        wx.closeSocket();
    }

    /**
     * webSocket是在這裡接收訊息的
     * 在socket連線成功時,伺服器會主動給客戶端推送一條訊息型別為login的資訊,攜帶了使用者的基本資訊,如id,頭像和暱稱。
     * 在login資訊接收前傳送的所有訊息,都會被推到msgQueue佇列中,在登入成功後會自動重新發送。
     * 這裡我進行了事件的分發,接收到非login型別的訊息,會回撥監聽函式。
     * @private
     */
    _onSocketMessage() {
        wx.onSocketMessage((res) => {
            let msg = JSON.parse(res.data);
            if ('login' === msg.type) {
                this._isLogin = true;
                getApp().globalData.userInfo = msg.userInfo;
                getApp().globalData.friendsId = msg.friendsId;
                if (this._msgQueue.length) {
                    let temp;
                    while (temp = this._msgQueue.shift()) {
                        this.sendMsg({content: {...temp, userId: msg.userInfo.userId}});
                    }
                }
            } else {
                this._receiveListener && this._receiveListener(msg);
            }
        })
    }

}

畢竟要面向介面程式設計嘛,這樣的話,你使用第三方的IM-SDK的話,就可以直接繼承這個IIMHandler,按介面規範在子類中實現細節就可以了。

也很簡單,對吧。

在App.js中初始化IMWebSocket

這裡面有一個IMFactory,是一個工廠類,它的create()方法返回的是WebSocketHandlerImp,這個工廠類的程式碼我就不貼了。

//app.js
import IMFactory from "./modules/im-sdk/im-factory";

App({
    globalData: {
        userInfo: {},
    },
    getIMHandler() {
        return this.iIMHandler;
    },
    onLaunch() {
        this.iIMHandler = IMFactory.create();
    },
    onHide() {
        // this.iIMHandler.closeConnection();
    },
    onShow() {
        this.iIMHandler.createConnection({options: {url: 'ws://10.4.94.185:8001'}});
    }
});

IM模擬類 im-operator.js

最後重點說下IM的控制類 IMOperator。

現在在建立IMOperator時,需要你額外傳入好友資訊,這個資訊應該是在會話列表點選時傳入的,如下所示:

 /**
     * 生命週期函式--監聽頁面載入
     */
    onLoad: function (options) {
        const friend = JSON.parse(options.friend);
        this.initData();
        wx.setNavigationBarTitle({
            title: friend.friendName
        });
        this.imOperator = new IMOperator(this, friend);//額外傳入好友資訊
        ...
        ...
    },

生成傳送的資料的文字

記住,你所有傳送的訊息和接收到的訊息,都是以文字訊息的形式,只是在渲染的時候解析,生成不同的訊息型別來展示!!!

 createChatItemContent({type = IMOperator.TextType, content = '', duration} = {}) {
         if (!content.replace(/^\s*|\s*$/g, '')) return;
         return {
             content,
             type,
             conversationId: 0,//會話id,目前未用到
             userId: getApp().globalData.userInfo.userId,
             friendId: this.getFriendId(),//好友id
             duration
         };
     }

這是生成傳送資料的文字的方法。它會返回一個物件。

  • type: 訊息型別 TextType/VoiceType/ImageType/CustomType
  • content: 需要傳送的原始文字資訊。可以是文字、語音檔案路徑、圖片檔案路徑。
  • duration: 語音時長。如果是語音型別,則需要傳這個欄位。

生成訊息物件

除自定義訊息型別外,其他的無論是自己傳送的訊息,還是好友的訊息,在UI上渲染時,都是以該訊息物件的格式來統一的。

createNormalChatItem({type = IMOperator.TextType, content = '', isMy = true, duration} = {}) {
        if (!content) return;
        const currentTimestamp = Date.now();
        const time = dealChatTime(currentTimestamp, this._latestTImestamp);
        let obj = {
            msgId: 0,//訊息id
            friendId: this.getFriendId(),//好友id
            isMy: isMy,//我傳送的訊息?
            showTime: time.ifShowTime,//是否顯示該次傳送時間
            time: time.timeStr,//傳送時間 如 09:15,
            timestamp: currentTimestamp,//該條資料的時間戳,一般用於排序
            type: type,//內容的型別,目前有這幾種型別: text/voice/image/custom | 文字/語音/圖片/自定義
            content: content,// 顯示的內容,根據不同的型別,在這裡填充不同的資訊。
            headUrl: isMy ? this._myHeadUrl : this._otherHeadUrl,//顯示的頭像,自己或好友的。
            sendStatus: 'success',//傳送狀態,目前有這幾種狀態:sending/success/failed | 傳送中/傳送成功/傳送失敗
            voiceDuration: duration,//語音時長 單位秒
            isPlaying: false,//語音是否正在播放
        };
        obj.saveKey = obj.friendId + '_' + obj.msgId;//saveKey是儲存檔案時的key
        return obj;
    }
  • type:訊息型別
  • content:需要傳送的IM訊息,是由createChatItemContent生成的JSON格式字串。
  • isMy:是否是我自己的訊息。
  • duration:語音時長。如果是語音型別,則需要傳這個欄位。

生成自定義訊息型別物件

自定義訊息型別的UI類似於會話頁面中的展示聊天時間的UI。

static createCustomChatItem() {
        return {
            timestamp: Date.now(),
            type: IMOperator.CustomType,
            content: '會話已關閉'
        }
    }

傳送資料介面

傳送資料這塊目前已經實現了WebSocket通訊。

支援傳送文字、語音、圖片及自定義型別訊息。不過因傳送檔案型別的訊息需要上傳檔案的伺服器,所以目前僅支援傳送文字訊息和自定義型別訊息。如果你自己配置好了上傳檔案的伺服器,那麼就可以傳送語音和圖片訊息了。

以傳送文字訊息為例:

首先在輸入元件的文字輸入監聽回撥介面中呼叫this.msgManager的傳送訊息方法sendMsg()

chatInput.setTextMessageListener((e) => {
            let content = e.detail.value;
            this.msgManager.sendMsg({type: IMOperator.TextType, content});
        });

sendMsg方法中會去判斷髮送的訊息型別,最終都會呼叫下面的IM傳送介面。

onSimulateSendMsg({content, success, fail}) {
        //這裡content即為要傳送的資料
        //注意:這裡的content是一個物件了,不再是一個JSON格式的字串。這樣可以在傳送訊息的底層介面中統一處理。
        getApp().getIMHandler().sendMsg({
            content,
            success: (content) => {
                //這個content格式一樣,也是一個物件
                const item = this.createNormalChatItem(content);
                this._latestTImestamp = item.timestamp;
                success && success(item);
            },
            fail
        });
    }
  • content:這裡的content是一個物件,類似於:{“content”:“233”,“type”:“text”},是由該類中createChatItemContent方法生成的。
  • success:傳送成功回撥,我這裡返回了createNormalChatItem生成的訊息物件。
  • fail:傳送失敗回撥,你可以自行傳參。

其他訊息的傳送方式都是與之類似的。

會話頁面接收資料介面

關於會話頁面訊息是怎麼接收到的,下面的展示了一個完整的主要流程。

  • 會話頁面接收訊息流程圖:

會話頁面接收訊息流程圖

下面貼的是核心程式碼

onSimulateReceiveMsg(cbOk) {
        getApp().getIMHandler().setOnReceiveMessageListener({
            listener: (msg) => {
                if (!msg) {
                    return;
                }
                msg.isMy = msg.msgUserId === getApp().globalData.userInfo.userId;
                const item = this.createNormalChatItem(msg);
                // const item = this.createNormalChatItem({type: 'voice', content: '上傳檔案返回的語音檔案路徑', isMy: false});
                // const item = this.createNormalChatItem({type: 'image', content: '上傳檔案返回的圖片檔案路徑', isMy: false});
                this._latestTImestamp = item.timestamp;
                //這裡是收到好友訊息的回撥函式,建議傳入的item是 由 createNormalChatItem 方法生成的。
                cbOk && cbOk(item);
            }
        });

    }
  • cbOk:接收到訊息的回撥,這裡我也是模擬的,返回了由this.createNormalChatItem({type: 'text', content: '這是模擬好友回覆的訊息', isMy: false})生成的訊息物件

在上面傳送資料介面程式碼中可以看到,在接收到資料時,先使用createNormalChatItem來生成訊息型別資料,然後回撥onSimulateReceiveMsgCb函式,即可完成資料的接收。

我是在chat.jsonLoad生命週期中註冊的監聽:

onLoad: function (options) {

        const friend = JSON.parse(options.friend);
        console.log(friend);
        this.initData();
        wx.setNavigationBarTitle({
            title: friend.friendName
        });
        this.imOperator = new IMOperator(this, friend);
        this.UI = new UI(this);
        this.msgManager = new MsgManager(this);

        this.imOperator.onSimulateReceiveMsg((msg) => {
            //執行到這步時,好友的訊息早已經接收到並生成了訊息型別資料msg,接下來要做的就是將資料渲染到頁面上了
            //showMsg()是用於渲染訊息型別資料的。
            this.msgManager.showMsg({msg})
        });
        this.UI.updateChatStatus('正在聊天中...');
    },

渲染訊息列表

我使用chatItems來儲存所有的訊息型別,包括自定義訊息型別。
佈局怎麼寫的我就不講了,有關UI渲染的程式碼,我全部放在了ui.js中,自己去看下吧,也都很簡單。

配合使用輸入元件。

最上面說的輸入元件,有各種互動情況下的事件回撥,在回撥函式中處理對應邏輯即可。這部分的所有程式碼我都放到了chat.js中。

以上就是客戶端WebSocket及會話頁面的所有難點內容

服務端WebSocket實現的業務功能

伺服器端是用nodejs開發的,配合客戶端實現了簡單的IM訊息展示邏輯。webSocket所有功能僅供學習和參考,若想商用,請自行開發。

請務必閱讀這部分內容,新手請自行學習依賴的安裝和gulp的初級使用。專案的執行是沒有問題的!!

如何安裝使用

1.開發者工具匯入專案
修改app.js檔案中下面配置的url為你本地網路ip
this.imWebSocket.createSocket({url: 'ws://10.4.97.87:8001'});
2. 搭建本地WebSocket服務
安裝依賴 npm install
Terminal執行 gulp 即可開啟WebSocket服務
3. 使用開發者工具執行專案

nodejs-websocket具體API詳見https://www.npmjs.com/package/nodejs-websocket

服務端簡單實現了兩人實時聊天功能,獲取歷史訊息,獲取會話列表、好友列表。需要注意的是,目前訊息都是在記憶體中,重啟服務後所有資料會重置!
目前只能兩人聊天,而且當兩個人同時線上時,後面一個人重新連線了,就會登上另外一個人的號,所以會出現自己發訊息,自己收到自己的訊息的情況。這時候你可以重啟WebSocket服務,兩臺裝置重新進下小程式就可以了。

服務端程式碼就不貼了。

最後說幾句

小程式適合開發輕量級應用,不管你怎麼推崇小程式,它都是有效能上的瓶頸問題的。比如說一個頁面中載入大量圖片會導致佔用驚人的記憶體空間,長列表的無法複用控制元件問題、重複setData()造成頁面閃動以及儲存空間限制在10M等等。因此,使用小程式開發IM,不建議將歷史訊息儲存在本地,也不建議單個頁面中載入大量的聊天訊息,更何況是有很多圖片的聊天訊息!這將造成頁面卡頓,影響小程式的響應速度!而這些東西目前是無法從技術上優化的!

不過有些東西是可以優化的。我想你的小程式肯定有這樣的使用場景,點選一個按鈕跳轉到另外一個頁面,然後在向伺服器請求獲取資料。因為要先渲染空頁面再渲染資料,頁面偶爾會閃爍。其實這個是可以優化的。即在點選按鈕時就請求資料,這樣的話,就能利用頁面跳轉的200ms左右的時間來完成資料的預載入。如果協議返回夠快的話,頁面在開啟時會立刻渲染出資料。不過問題也就來了,直接這樣做的話會讓你的頁面混有不相干的協議,違法單一職責。那麼我為此編寫了一個小程式預載入框架,讓你既能在頁面跳轉前就預載入資料,又不會破壞專案的結構。哈哈,為自己打一波廣告!

使用中有什麼問題的話,可以在部落格或GitHub上聯絡我。我會及時回覆。

謝謝大家!

合作

小程式技術交流請加QQ群:821711186

如有合作意向或是想要推廣您的產品,可加QQ:1178545208

LINK