美文网首页
微信小程序授权、组件化、集成IM实战记录

微信小程序授权、组件化、集成IM实战记录

作者: ByteStefan | 来源:发表于2019-04-09 16:13 被阅读0次

       最近参与了公司开发的一个微信小程序项目,也是第一次真正实战微信小程序。由于之前没有怎么专门学习 html 和 css,所以页面交互与显示就交给了公司前端工程师,我主要负责 js 逻辑开发。在这里简单总结一下开发中用到的知识,同时也记录一下“微信小程序集成腾讯云IM”,实现简单的文字聊天。之前写过一篇“微信小程序开发小结”,里面写了一些比较细节上的小技巧,对于刚刚上手的可以参考一下。

       那么废话不多说,下面开始进入正题:

一、 小程序授权登录

  1. 在默认首屏界面的中添加授权提示,设置按钮( Button 标签)open-type = "getUserInfo",同时设置 bindgetuserinfo = "getuserinfo" (回调函数)来接收授权后的用户信息。在这个授权提示的样式中最好添加一个变量用于控制是否显示,这里我用 isShowAuthorize 来进行控制,true 为显示授权提示;false 为不显示授权提示。

  2. 在首屏界面的 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
        }
    }
})
  1. 不要忘记在授权按钮中设置了 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) {
      // 页面尺寸变化
    }
}

四、说点零碎的点

  1. 开发中可能会用到“广播/监听”来实现跨组件或跨界面的实时数据更新。这里给大家推荐一个开源框架 WxNotificationCenter ,使用方便。当然如果你看了源码可能会吐槽:这么简单个 js 我也能写啊。我想说既然人家已经给你写好了,咱们也没必要重复造轮子了,你说是吧...

  2. 开发中难免会碰到对时间戳的处理,这里再给大家推荐一个开源框架 moment,里面综合了很多常用的时间戳处理方法,它提供了官网与官方文档方便大家查看。

五、来说说腾讯云IM

        本项目中涉及到了集成腾讯云 IM 实现简单的文字聊天。在开发中发现了很多好玩的东西,先看一下基础实现,主要包括:①. 腾讯 IM 集成;②. IM 登录/登出;③. 获取最近联系人;④. 未读消息;⑤. 历史消息。

  1. 腾讯 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 方式正在研发中。

  2. IM 登录/登出:这里可以根据官方文档来实现。在本项目中仅仅是 c2c 文字聊天,所以监听仅添加了连接状态监听和新消息监听。注意!①. 所有的后续操作(最新联系人、历史消息、聊天等等)都要求在 IM 登录成功之后才能实现,不然会报“tinyid 或 a2 不能为空”的异常。②. 在新消息监听回调中不要出现业务堵塞情况,如果出现堵塞情况会导致轮询心跳超时而被停止,这样用户就无法收到最新消息。

  3. 获取最新联系人:通过 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()
  }
}
  1. 获取未读消息:这块在 3. 的代码中已经使用了。可以看上面的代码。
  2. 获取历史消息:该接口大部分都是在进入聊天主界面后请求,这里需要注意三点细节问题:
    ①. 该接口支持根据最后一条消息的时间和 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 ''
    }
  }
}

效果图:



image.jpeg

第⑤点比较简单,只需要在 onLoad 和 onUnload 设置即可,如果不设置那么在获取未读消息的时候会没有未读消息的,切记!

//获取会话
var selSess = webim.MsgStore.sessByTypeId(webim.SESSION_TYPE.C2C, 对方 account );
//在 onLoad 中设置为消息已读状态
webim.setAutoRead(selSess, true, true);
//在 onUnload 中设置消息未读状态
webim.setAutoRead(selSess, false, false);

相关文章

网友评论

      本文标题:微信小程序授权、组件化、集成IM实战记录

      本文链接:https://www.haomeiwen.com/subject/ddqiiqtx.html