小程式即時通訊聊天控制元件(一)
小程式即時通訊(一)輸入元件及使用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-input
和image
資料夾下,示例頁面是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連線和會話列表頁收發訊息流程圖:
程式碼非常簡單。
// 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
是以訊息idmsgId
和好友idfriendId
,拼接而成的。 -
儲存溢位演算法:在儲存檔案時,我參考了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.js
的onLoad
生命週期中註冊的監聽:
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