IM 聊天接入思考過程
前期
初識IM聊天
帶著問題去調(diào)研
- 必須接入環(huán)信嗎?除了環(huán)信是否可以接入其他即時通信?
- 環(huán)信目前有哪些功能呢?支持微信小程序嗎棋嘲?
- 如何接入小程序呢?
調(diào)研分析
- 必須接入環(huán)信嗎矩桂?除了環(huán)信是否可以接入其他即時通信?
現(xiàn)狀: 微信小程序API 提供了WebSocket 方法沸移。
擴展: 如果服務(wù)端支持scoket通信,ios\android\H5 也全都支持Im聊天了
備注:專業(yè)第三方Im有融云、環(huán)信、云之訊等雹锣,底層實現(xiàn)均是基于scoket 通信网沾。明白scoket通信后也可以自己寫即時通信。
- 環(huán)信目前有哪些功能呢蕊爵?支持微信小程序嗎辉哥?
錯誤想法: 環(huán)信就是做im聊天的,咱們上去按照接入文檔攒射,開發(fā)就能搞定4椎!会放!
這種想法是很致命的饲齐。在所有的第三方組件接入中,如果我們不能跳出來看待問題鸦概,只是為了完成任務(wù)而完成任務(wù)箩张。那么我們永遠是最底層的低級碼農(nóng)。
環(huán)信目前是同行業(yè)里面做的算不錯的窗市。那么他的官網(wǎng)先慷、接入規(guī)范都應(yīng)該有的。微信小程序也是支持的咨察。在后面小編會帶領(lǐng)大家一切怎么去閱讀一個官網(wǎng)
- 如何接入小程序论熙?
接入小程序是否需要申請一個賬號呢?我直接運行他們的demo可以嗎摄狱? 怎么去測試呢? 此時我們可以有很多的猜想脓诡。我認為在開始接入之前我們應(yīng)該很好的進行一些思考,答案顯而易見媒役。
環(huán)信接入思考篇
快即時慢
在工作中祝谚,大家會經(jīng)常遇到第三方組件的接入。當(dāng)接收到任務(wù)后酣衷,為了盡快完成任務(wù)交惯。上來就google,找攻略穿仪,找技巧席爽。往往認為這樣做速度是最快的。結(jié)果適得其反啊片,做了很多無用的功只锻。我們意識中的快,結(jié)果卻變成了慢
慢即時快
逆向思維: 任何一個第三方的組件紫谷,特別是一個大點的平臺齐饮,他們?yōu)榱送瞥鲎约旱漠a(chǎn)品捐寥,一定會有各種各樣的功能支持,接入文檔說明沈矿。我們放慢速度上真,將這些資源用上半天的時間進行簡單的梳理咬腋。后期的開發(fā)進度會有很大的提升羹膳。
上圖是我在接入環(huán)信Im后進行的反思。因為在接入環(huán)信之前根竿,其他團隊成員用了很長的時間聯(lián)調(diào)陵像。假如他們在接入環(huán)信聊天之前,了解環(huán)信擁有自己的后臺寇壳,可以直接給用戶端發(fā)送測試消息;可以直接創(chuàng)建用戶醒颖、創(chuàng)建聊天室、創(chuàng)建群組壳炎。他們還會花費那么久的時間去聯(lián)調(diào)嗎?完全不用依賴服務(wù)端泞歉。不用依賴ios,依賴android匿辩。自己使用環(huán)信后臺腰耙,輕輕松松完成各種測試。
環(huán)信接入
-
環(huán)信官網(wǎng)注冊自己的即時通訊云铲球,并登陸后臺
image -
創(chuàng)建自己的應(yīng)用挺庞,并記錄關(guān)鍵信息
image
以下是關(guān)鍵信息哦!<诓 选侨!
image
備注:
- 應(yīng)用標識 應(yīng)用接入時會使用
- IM 用戶 可以創(chuàng)建、刪除用戶然走、發(fā)送消息
- 群組 可以創(chuàng)建援制、刪除群組信息、發(fā)送消息
- 聊天室 可以創(chuàng)建芍瑞、刪除聊天室晨仑、發(fā)送消息
tip 通過這個后臺管理系統(tǒng),就可以玩轉(zhuǎn)環(huán)信的接入測試了啄巧。
-
從環(huán)信下載小程序demo寻歧,替換 appkey 進行聯(lián)調(diào)測試
image - 測試走起
用戶測試 在環(huán)信后臺創(chuàng)建用戶,在小程序端登錄 (用戶demo1 密碼:123456)
image一對一會話測試
① 在環(huán)信后臺創(chuàng)建用戶demo2
② 點擊操作秩仆,查看用戶好友將demo1和demo2 添加為好友码泛。
③ 在小程序端用demo1給demo2發(fā)送測試消息。
④ 退出demo1用戶澄耍,登錄demo2查看是否會接收到demo1發(fā)送的會話
image
由于環(huán)信工程師們相信碼農(nóng)的實力噪珊,在群組測試和聊天室測試這塊為大家留下了想象空間晌缘。demo 中群組測試和聊天室測試為明確寫出。讓我繼續(xù)帶大家飛
群組測試
① 創(chuàng)建群組記錄群組id痢站,并給群組添加成員(demo2)
image
② 環(huán)信后臺給群組發(fā)送測試消息
image
③ 控制臺能收到群組測試消息磷箕,怎么展示呢? 請閱讀源碼解析篇- 聊天室測試
① 創(chuàng)建聊天室記錄聊天室id,將demo1 設(shè)置為超級管理員,demo2設(shè)置為管理員
② 聊天室這里沒有聊天室消息的發(fā)送阵难。請閱讀源碼解析篇
通過以上4個簡單的測試岳枷,android、ios呜叫、h5空繁、小程序的聊天測試均可以參照以上4點進行順利的測試。初期就此結(jié)束朱庆。下面帶代價進行源碼的解析
中期
看源碼前期思考
核心源碼閱讀
以上是環(huán)信sdk 基礎(chǔ)代碼結(jié)構(gòu)盛泡。 通過簡單閱讀會發(fā)現(xiàn):
- 環(huán)信的scoket 通信也使用了微信小程序暴露的scoket 通信 (猜想 android、ios 其他端也有對應(yīng)的scoket通信)
- 環(huán)信的api包裝在connection.js 組件中娱颊,如果某些api沒有傲诵,咱們可以擴展connection 中的方法
環(huán)信核心代碼閱讀完成后,發(fā)現(xiàn)沒有涉及到緩存箱硕∷┲瘢看來緩存的處理是在對應(yīng)的業(yè)務(wù)邏輯中。
設(shè)想:
- 消息應(yīng)該在哪里緩存
- 哪里進行會話鏈接的監(jiān)聽注冊
環(huán)信demo 代碼閱讀
會話颅痊、群組
通過前面提到的方式殖熟,大家可以在小程序控制臺抓取到用戶收到的會話和群組消息
會話
app.js
環(huán)信scoket 注冊監(jiān)聽代碼在app.js 中
核心代碼如下:
{
//調(diào)用API從本地緩存中獲取數(shù)據(jù)
var that = this
var logs = wx.getStorageSync('logs') || []
logs.unshift(Date.now())
wx.setStorageSync('logs', logs)
WebIM.conn.listen({
onOpened: function (message) {//連接成功回調(diào)
// 如果isAutoLogin設(shè)置為false,那么必須手動設(shè)置上線斑响,否則無法收消息
// 手動上線指的是調(diào)用conn.setPresence(); 如果conn初始化時已將isAutoLogin設(shè)置為true
// 則無需調(diào)用conn.setPresence();
WebIM.conn.setPresence()
},
onPresence: function (message) { //處理“廣播”或“發(fā)布-訂閱”消息菱属,如聯(lián)系人訂閱請求、處理群組舰罚、聊天室被踢解散等消息
switch(message.type){
case "unsubscribe":
pages[0].moveFriend(message);
break;
case "subscribe":
if (message.status === '[resp:true]') {
return
} else {
pages[0].handleFriendMsg(message)
}
break;
case "joinChatRoomSuccess":
console.log('Message: ', message);
wx.showToast({
title: "JoinChatRoomSuccess",
});
break;
case "memberJoinChatRoomSuccess":
console.log('memberMessage: ', message);
wx.showToast({
title: "memberJoinChatRoomSuccess",
});
break;
case "memberLeaveChatRoomSuccess":
console.log("LeaveChatRoom");
wx.showToast({
title: "leaveChatRoomSuccess",
});
break;
}
},
onRoster: function (message) { //處理好友申請
var pages = getCurrentPages()
if (pages[0]) {
pages[0].onShow()
}
},
onVideoMessage: function(message){ //視頻處理
console.log('onVideoMessage: ', message);
var page = that.getRoomPage()
if (message) {
if (page) {
page.receiveVideo(message, 'video')
} else {
var chatMsg = that.globalData.chatMsg || []
var time = WebIM.time()
var msgData = {
info: {
from: message.from,
to: message.to
},
username: message.from,
yourname: message.from,
msg: {
type: 'video',
data: message.url
},
style: '',
time: time,
mid: 'video' + message.id
}
msgData.style = ''
chatMsg = wx.getStorageSync(msgData.yourname + message.to) || []
chatMsg.push(msgData)
wx.setStorage({
key: msgData.yourname + message.to,
data: chatMsg,
success: function () {
//console.log('success')
}
})
}
}
},
onAudioMessage: function (message) { // 音頻處理
console.log('onAudioMessage', message)
var page = that.getRoomPage()
console.log(page)
if (message) {
if (page) {
page.receiveMsg(message, 'audio')
} else {
var chatMsg = that.globalData.chatMsg || []
var value = WebIM.parseEmoji(message.data.replace(/\n/mg, ''))
var time = WebIM.time()
var msgData = {
info: {
from: message.from,
to: message.to
},
username: message.from,
yourname: message.from,
msg: {
type: 'audio',
data: value
},
style: '',
time: time,
mid: 'audio' + message.id
}
console.log("Audio msgData: ", msgData);
chatMsg = wx.getStorageSync(msgData.yourname + message.to) || []
chatMsg.push(msgData)
wx.setStorage({
key: msgData.yourname + message.to,
data: chatMsg,
success: function () {
//console.log('success')
}
})
}
}
},
onLocationMessage: function (message) { // 收到位置信息
console.log("Location message: ", message);
},
onTextMessage: function (message) {//收到文本消息
var page = that.getRoomPage()
console.log(page)
if (message) {
if (page) {
page.receiveMsg(message, 'txt')
} else {
var chatMsg = that.globalData.chatMsg || []
var value = WebIM.parseEmoji(message.data.replace(/\n/mg, ''))
var time = WebIM.time()
var msgData = {
info: {
from: message.from,
to: message.to
},
username: message.from,
yourname: message.from,
msg: {
type: 'txt',
data: value
},
style: '',
time: time,
mid: 'txt' + message.id
}
chatMsg = wx.getStorageSync(msgData.yourname + message.to) || []
chatMsg.push(msgData)
wx.setStorage({
key: msgData.yourname + message.to,
data: chatMsg,
success: function () {
//console.log('success')
}
})
}
}
},
onEmojiMessage: function (message) { //收到表情信息
//console.log('onEmojiMessage',message)
var page = that.getRoomPage()
//console.log(pages)
if (message) {
if (page) {
page.receiveMsg(message, 'emoji')
} else {
var chatMsg = that.globalData.chatMsg || []
var time = WebIM.time()
var msgData = {
info: {
from: message.from,
to: message.to
},
username: message.from,
yourname: message.from,
msg: {
type: 'emoji',
data: message.data
},
style: '',
time: time,
mid: 'emoji' + message.id
}
msgData.style = ''
chatMsg = wx.getStorageSync(msgData.yourname + message.to) || [] //tip 從本地緩存中獲取用戶的消息 發(fā)消息+來源 適用于單人會話 msgData.yourname + message.to+當(dāng)前登錄人 群組/聊天室
chatMsg.push(msgData)
//console.log(chatMsg)
wx.setStorage({
key: msgData.yourname + message.to,
data: chatMsg,
success: function () {
//console.log('success')
}
})
}
}
},
onPictureMessage: function (message) {//收到圖片信息
//console.log('Picture',message);
var page = that.getRoomPage()
if (message) {
if (page) {
//console.log("wdawdawdawdqwd")
page.receiveImage(message, 'img')
} else {
var chatMsg = that.globalData.chatMsg || []
var time = WebIM.time()
var msgData = {
info: {
from: message.from,
to: message.to
},
username: message.from,
yourname: message.from,
msg: {
type: 'img',
data: message.url
},
style: '',
time: time,
mid: 'img' + message.id
}
msgData.style = ''
chatMsg = wx.getStorageSync(msgData.yourname + message.to) || []
chatMsg.push(msgData)
wx.setStorage({
key: msgData.yourname + message.to,
data: chatMsg,
success: function () {
//console.log('success')
}
})
}
}
},
// 各種異常
onError: function (error) {
// 16: server-side close the websocket connection
if (error.type == WebIM.statusCode.WEBIM_CONNCTION_DISCONNECTED) {
if (WebIM.conn.autoReconnectNumTotal < WebIM.conn.autoReconnectNumMax) {
return;
}
wx.showToast({
title: 'server-side close the websocket connection',
duration: 1000
});
wx.redirectTo({
url: '../login/login'
});
return;
}
// 8: offline by multi login
if (error.type == WebIM.statusCode.WEBIM_CONNCTION_SERVER_ERROR) {
wx.showToast({
title: 'offline by multi login',
duration: 1000
})
wx.redirectTo({
url: '../login/login'
})
return;
}
},
})
}
實際開發(fā)過程中纽门,在微信中,退出小程序赏陵,重新進入時,webscoket 通信并沒有重新創(chuàng)建鏈接饲漾。存在用戶收到不到消息的情況蝙搔。可以將以上代碼封裝考传,例如addHXLIstener(...)吃型。當(dāng)用戶重新打開后,再次注冊環(huán)信監(jiān)聽即可僚楞。
環(huán)信登錄 例如 initLoginHX();
var uin=wx.getStorageSync('hxuin');
var pwd=wx.getStorageSync('hxpwd');
console.log('initHX:' + uin+"||"+pwd);
var options = {
apiUrl: '服務(wù)器url',
user: '用戶名',// 用戶名要是字符
pwd: '密碼',
grant_type: 'password',
appKey: 'appkey',
success: function (res) {
console.log("環(huán)信創(chuàng)建連接成功")
},
error: function (res) {
console.log("環(huán)信創(chuàng)建連接失敗")
}
};
WebIM.conn.open(options);
chat 會話
環(huán)信的會話列表存儲在本地勤晚,并沒有調(diào)用服務(wù)器端數(shù)據(jù)
var that = this
var member = wx.getStorageSync('member')
var myName = wx.getStorageSync('myUsername')
var array = []
for (var i = 0; i < member.length; i++) {
if (wx.getStorageSync(member[i].name + myName) != '') {
array.push(wx.getStorageSync(member[i].name + myName)[wx.getStorageSync(member[i].name + myName).length - 1])
}
}
//console.log(array枉层,'1')
this.setData({
arr: array
})
通過以上代碼得出結(jié)論: 環(huán)信的會話是通過遍歷用戶id+對方id 構(gòu)成的數(shù)據(jù)。
那群組和聊天室的怎么處理呢赐写?
環(huán)信小程序demo中只提供了聊天室列表的獲取接口我們可以輕松實現(xiàn)聊天室列表鸟蜡,并沒有提供群組列表的獲取方式。我們需要在conection中擴展調(diào)用群組列表的接口挺邀,來實現(xiàn)群組列表揉忘。參照聊天室列表獲取即可實現(xiàn)。聊天室列表實現(xiàn)方式如下:
connection.prototype.getChatRooms = function (options) {
var conn = this,
token = options.accessToken || this.context.accessToken;
if (token) {
var apiUrl = this.apiUrl;
var appName = this.context.appName;
var orgName = this.context.orgName;
if (!appName || !orgName) {
conn.onError({
type: _code.WEBIM_CONNCTION_AUTH_ERROR
});
return;
}
var suc = function (data, xhr) {
typeof options.success === 'function' && options.success(data);
};
var error = function (res, xhr, msg) {
if (res.error && res.error_description) {
conn.onError({
type: _code.WEBIM_CONNCTION_LOAD_CHATROOM_ERROR,
msg: res.error_description,
data: res,
xhr: xhr
});
}
};
var pageInfo = {
pagenum: parseInt(options.pagenum) || 1,
pagesize: parseInt(options.pagesize) || 20
};
// 想要實現(xiàn)群組列表悠夯,修改對應(yīng)接口即可
var opts = {
url: apiUrl + '/' + orgName + '/' + appName + '/chatrooms',
dataType: 'json',
type: 'GET',
header: {'Authorization': 'Bearer ' + token},
data: pageInfo,
success: suc || _utils.emptyfn,
fail: error || _utils.emptyfn
};
wx.request(opts);
} else {
conn.onError({
type: _code.WEBIM_CONNCTION_TOKEN_NOT_ASSIGN_ERROR
});
}
chatroom
從本地緩存中獲取聊天記錄癌淮,并展示
// 環(huán)信demo 發(fā)送消息
sendMessage: function () {
if (!this.data.userMessage.trim()) return;
var that = this
// //console.log(that.data.userMessage)
// //console.log(that.data.sendInfo)
var myName = wx.getStorageSync('myUsername')
var id = WebIM.conn.getUniqueId();
var msg = new WebIM.message('txt', id);
msg.set({
msg: that.data.sendInfo,
to: that.data.yourname,
roomType: false,
success: function (id, serverMsgId) {
console.log('send text message success')
}
});
// //console.log(msg)
console.log("Sending textmessage")
msg.body.chatType = 'singleChat'; // 群組聊天 groupRoom
WebIM.conn.send(msg.body);
// 消息發(fā)送完成
if (msg) {
var value = WebIM.parseEmoji(msg.value.replace(/\n/mg, '')) // 環(huán)信表情處理
var time = WebIM.time()
var msgData = {
info: {
to: msg.body.to
},
username: that.data.myName,
yourname: msg.body.to,
msg: {
type: msg.type,
data: value
},
style: 'self',
time: time,
mid: msg.id
}
that.data.chatMsg.push(msgData)
// console.log(that.data.chatMsg)
// 存儲聊天記錄
// 注: 單獨單聊天 key 對方環(huán)信uin+自己的uin
// 注: 群組聊天 key 群組id\聊天室id+對方環(huán)信uin+自己的uin
wx.setStorage({
key: that.data.yourname + myName,
data: that.data.chatMsg,
success: function () {
//console.log('success', that.data)
that.setData({
chatMsg: that.data.chatMsg,
emojiList: [],
inputMessage: ''
})
setTimeout(function () {
that.setData({
toView: that.data.chatMsg[that.data.chatMsg.length - 1].mid
})
}, 100)
}
})
that.setData({
userMessage: ''
})
}
},
// 環(huán)信demo 收到消息
receiveMsg: function (msg, type) {
var that = this
var myName = wx.getStorageSync('myUsername')
if (msg.from == that.data.yourname || msg.to == that.data.yourname) {
if (type == 'txt') {
var value = WebIM.parseEmoji(msg.data.replace(/\n/mg, ''))
} else if (type == 'emoji') {
var value = msg.data
} else if(type == 'audio'){
// 如果是音頻則請求服務(wù)器轉(zhuǎn)碼
console.log('Audio Audio msg: ', msg);
var token = msg.accessToken;
console.log('get token: ', token)
var options = {
url: msg.url,
header: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'audio/mp3',
'Authorization': 'Bearer ' + token
},
success: function(res){
console.log('downloadFile success Play', res);
// wx.playVoice({
// filePath: res.tempFilePath
// })
msg.url = res.tempFilePath
var msgData = {
info: {
from: msg.from,
to: msg.to
},
username: '',
yourname: msg.from,
msg: {
type: type,
data: value,
url: msg.url
},
style: '',
time: time,
mid: msg.type + msg.id
}
if (msg.from == that.data.yourname) {
msgData.style = ''
msgData.username = msg.from
} else {
msgData.style = 'self'
msgData.username = msg.to
}
var msgArr = that.data.chatMsg;
msgArr.pop();
msgArr.push(msgData);
that.setData({
chatMsg: that.data.chatMsg,
})
console.log("New audio");
},
fail: function(e){
console.log('downloadFile failed', e);
}
};
console.log('Download');
wx.downloadFile(options);
}
//console.log(msg)
//console.log(value)
var time = WebIM.time()
var msgData = {
info: {
from: msg.from,
to: msg.to
},
username: '',
yourname: msg.from,
msg: {
type: type,
data: value,
url: msg.url
},
style: '',
time: time,
mid: msg.type + msg.id
}
console.log('Audio Audio msgData: ', msgData);
if (msg.from == that.data.yourname) {
msgData.style = ''
msgData.username = msg.from
} else {
msgData.style = 'self'
msgData.username = msg.to
}
//console.log(msgData, that.data.chatMsg, that.data)
that.data.chatMsg.push(msgData)
// 存儲聊天記錄
// 注: 單獨單聊天 key 對方環(huán)信uin+自己的uin
// 注: 群組聊天 key 群組id\聊天室id+對方環(huán)信uin+自己的uin
wx.setStorage({
key: that.data.yourname + myName,
data: that.data.chatMsg,
success: function () {
if(type == 'audio')
return;
//console.log('success', that.data)
that.setData({
chatMsg: that.data.chatMsg,
})
setTimeout(function () {
that.setData({
toView: that.data.chatMsg[that.data.chatMsg.length - 1].mid
})
}, 100)
}
})
}
},
環(huán)信聊天頁面躺坟,聊天數(shù)據(jù)全部存儲在緩存當(dāng)中沦补,跟進聊天類型的不同,主要需要調(diào)整緩存的key咪橙。詳情如下:
- 單對單聊天 對方uin+自己的uin
- 群組聊天(針對某個商品夕膀,不需要好友關(guān)系,只需要臨時聊天) 群組id+對方uin+自己的uin
- 聊天室(同群組聊天)
問題大雜燴
- 群組聊天緩存如何存儲美侦?
答: 緩存key 設(shè)置為 群組id+對方uin+自己的uin
- 聊天時产舞,如何在聊天中攜帶擴展信息
答: 消息內(nèi)容中,ext 支持用戶自定義參數(shù)傳遞
var option = {
msg: data.userMessage.trim(), // 消息內(nèi)容
to: data.groupId, // 接收消息對象(聊天室id)
roomType: true,
chatType: 'groupRoom',
from: data.myuin,
ext: {
//todo 需要補充的字符哦
},
success: function () {
console.log('send room text success');
},
fail: function () {
console.log('failed');
}
};
```
- 會話列表如何實現(xiàn)菠剩?
答: 通過接口獲取環(huán)信的群組列表易猫,通過自己的服務(wù)器端補全對應(yīng)的會話信息。
回顧
整個環(huán)信接入具壮,整體圍繞 假設(shè)-->猜想-->實踐完成的准颓。仔細閱讀官網(wǎng),會為大家節(jié)約很多時間