拍拍二手闲置平台,可以将自己的闲置物品进行转让或者捐赠。想和卖家达成共识就需要涉及IM聊天。拍拍二手闲置平台目前接入的是环信IM聊天。下面我将从三个阶段带大家玩转环信IM会话。
前期
初识IM聊天
带着问题去调研
必须接入环信吗?除了环信是否可以接入其他即时通信?
环信目前有哪些功能呢?支持微信小程序吗?
如何接入小程序呢?
调研分析
必须接入环信吗?除了环信是否可以接入其他即时通信?
现状: 微信小程序API 提供了WebSocket 方法。
扩展: 如果服务端支持scoket通信,ios\android\H5 也全都支持Im聊天了
备注:专业第三方Im有融云、环信、云之讯等,底层实现均是基于scoket 通信。明白scoket通信后也可以自己写即时通信。
环信目前有哪些功能呢?支持微信小程序吗?
错误想法: 环信就是做im聊天的,咱们上去按照接入文档,开发就能搞定!!!
这种想法是很致命的。在所有的第三方组件接入中,如果我们不能跳出来看待问题,只是为了完成任务而完成任务。那么我们永远是最底层的低级码农。
环信目前是同行业里面做的算不错的。那么他的官网、接入规范都应该有的。微信小程序也是支持的。在后面小编会带领大家一切怎么去阅读一个官网
如何接入小程序?
接入小程序是否需要申请一个账号呢?我直接运行他们的demo可以吗? 怎么去测试呢? 此时我们可以有很多的猜想。我认为在开始接入之前我们应该很好的进行一些思考,答案显而易见。
环信接入思考篇
快即时慢
在工作中,大家会经常遇到第三方组件的接入。当接收到任务后,为了尽快完成任务。上来就google,找攻略,找技巧。往往认为这样做速度是最快的。结果适得其反,做了很多无用的功。我们意识中的快,结果却变成了慢
慢即时快
逆向思维: 任何一个第三方的组件,特别是一个大点的平台,他们为了推出自己的产品,一定会有各种各样的功能支持,接入文档说明。我们放慢速度,将这些资源用上半天的时间进行简单的梳理。后期的开发进度会有很大的提升。
上图是我在接入环信Im后进行的反思。因为在接入环信之前,其他团队成员用了很长的时间联调。假如他们在接入环信聊天之前,了解环信拥有自己的后台,可以直接给用户端发送测试消息;可以直接创建用户、创建聊天室、创建群组。他们还会花费那么久的时间去联调吗?完全不用依赖服务端。不用依赖ios,依赖android。自己使用环信后台,轻轻松松完成各种测试。
环信接入
环信官网注册自己的即时通讯云,并登陆后台
2.创建自己的应用,并记录关键信息
以下是关键信息哦!!!
备注:
应用标识 应用接入时会使用
IM 用户 可以创建、删除用户、发送消息
群组 可以创建、删除群组信息、发送消息
聊天室 可以创建、删除聊天室、发送消息
tip 通过这个后台管理系统,就可以玩转环信的接入测试了。
从环信下载小程序demo,替换 appkey 进行联调测试
测试走起
用户测试 在环信后台创建用户,在小程序端登录 (用户demo1 密码:123456)
一对一会话测试
① 在环信后台创建用户demo2
② 点击操作,查看用户好友将demo1和demo2 添加为好友。
③ 在小程序端用demo1给demo2发送测试消息。
④ 退出demo1用户,登录demo2查看是否会接收到demo1发送的会话
由于环信工程师们相信码农的实力,在群组测试和聊天室测试这块为大家留下了想象空间。demo 中群组测试和聊天室测试为明确写出。让我继续带大家飞
群组测试
① 创建群组记录群组id,并给群组添加成员(demo2)
② 环信后台给群组发送测试消息
③ 控制台能收到群组测试消息,怎么展示呢? 请阅读源码解析篇
4.聊天室测试
① 创建聊天室记录聊天室id,将demo1 设置为超级管理员,demo2设置为管理员
② 聊天室这里没有聊天室消息的发送。请阅读源码解析篇
通过以上4个简单的测试,android、ios、h5、小程序的聊天测试均可以参照以上4点进行顺利的测试。初期就此结束。下面带代价进行源码的解析
中期
看源码前期思考
核心源码阅读
以上是环信sdk 基础代码结构。 通过简单阅读会发现:
环信的scoket 通信也使用了微信小程序暴露的scoket 通信 (猜想 android、ios 其他端也有对应的scoket通信)
环信的api包装在connection.js 组件中,如果某些api没有,咱们可以扩展connection 中的方法
环信核心代码阅读完成后,发现没有涉及到缓存。看来缓存的处理是在对应的业务逻辑中。
设想:
消息应该在哪里缓存
哪里进行会话链接的监听注册
环信demo 代码阅读
会话、群组
通过前面提到的方式,大家可以在小程序控制台抓取到用户收到的会话和群组消息
会话
app.js
环信scoket 注册监听代码在app.js 中
核心代码如下:
{ //调用API从本地缓存中获取数据 var that = this var logs = wx.getStorageSync('logs') || [] logs.unshift(Date.now()) wx.setStorageSync('logs', logs) WebIM.conn.listen({ onOpened: function (message) {//连接成功回调 // 如果isAutoLogin设置为false,那么必须手动设置上线,否则无法收消息 // 手动上线指的是调用conn.setPresence(); 如果conn初始化时已将isAutoLogin设置为true // 则无需调用conn.setPresence(); WebIM.conn.setPresence() }, onPresence: function (message) { //处理“广播”或“发布-订阅”消息,如联系人订阅请求、处理群组、聊天室被踢解散等消息 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 从本地缓存中获取用户的消息 发消息+来源 适用于单人会话 msgData.yourname + message.to+当前登录人 群组/聊天室 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; } }, }) }
实际开发过程中,在微信中,退出小程序,重新进入时,webscoket 通信并没有重新创建链接。存在用户收到不到消息的情况。可以将以上代码封装,例如addHXLIstener(...)。当用户重新打开后,再次注册环信监听即可。
拍拍二手闲置交易平台,主要集成的是文本聊天功能。
环信登录 例如 initLoginHX();
var uin=wx.getStorageSync('hxuin'); var pwd=wx.getStorageSync('hxpwd'); console.log('initHX:' + uin+"||"+pwd); var options = { apiUrl: '服务器url', user: '用户名',// 用户名要是字符 pwd: '密码', grant_type: 'password', appKey: 'appkey', success: function (res) { console.log("环信创建连接成功") }, error: function (res) { console.log("环信创建连接失败") } }; WebIM.conn.open(options);
chat 会话
环信的会话列表存储在本地,并没有调用服务器端数据
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 })
通过以上代码得出结论: 环信的会话是通过遍历用户id+对方id 构成的数据。
那群组和聊天室的怎么处理呢?
环信小程序demo中只提供了聊天室列表的获取接口我们可以轻松实现聊天室列表,并没有提供群组列表的获取方式。我们需要在conection中扩展调用群组列表的接口,来实现群组列表。参照聊天室列表获取即可实现。聊天室列表实现方式如下:
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 }; // 想要实现群组列表,修改对应接口即可 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
从本地缓存中获取聊天记录,并展示
// 环信demo 发送消息 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); // 消息发送完成 if (msg) { var value = WebIM.parseEmoji(msg.value.replace(/\n/mg, '')) // 环信表情处理 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 对方环信uin+自己的uin // 注: 群组聊天 key 群组id\聊天室id+对方环信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: '' }) } },// 环信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'){ // 如果是音频则请求服务器转码 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 对方环信uin+自己的uin // 注: 群组聊天 key 群组id\聊天室id+对方环信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) } }) } },
环信聊天页面,聊天数据全部存储在缓存当中,跟进聊天类型的不同,主要需要调整缓存的key。详情如下:
单对单聊天 对方uin+自己的uin
群组聊天(针对某个商品,不需要好友关系,只需要临时聊天) 群组id+对方uin+自己的uin
聊天室(同群组聊天)
问题大杂烩
群组聊天缓存如何存储?
答: 缓存key 设置为 群组id+对方uin+自己的uin
聊天时,如何在聊天中携带扩展信息
答: 消息内容中,ext 支持用户自定义参数传递
var option = { msg: data.userMessage.trim(), // 消息内容 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'); } };```
会话列表如何实现?
答: 通过接口获取环信的群组列表,通过自己的服务器端补全对应的会话信息。
回顾
整个环信接入,整体围绕 假设-->猜想-->实践完成的。仔细阅读官网http://www.easemob.com,会为大家节约很多时间
作者:贾慧斌
链接:https://www.jianshu.com/p/8919316d26b8
来源:简书