最近参与了公司开发的一个微信小程序项目,也是第一次真正实战微信小程序。由于之前没有怎么专门学习 html 和 css,所以页面交互与显示就交给了公司前端工程师,我主要负责 js 逻辑开发。在这里简单总结一下开发中用到的知识,同时也记录一下“微信小程序集成腾讯云IM”,实现简单的文字聊天。之前写过一篇“微信小程序开发小结”,里面写了一些比较细节上的小技巧,对于刚刚上手的可以参考一下。
那么废话不多说,下面开始进入正题:
一、 小程序授权登录
-
在默认首屏界面的中添加授权提示,设置按钮( Button 标签)open-type = "getUserInfo",同时设置 bindgetuserinfo = "getuserinfo" (回调函数)来接收授权后的用户信息。在这个授权提示的样式中最好添加一个变量用于控制是否显示,这里我用 isShowAuthorize 来进行控制,true 为显示授权提示;false 为不显示授权提示。
-
在首屏界面的 onLoad 或者 onShow 方法中添加授权逻辑,这里单独一个方法( getAuthorize() )来处理,该方法中设置 isShowAuthorize 为 true,然后走微信的获取用户信息逻辑,具体看下面代码:
wx.getSetting({
success: res => {
if (res.authSetting['scope.userInfo']) {
// 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框
wx.getUserInfo({
success: res => {
/**
* res 中包含微信用户信息
* ① 将信息保存起来
* ② isShowAuthorize 设置为 false
* ③ wx.login 得到 code
* ④ 利用 code 获取服务端用户信息
*/
... ...
}
})
} else {
//显示授提示 - isShowAuthorize 设置为 true
}
}
})
- 不要忘记在授权按钮中设置了 bindgetuserinfo="getuserinfo" 的回调函数,这里也要处理用户点击了授权后的逻辑。
getuserinfo: function(e) {
if (e.detail.userInfo) {
// e.detail.userInfo 中包含了微信用户信息,剩余的步骤有 2. 中的已授权逻辑相同
} else {
// 用户按了拒绝按钮,isShowAuthorize 为 false,用于继续提醒用户授权
}
}
二、关于 Canvas 绘制图片
微信小程序虽然提供了 Canvas API,但是友情提醒请谨慎使用,最好将绘图工作交由后端处理,然后通过图片地址进行显示。在我们公司的小程序项目中,利用 Canvas 绘图主要出现两个问题:①. 绘图不稳定,在安卓手机上能完成绘图并显示,但在 ios 中会偶然出现绘图完成却不显示;②. 绘图太依靠手机性能,在部分低端手机中会出现绘制图片时间过长的情况。
三、小程序组件化开发
使用组件化让项目代码更加直观,但是唯一缺点就是需要频繁的设置数据监听,通过监听来实现业务逻辑。至于如何使用可参考官方文档-自定义组件。这里主要说一下“组件声明周期”。
//下面代码中体现的是组件的生命周期,主要监听的是组件添加到页面节点、组件从页面节点移除,
//这就说明了如果你通过 hidden 属性在 wxml 中来显示隐藏组件并不会触发上线的生命周期。
lifetimes: {
attached() {
// 在组件实例进入页面节点树时执行
},
detached() {
// 在组件实例被从页面节点树移除时执行
},
},
// 以下是旧式的定义方式,可以保持对 <2.2.3 版本基础库的兼容
attached() {
// 在组件实例进入页面节点树时执行
},
detached() {
// 在组件实例被从页面节点树移除时执行
}
// 下面代码中体现的是组件在页面中的生命周期。如果你想通过这个生命周期监听来实现组件的显示隐藏,
//那么恭喜你还是不行...这个生命周期监听仅仅在页面跳转时才会被触发,即从这个组件跳转到另一个界面时触发。
//如果你想监听组件的显示隐藏,只能在页面中添加属性,然后在组件中监听该属性来实现显示隐藏状态的业务逻辑。
pageLifetimes: {
show() {
// 页面被展示
},
hide() {
// 页面被隐藏
},
resize(size) {
// 页面尺寸变化
}
}
四、说点零碎的点
-
开发中可能会用到“广播/监听”来实现跨组件或跨界面的实时数据更新。这里给大家推荐一个开源框架 WxNotificationCenter ,使用方便。当然如果你看了源码可能会吐槽:这么简单个 js 我也能写啊。我想说既然人家已经给你写好了,咱们也没必要重复造轮子了,你说是吧...
-
开发中难免会碰到对时间戳的处理,这里再给大家推荐一个开源框架 moment,里面综合了很多常用的时间戳处理方法,它提供了官网与官方文档方便大家查看。
五、来说说腾讯云IM
本项目中涉及到了集成腾讯云 IM 实现简单的文字聊天。在开发中发现了很多好玩的东西,先看一下基础实现,主要包括:①. 腾讯 IM 集成;②. IM 登录/登出;③. 获取最近联系人;④. 未读消息;⑤. 历史消息。
-
腾讯 IM 集成:首先从 github 下载官方 SDK( SDK 地址),然后从 WXMini 文件夹中将 webim_wx.js 和 webim_handler.js 复制到自己项目里合适的目录中。
解释下这两个文件,webim_wx 中是腾讯 IM 的核心文件,里面包含了所有的 IM 功能,webim_handler 中是腾讯官方给封装好的 IM 功能,方便调用。这里由于项目只需实现文字聊天,我将 webim_handler 进行了删减和修改,你也可以根据自己的业务逻辑进行删减与修改。
如果你看过 webim_wx.js 的代码或者看控制台的 Network 打印,你会发现原来小程序版本的 IM 是通过轮询实现的...据腾讯云官方解释:目前仅提供轮询的方式进行实时通讯,socket 方式正在研发中。 -
IM 登录/登出:这里可以根据官方文档来实现。在本项目中仅仅是 c2c 文字聊天,所以监听仅添加了连接状态监听和新消息监听。注意!①. 所有的后续操作(最新联系人、历史消息、聊天等等)都要求在 IM 登录成功之后才能实现,不然会报“tinyid 或 a2 不能为空”的异常。②. 在新消息监听回调中不要出现业务堵塞情况,如果出现堵塞情况会导致轮询心跳超时而被停止,这样用户就无法收到最新消息。
-
获取最新联系人:通过 webim_handler.js 中的方法即可实现,但是需要注意细节,如果有未读消息需要添加标识,通过 IM 获取到的最近联系人中的头像和昵称需要服务端提供等。还需要注意最新联系人最多是100条,而且没有分页,所以需要注意处理数据刷新问题。这里直接上代码比较直观。
//获取最新的联系人
webimhandler.getRecentContactList(function(res) {
var unReadAccountList = []
var imMsgList = new Array()
//获取所有会话
var sessMap = webim.MsgStore.sessMap();
for (var index in sessMap) {
//判断该会话中是否包含未读信息
if (sessMap[index].unread() > 0) {
//将未读消息的 account 存起来
unReadAccountList.push(sessMap[index]._impl.id)
}
}
//这里最好在 data 中保存一份方便在别处使用
that.setData({
unReadAccounts: unReadAccountList
})
//循环最新联系人
for (var i in res.SessionItem) {
var msgState = false
var recentContact = res.SessionItem[i]
//循环未读消息 account
for (var j in that.data.unReadAccounts) {
//若最新联系人中包含未读消息的 account ,那么设置一个状态标识
if (that.data.unReadAccounts[j] == recentContact.To_Account) {
msgState = true
}
}
//将最新联系人进行数据封装,用于稍后从服务端获取头像和昵称
var recentContactBean = {
account: recentContact.To_Account,
headImg: '../../../images/default_headPic.png',
nickName: '****',
lastMsg: recentContact.MsgShow,
lastTime: util.compareTime(recentContact.MsgTimeStamp),
newMsgState: msgState
}
imMsgList.push(recentContactBean)
}
if (imMsgList.length > 0) {
wx.showLoading({
title: '正在加载...',
})
//下面这一坨代码主要用于判断是否需要重新获取头像和昵称,防止频繁调用服务端接口
var oldRecentContactList = that.data.recentContactList
if (oldRecentContactList == null || oldRecentContactList.length == 0) {
//第一次进入
that.setData({
recentContactList: imMsgList,
}, function() {
//在 setData 中设置 function 可以实现,set完成数据之后的操作,这里是从服务端获取头像和昵称
that.getRecentContact(0)
})
} else {
for (var i in imMsgList) {
var recentContact = imMsgList[i]
//判断之前的数据中是否包含最近联系人,若包含则返回当前坐标,若不包含则返回-1
var beanIndex = oldRecentContactList.findIndex(function(currentValue) {
return currentValue.account == recentContact.account
})
if (beanIndex != -1) {
//不是新最新联系人,但是有最新的消息,则头像和昵称使用原数据
if (oldRecentContactList[beanIndex].lastMsg != recentContact.lastMsg) {
var oldContactBean = oldRecentContactList[beanIndex]
oldContactBean.lastMsg = recentContact.lastMsg
oldContactBean.lastTime = recentContact.lastTime
//将之前的数据移除,将最新的数据添加到顶部
oldRecentContactList.splice(beanIndex, 1)
oldRecentContactList.unshift(oldContactBean)
}
} else {
//新增加的最近联系人,直接添加到顶部
oldRecentContactList.unshift(recentContact)
}
}
//刷新数据
that.setData({
recentContactList: oldRecentContactList
}, function() {
that.getRecentContact(0)
})
}
}
}, function(res) {
//获取最近联系人失败
wx.hideLoading()
console.table(res)
})
//从服务端获取用户信息
getRecentContact(index) {
var that = this
var recentContact = that.data.recentContactList[index]
if (recentContact.nickName == '****') {
//网络请求服务端,这里我做了封装,你可以在 wx.request 中实现
api.getPersonInfoByUserId(recentContact.account).then(function(res) {
//通过 key 来直接修改对象中的数据
var updateNickStr = 'recentContactList[' + index + '].nickName'
var updateHeadImgStr = 'recentContactList[' + index + '].headImg'
that.setData({
[updateNickStr]: res.wxNick,
[updateHeadImgStr]: res.userHeadImg
})
//由于是在 for 循环里做异步网络请求,所以下面写法可以解决信息不对应问题
if (index < that.data.recentContactList.length - 1) {
that.getRecentContact(index + 1)
} else {
wx.hideLoading()
}
}).catch(function(error) {
console.error("获取用户信息失败:" + error)
if (index < that.data.recentContactList.length - 1) {
that.getRecentContact(index + 1)
} else {
wx.hideLoading()
}
})
} else {
wx.hideLoading()
}
}
- 获取未读消息:这块在 3. 的代码中已经使用了。可以看上面的代码。
- 获取历史消息:该接口大部分都是在进入聊天主界面后请求,这里需要注意三点细节问题:
①. 该接口支持根据最后一条消息的时间和 MsgKey 进行分页,所以必须你手动修改 webim_handler.js 里面的代码,自己实现分页的逻辑;
②. 获取到的历史消息还是需要你对数据进行封装,将服务端头像和昵称封装到里面;
③. 根据获取到的历史消息里面的 getIsSend() 判断是否自己发送的,来进行左右两边的收发显示。
④. 最好添加上时间分组,类似QQ、微信那样显示,多少时间段内的显示一个时间在消息上面。
⑤. 在进入页面时将所有收到的消息标识为已读,退出页面时再设置为未读,这很重要!!!
前面三点比较简单,主要讲一下第④点和第⑤点。
第④点,实现一段时间后的消息上面添加时间提醒。直接上代码比较更直观,最后再看一下效果。
//获取历史消息
webimhandler.getHistoryMsg(对方的 account, 自己的头像, 对方头像,function(histroyMsg) {
var historyMsgList = []
var currentSystemTime = Date.parse(new Date());
var lastTime = ''
var historyMsgTime = ''
if (histroyMsg != null && histroyMsg.length > 0) {
//循环历史消息,判断时间添加到新的数组中
for (var index = histroyMsg.length - 1; index >= 0; index--) {
//最后一条消息可以直接添加到数组中
historyMsgList.unshift(histroyMsg[index])
//判断是不是最后一条消息
if (index === histroyMsg.length - 1) {
//最后一条消息与当前的系统时间对比
historyMsgTime = util.coverIMTime(currentSystemTime, histroyMsg[index].time)
} else {
//不是最后一条消息则与上一条消息的时间对比
historyMsgTime = util.coverIMTime(lastTime, histroyMsg[index].time)
}
//重新赋值,记录上一条消息的时间
lastTime = histroyMsg[index].time
//根据对比判断是否需要添加一条“时间”的消息
if (historyMsgTime != null && historyMsgTime != '') {
//添加一条“时间”的消息
var historyMsg = {
isSelfSend: '',
account: '',
headImg: '',
msg: '',
time: historyMsgTime,
showMsgTime: true
}
historyMsgList.unshift(historyMsg)
}
}
}
//刷新数据,并移动
that.setData({
msgList: historyMsgList,
scrollTop: historyMsgList.length * 100
})
})
// IM 时间转换
const coverIMTime = (lastTime, currentTime) => {
//入参判空和转换
....
// 当前时间戳和目标时间戳转换成moment对象的类型
let systemDate_day = moment(moment().format('YYYY-MM-DD'))
let numberDate_day = moment(moment(currentTime).format('YYYY-MM-DD'))
//判断是否当天消息
const day = systemDate_day.diff(numberDate_day, 'days')
let currentDate = moment(moment(currentTime).format('YYYY-MM-DD HH:mm:ss'))
let lastDate = moment(moment(lastTime).format('YYYY-MM-DD HH:mm:ss'))
const minute = currentDate.diff(lastDate, 'minute')
if (Math.abs(day) === 0) {
//今天的消息 -- 显示 时间
if (Math.abs(minute) > 10) {
return moment(currentTime).format('HH:mm')
} else {
return ''
}
} else if (Math.abs(day) === 1) {
//昨天的消息 -- 显示 昨天+时间
if (Math.abs(minute) > 10) {
return '昨天' + moment(currentTime).format('HH:mm')
} else {
return ''
}
} else {
//之前的消息 -- 显示 日期+时间
if (Math.abs(minute) > 10) {
return moment(currentTime).format('MM月DD日 HH:mm')
} else {
return ''
}
}
}
效果图:


第⑤点比较简单,只需要在 onLoad 和 onUnload 设置即可,如果不设置那么在获取未读消息的时候会没有未读消息的,切记!
//获取会话
var selSess = webim.MsgStore.sessByTypeId(webim.SESSION_TYPE.C2C, 对方 account );
//在 onLoad 中设置为消息已读状态
webim.setAutoRead(selSess, true, true);
//在 onUnload 中设置消息未读状态
webim.setAutoRead(selSess, false, false);
网友评论