微信小程序-原生JS集成腾讯IM实时聊天/实时音视频对话功能

2,611 阅读33分钟

一、腾讯IM集成

最近公司微信小程序需要集成腾讯IM实现实时聊天功能,这篇文章就记录我在集成过程中所踩得坑和心得

前期准备

首先第一步: 当然是进官网读文档 文档地址 : cloud.tencent.com/document/pr… 第一个看到的就是这个一分钟跑通demo(实际上我感觉看了跟没看一样,没啥太大帮助)也就清楚了一下前期准备工作:

  1. 首先你要有一个可用的腾讯云账号
  2. 然后你需要登录 即时通信IM控制台
  3. 在控制台中添加新应用
  4. 创建应用后点进去可以拿到应用的 SDKAppId 以及 密钥
  5. 注意:大哥些这里可以把demo源文件下载下来,一会儿有大用处

接下来继续翻,真正符合我需求的还是文档 常规集成 这一块

在这里插入图片描述 根据文档要求先将依赖搞好(其实也可以直接在源文件里面把 tim-wx.js 直接拷过来放你项目里也行)

// IM 小程序 SDK
npm install tim-wx-sdk --save
// 发送图片、文件等消息需要的 COS SDK
npm install cos-wx-sdk-v5 --save

依赖安好,引用,搞到这里我就出现了第一个问题,搞得我头痛,我引用依赖后报了个错误

import TIM from '../../tim-wx-sdk/tim-wx';

在这里插入图片描述 一开始把我搞蒙了,然后就当然网上查,然后得到的信息是小程序开启ES6转码后async函数无法使用,说到底就是这依赖语法用得有点高呗,然后网上给的解决办法最为直接有效的有两种(都是在小程序项目详情的本地设置里处理): 在这里插入图片描述

  1. 关了ES6 转 ES5 (我当时就否决了,他娘的这个一关我小程序其它地方不得崩死?谁还不用点es6啊)
  2. 开启增强编译( 这就好办了,立刻我就把增强编译打开了 注意:如果你没有这个选项,你就更新你的开发者工具,别用老古董版本了)

然后我满怀希望的再次编译得到的还是它(当时我就急了,网友这不骗人吗!!!) 在这里插入图片描述 然后又是慢慢网搜路,还是不知道哪儿出问题了 就是不行.....最后的最后,被我找到了,还是本地设置里 在这里插入图片描述 我他娘的用了老古董调试基础库= =(怪我,不该质疑无私的码友们)我火速改到最新的!!! 在这里插入图片描述 然后就不报错了!!!泪目啊泪目,我算是把第一个坑踩过了

实例创建及初始化

接下来就是按部就班初始化im了(直接把文档的扒下来就行)

  init_TIM() {//初始化im实时聊天
    let options = {
      SDKAppID: 0// 接入时需要将0替换为您的即时通信 IM 应用的 SDKAppID
    };
  
    let that = this
    // 创建 SDK 实例,`TIM.create()`方法对于同一个 `SDKAppID` 只会返回同一份实例
    this.tim = TIM.create(options);// SDK 实例通常用 tim 表示
    // 设置 SDK 日志输出级别,详细分级请参见 setLogLevel 接口的说明
    this.tim.setLogLevel(0); // 普通级别,日志量较多,接入时建议使用
    // tim.setLogLevel(1); // release 级别,SDK 输出关键信息,生产环境时建议使用
    // 注册 COS SDK 插件
    // tim.registerPlugin({'cos-wx-sdk': COS})
  
    // 监听事件,例如:
    this.tim.on(TIM.EVENT.SDK_READY, function(event) {
      // 收到离线消息和会话列表同步完毕通知,接入侧可以调用 sendMessage 等需要鉴权的接口
      // event.name - TIM.EVENT.SDK_READY
    });
  
    this.tim.on(TIM.EVENT.MESSAGE_RECEIVED, function(event) {
      // 收到推送的单聊、群聊、群提示、群系统通知的新消息,可通过遍历 event.data 获取消息列表数据并渲染到页面
      // event.name - TIM.EVENT.MESSAGE_RECEIVED
      // event.data - 存储 Message 对象的数组 - [Message]
    });
  
    this.tim.on(TIM.EVENT.MESSAGE_REVOKED, function(event) {
      // 收到消息被撤回的通知
      // event.name - TIM.EVENT.MESSAGE_REVOKED
      // event.data - 存储 Message 对象的数组 - [Message] - 每个 Message 对象的 isRevoked 属性值为 true
    });
  
    this.tim.on(TIM.EVENT.MESSAGE_READ_BY_PEER, function(event) {
      // SDK 收到对端已读消息的通知,即已读回执。使用前需要将 SDK 版本升级至 v2.7.0 或以上。仅支持单聊会话。
      // event.name - TIM.EVENT.MESSAGE_READ_BY_PEER
      // event.data - event.data - 存储 Message 对象的数组 - [Message] - 每个 Message 对象的 isPeerRead 属性值为 true
    });
  
    this.tim.on(TIM.EVENT.CONVERSATION_LIST_UPDATED, function(event) {
      // 收到会话列表更新通知,可通过遍历 event.data 获取会话列表数据并渲染到页面
      // event.name - TIM.EVENT.CONVERSATION_LIST_UPDATED
      // event.data - 存储 Conversation 对象的数组 - [Conversation]
    });
  
    this.tim.on(TIM.EVENT.GROUP_LIST_UPDATED, function(event) {
      // 收到群组列表更新通知,可通过遍历 event.data 获取群组列表数据并渲染到页面
      // event.name - TIM.EVENT.GROUP_LIST_UPDATED
      // event.data - 存储 Group 对象的数组 - [Group]
    });
  
    this.tim.on(TIM.EVENT.PROFILE_UPDATED, function(event) {
      // 收到自己或好友的资料变更通知
      // event.name - TIM.EVENT.PROFILE_UPDATED
      // event.data - 存储 Profile 对象的数组 - [Profile]
    });
  
    this.tim.on(TIM.EVENT.BLACKLIST_UPDATED, function(event) {
      // 收到黑名单列表更新通知
      // event.name - TIM.EVENT.BLACKLIST_UPDATED
      // event.data - 存储 userID 的数组 - [userID]
    });
  
    this.tim.on(TIM.EVENT.ERROR, function(event) {
      // 收到 SDK 发生错误通知,可以获取错误码和错误信息
      // event.name - TIM.EVENT.ERROR
      // event.data.code - 错误码
      // event.data.message - 错误信息
    });
  
    this.tim.on(TIM.EVENT.SDK_NOT_READY, function(event) {
      // 收到 SDK 进入 not ready 状态通知,此时 SDK 无法正常工作
      // event.name - TIM.EVENT.SDK_NOT_READY
    });
  
    this.tim.on(TIM.EVENT.KICKED_OUT, function(event) {
      // 收到被踢下线通知
      // event.name - TIM.EVENT.KICKED_OUT
      // event.data.type - 被踢下线的原因,例如:
      //    - TIM.TYPES.KICKED_OUT_MULT_ACCOUNT 多实例登录被踢
      //    - TIM.TYPES.KICKED_OUT_MULT_DEVICE 多终端登录被踢
      //    - TIM.TYPES.KICKED_OUT_USERSIG_EXPIRED 签名过期被踢 (v2.4.0起支持)。 
    });
  
    this.tim.on(TIM.EVENT.NET_STATE_CHANGE, function(event) { 
      //  网络状态发生改变(v2.5.0 起支持)。 
      // event.name - TIM.EVENT.NET_STATE_CHANGE 
      // event.data.state 当前网络状态,枚举值及说明如下: 
      //     \- TIM.TYPES.NET_STATE_CONNECTED - 已接入网络 
      //     \- TIM.TYPES.NET_STATE_CONNECTING - 连接中。很可能遇到网络抖动,SDK 在重试。接入侧可根据此状态提示“当前网络不稳定”或“连接中” 
      //    \- TIM.TYPES.NET_STATE_DISCONNECTED - 未接入网络。接入侧可根据此状态提示“当前网络不可用”。SDK 仍会继续重试,若用户网络恢复,SDK 会自动同步消息  
    });
  
    console.log('执行了init');
    // this.login_TIM();
  },

调用 , 执行

在这里插入图片描述

IM登录

看到这一串大宝贝儿,我就舒了口气,成功了嘛,那接下来我就登陆嘛!

login_TIM() {//登录im实时聊天
    let promise = this.tim.login({userID: '', userSig: ''});
    promise.then(function(imResponse) {
      console.log(imResponse.data); // 登录成功
      if (imResponse.data.repeatLogin === true) {
        // 标识账号已登录,本次登录操作为重复登录。v2.5.1 起支持
        console.log(imResponse.data.errorInfo);
      }
    }).catch(function(imError) {
      console.warn('login error:', imError); // 登录失败的相关信息
    });
  },

到这里userID随便填一个测试的就行,那userSig不行啊,要计算啊,那我就看文档 : cloud.tencent.com/document/pr… 他倒是给了客户端计算的方法和源码

在这里插入图片描述 下载下来一看源码

global.webpackJsonpMpvue([18],{
/***/ "dutN":
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return _SDKAPPID; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "b", function() { return genTestUserSig; });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__lib_generate_test_usersig_es_min_js__ = __webpack_require__("n7IX");
/*eslint-disable*/


const _SDKAPPID = 0;
const _SECRETKEY = '';
/*
 * Module:   GenerateTestUserSig
 *
 * Function: 用于生成测试用的 UserSig,UserSig 是腾讯云为其云服务设计的一种安全保护签名。
 *           其计算方法是对 SDKAppID、UserID 和 EXPIRETIME 进行加密,加密算法为 HMAC-SHA256。
 *
 * Attention: 请不要将如下代码发布到您的线上正式版本的 App 中,原因如下:
 *
 *            本文件中的代码虽然能够正确计算出 UserSig,但仅适合快速调通 SDK 的基本功能,不适合线上产品,
 *            这是因为客户端代码中的 SECRETKEY 很容易被反编译逆向破解,尤其是 Web 端的代码被破解的难度几乎为零。
 *            一旦您的密钥泄露,攻击者就可以计算出正确的 UserSig 来盗用您的腾讯云流量。
 *
 *            正确的做法是将 UserSig 的计算代码和加密密钥放在您的业务服务器上,然后由 App 按需向您的服务器获取实时算出的 UserSig。
 *            由于破解服务器的成本要高于破解客户端 App,所以服务器计算的方案能够更好地保护您的加密密钥。
 *
 * Reference:https://cloud.tencent.com/document/product/647/17275#Server
 */
function genTestUserSig(userID) {
  /**
   * 腾讯云 SDKAppId,需要替换为您自己账号下的 SDKAppId。
   *
   * 进入腾讯云实时音视频[控制台](https://console.cloud.tencent.com/rav ) 创建应用,即可看到 SDKAppId,
   * 它是腾讯云用于区分客户的唯一标识。
   */
  var SDKAPPID = _SDKAPPID;

  /**
   * 签名过期时间,建议不要设置的过短
   * <p>
   * 时间单位:秒
   * 默认时间:7 x 24 x 60 x 60 = 604800 = 7 天
   */
  var EXPIRETIME = 604800;


  /**
   * 计算签名用的加密密钥,获取步骤如下:
   *
   * step1. 进入腾讯云实时音视频[控制台](https://console.cloud.tencent.com/rav ),如果还没有应用就创建一个,
   * step2. 单击“应用配置”进入基础配置页面,并进一步找到“帐号体系集成”部分。
   * step3. 点击“查看密钥”按钮,就可以看到计算 UserSig 使用的加密的密钥了,请将其拷贝并复制到如下的变量中
   *
   * 注意:该方案仅适用于调试Demo,正式上线前请将 UserSig 计算代码和密钥迁移到您的后台服务器上,以避免加密密钥泄露导致的流量盗用。
   * 文档:https://cloud.tencent.com/document/product/647/17275#Server
   */
  var SECRETKEY = _SECRETKEY;
  
  var generator = new __WEBPACK_IMPORTED_MODULE_0__lib_generate_test_usersig_es_min_js__["a" /* default */](SDKAPPID, SECRETKEY, EXPIRETIME);
  var userSig = generator.genTestUserSig(userID);
  return {
    sdkappid: SDKAPPID,
    userSig: userSig
  };
}
/***/ })
});

好家伙你倒是用了Vue模板去搞,我要是不用Vue,你让我原生js怎么去引,怎么去用,他娘的,这算是第二个坑了嘛,又开始网搜,最后的最后,我们下载的demo源文件发挥大作用了

在这里插入图片描述 这个文件路径下面有两个js文件

在这里插入图片描述 把这两个甩到项目里去再去引用这个 GenerateTestUserSig.js 文件,他是好孩子,是原生的

引用

import { genTestUserSig } from '../../utils/GenerateTestUserSig'

输出

  onLoad:function(){
    console.log(genTestUserSig('TEST-1'));
  },

打印

在这里插入图片描述 乖乖 , 总算是有了,至此第二个坑踩完了(这里我要吐槽一下开发文档,你他娘的有原生的就不能告知一下?非要人自己去找)

接下来登录(注意:userID必须是字符串类型)

  login_TIM() {//登录im实时聊天
    let promise = this.tim.login({userID: 'TEST-1', userSig: genTestUserSig('TEST-1').userSig});
    promise.then(function(imResponse) {
      console.log(imResponse.data); // 登录成功
      if (imResponse.data.repeatLogin === true) {
        // 标识账号已登录,本次登录操作为重复登录。v2.5.1 起支持
        console.log(imResponse.data.errorInfo);
      }
    }).catch(function(imError) {
      console.warn('login error:', imError); // 登录失败的相关信息
    });
  },

在这里插入图片描述

OJBK ! 成了,总算了给他登进去了 --------更新割----------

收发消息

接下来就是收发消息了,看了下文档,这就比较简单跑通了直接调接口,官方文档还是写得很通俗易懂了

// 发送文本消息,Web 端与小程序端相同
// 1. 创建消息实例,接口返回的实例可以上屏
let message = tim.createTextMessage({
  to: 'user1',
  conversationType: TIM.TYPES.CONV_C2C,
  // 消息优先级,用于群聊(v2.4.2起支持)。如果某个群的消息超过了频率限制,后台会优先下发高优先级的消息,详细请参考:https://cloud.tencent.com/document/product/269/3663#.E6.B6.88.E6.81.AF.E4.BC.98.E5.85.88.E7.BA.A7.E4.B8.8E.E9.A2.91.E7.8E.87.E6.8E.A7.E5.88.B6)
  // 支持的枚举值:TIM.TYPES.MSG_PRIORITY_HIGH, TIM.TYPES.MSG_PRIORITY_NORMAL(默认), TIM.TYPES.MSG_PRIORITY_LOW, TIM.TYPES.MSG_PRIORITY_LOWEST
  // priority: TIM.TYPES.MSG_PRIORITY_NORMAL,
  payload: {
    text: 'Hello world!'
  }
});
// 2. 发送消息
let promise = tim.sendMessage(message);
promise.then(function(imResponse) {
  // 发送成功
  console.log(imResponse);
}).catch(function(imError) {
  // 发送失败
  console.warn('sendMessage error:', imError);
});

执行后打印如下

在这里插入图片描述 然后看看我在收消息的那方获取到的信息(这里是通过最开始 tim.on(TIM.EVENT.MESSAGE_RECEIVED, function(event) {}所监听到的事件)

在这里插入图片描述 当然还有发送图片消息,音频消息,文件消息等等,官方文档还是写得很清楚,也没碰到什么坑,所以我就不一一列出了.至此腾讯im主要的功能算是完全跑通了,接下来就是按照自己的项目需求继续开发了.然后我将整个腾讯IM的初始化/登录/发送消息等都放到了一个js文件中,这里就贴上来供大家参考下:

  const app = getApp() //获取APP.js便于设置操作全局变量
  
  import TIM from '../tim-wx-sdk/tim-wx';
  import { genTestUserSig } from '../tim-wx-sdk/GenerateTestUserSig'

  var tim = '';

  function init_TIM() {//初始化im实时聊天
    if(app.globalData_TIM.isInit){
      //这里设置了一个全局变量isLogin来标记是否已登录,避免重复创建im实例
      return false
    }
    let options = {
      SDKAppID: 0// 接入时需要将0替换为您的即时通信 IM 应用的 SDKAppID
    };
    
    let that = this
    // 创建 SDK 实例,`TIM.create()`方法对于同一个 `SDKAppID` 只会返回同一份实例
    tim = TIM.create(options);// SDK 实例通常用 tim 表示
    // 设置 SDK 日志输出级别,详细分级请参见 setLogLevel 接口的说明
    tim.setLogLevel(0); // 普通级别,日志量较多,接入时建议使用
    // tim.setLogLevel(1); // release 级别,SDK 输出关键信息,生产环境时建议使用
    // 注册 COS SDK 插件   此处暂时隐藏有需求要传图片,文件等的请放开进行配置,记住头部引入
    // tim.registerPlugin({'cos-wx-sdk': COS})
  
    // 监听事件,例如:
    tim.on(TIM.EVENT.SDK_READY, function(event) {
      // 收到离线消息和会话列表同步完毕通知,接入侧可以调用 sendMessage 等需要鉴权的接口
      // event.name - TIM.EVENT.SDK_READY
    });
  
    tim.on(TIM.EVENT.MESSAGE_RECEIVED, function(event) {
      console.log('收到消息');
      // 收到推送的单聊、群聊、群提示、群系统通知的新消息,可通过遍历 event.data 获取消息列表数据并渲染到页面
      // event.name - TIM.EVENT.MESSAGE_RECEIVED
      // event.data - 存储 Message 对象的数组 - [Message]
    });
  
    tim.on(TIM.EVENT.MESSAGE_REVOKED, function(event) {
      // 收到消息被撤回的通知
      // event.name - TIM.EVENT.MESSAGE_REVOKED
      // event.data - 存储 Message 对象的数组 - [Message] - 每个 Message 对象的 isRevoked 属性值为 true
    });
  
    tim.on(TIM.EVENT.MESSAGE_READ_BY_PEER, function(event) {
      // SDK 收到对端已读消息的通知,即已读回执。使用前需要将 SDK 版本升级至 v2.7.0 或以上。仅支持单聊会话。
      // event.name - TIM.EVENT.MESSAGE_READ_BY_PEER
      // event.data - event.data - 存储 Message 对象的数组 - [Message] - 每个 Message 对象的 isPeerRead 属性值为 true
    });
  
    tim.on(TIM.EVENT.CONVERSATION_LIST_UPDATED, function(event) {
      // 收到会话列表更新通知,可通过遍历 event.data 获取会话列表数据并渲染到页面
      // event.name - TIM.EVENT.CONVERSATION_LIST_UPDATED
      // event.data - 存储 Conversation 对象的数组 - [Conversation]
    });
  
    tim.on(TIM.EVENT.GROUP_LIST_UPDATED, function(event) {
      // 收到群组列表更新通知,可通过遍历 event.data 获取群组列表数据并渲染到页面
      // event.name - TIM.EVENT.GROUP_LIST_UPDATED
      // event.data - 存储 Group 对象的数组 - [Group]
    });
  
    tim.on(TIM.EVENT.PROFILE_UPDATED, function(event) {
      // 收到自己或好友的资料变更通知
      // event.name - TIM.EVENT.PROFILE_UPDATED
      // event.data - 存储 Profile 对象的数组 - [Profile]
    });
  
    tim.on(TIM.EVENT.BLACKLIST_UPDATED, function(event) {
      // 收到黑名单列表更新通知
      // event.name - TIM.EVENT.BLACKLIST_UPDATED
      // event.data - 存储 userID 的数组 - [userID]
    });
  
    tim.on(TIM.EVENT.ERROR, function(event) {
      // 收到 SDK 发生错误通知,可以获取错误码和错误信息
      // event.name - TIM.EVENT.ERROR
      // event.data.code - 错误码
      // event.data.message - 错误信息
    });
  
    tim.on(TIM.EVENT.SDK_NOT_READY, function(event) {
      // 收到 SDK 进入 not ready 状态通知,此时 SDK 无法正常工作
      // event.name - TIM.EVENT.SDK_NOT_READY
    });
  
    tim.on(TIM.EVENT.KICKED_OUT, function(event) {
      // 收到被踢下线通知
      // event.name - TIM.EVENT.KICKED_OUT
      // event.data.type - 被踢下线的原因,例如:
      //    - TIM.TYPES.KICKED_OUT_MULT_ACCOUNT 多实例登录被踢
      //    - TIM.TYPES.KICKED_OUT_MULT_DEVICE 多终端登录被踢
      //    - TIM.TYPES.KICKED_OUT_USERSIG_EXPIRED 签名过期被踢 (v2.4.0起支持)。 
    });
  
    tim.on(TIM.EVENT.NET_STATE_CHANGE, function(event) { 
      //  网络状态发生改变(v2.5.0 起支持)。 
      // event.name - TIM.EVENT.NET_STATE_CHANGE 
      // event.data.state 当前网络状态,枚举值及说明如下: 
      //     \- TIM.TYPES.NET_STATE_CONNECTED - 已接入网络 
      //     \- TIM.TYPES.NET_STATE_CONNECTING - 连接中。很可能遇到网络抖动,SDK 在重试。接入侧可根据此状态提示“当前网络不稳定”或“连接中” 
      //    \- TIM.TYPES.NET_STATE_DISCONNECTED - 未接入网络。接入侧可根据此状态提示“当前网络不可用”。SDK 仍会继续重试,若用户网络恢复,SDK 会自动同步消息  
    });
    app.globalData_TIM.isInit = true;  //完成im实例创建后设置标志为true
  }

  function login_TIM(userID) {//登录im实时聊天 
    let promise = tim.login({userID: userID, userSig: genTestUserSig(userID).userSig});
    promise.then(function(imResponse) {
      console.log('登录成功')
      console.log(imResponse.data); // 登录成功
      if (imResponse.data.repeatLogin === true) {
        // 标识账号已登录,本次登录操作为重复登录。v2.5.1 起支持
        console.log('当前重复登录')
        console.log(imResponse.data.errorInfo);
      }
    }).catch(function(imError) {
      console.warn('login error:', imError); // 登录失败的相关信息
    });
  }

  function sendMessage_TIM( sendTo , msg ) {
  	//这里是文字消息的发送,有需求可以增加个type参数控制发送方式,文字/图片/音视频/文件 调用不同的tim接口
    let message = tim.createTextMessage({
      to: sendTo,
      conversationType: TIM.TYPES.CONV_C2C,
      payload: {
        text: msg
      }
    });
    let promise = tim.sendMessage(message);
    promise.then(function(imResponse) {
      // 发送成功
      console.log('发送成功')
      console.log(imResponse);
    }).catch(function(imError) {
      // 发送失败
      console.log('发送失败')
      console.warn('sendMessage error:', imError);
    });
  }
  
  function logout_TIM() {
    let promise = tim.logout();
    promise.then(function(imResponse) {
      console.log(imResponse.data); // 登出成功
    }).catch(function(imError) {
      console.warn('logout error:', imError);
    });
  }

//导出初始化,登录,消息发送接口供外部使用
module.exports = {
  init_TIM,
  login_TIM,
  logout_TIM,
  sendMessage_TIM
}

在需要使用的页面通过以下方式使用

const app = getApp()

//头部引用
import { init_TIM,login_TIM,sendMessage_TIM,logout_TIM } from '../../../js/tim-init';  

Page({
  data: {
   userID: ''
  },
  onLoad() {
    init_TIM();//在需要的页面初始化
  },
  loginTim() {
    login_TIM(this.data.userID);//根据页面需求登录并传入登录userID
  },
  logoutTim() {
    logout_TIM();//根据页面需求登出
  },
  sendMessage() {
    sendMessage_TIM(sendTo ,msg) //sendTo为信息接受者的userID , msg为消息数据
  }
})

--------更新割----------

继续更新,上次写到了把各个接口和流程跑通了,接下来就是按照自己的需求进行集成了,我项目需求是一个1V1实时聊天的这么一个功能,大概是这么样一个界面 在这里插入图片描述 我这里把聊天区域对话框的样式代码贴出来,有需要的可以直接扒

HTML

<!-- 
     我这里页面是用的一个scroll-view标签来设定的聊天区域
     如果是直接一个页面都为聊天区域可以忽略我外层结构,按照自己的需求来就行 
-->
<view class="chat-area">
  <scroll-view scroll-into-view="{{ toView }}" scroll-y="true">
    <!-- 
         这里循环拿到的消息列表,根据每条消息的from属性
         是否等于当前聊天页面的userID来渲染为左侧消息或者右侧蓝底消息
         里面的数据引用请根据自己的页面数据修改 
     -->
     <block wx:for="{{ msgList }}" wx:key="index">
      <view wx:if="{{ item.from == userID }}" class="right-chat-info  flex-box-end" id="msg-{{index}}">
        <view class="right-chat-msg">{{ item.payload.text }}</view>
        <view class="right-chat-head"><image class="chat-headPortrait" src="/image/b.jpg"></image></view>
      </view>
      <view wx:else class="left-chat-info flex-box-start" id="msg-{{index}}">
        <view class="left-chat-head"><image class="chat-headPortrait" src="/image/a.jpg"></image></view>
        <view class="left-chat-msg">{{ item.payload.text }}</view>
      </view>
    </block>
    <view id="contact-view" class="pay-bottom-cancel inChatArea">
      <view style="border-right:unset" bindtap="contact">联系客服</view>
    </view>
  </scroll-view>
</view>

CSS

.flex-box-start{
  display: flex;
  justify-content: start;
}
.flex-box-end{
  display: flex;
  justify-content: flex-end;
}
.chat-area{
  width: calc(100% + 40rpx);
  height: calc(100vh - 620rpx);
  background-color: #f9f9f9;
  margin-top: 20rpx;
  transform: translateX(-20rpx);
}
.chat-area > scroll-view{
  position: relative;
  height: 100%;
  padding: 0rpx 20rpx 10rpx 20rpx;
}
.left-chat-info , .right-chat-info{
  margin-top: 30rpx;
}
.left-chat-head{
  width: 100rpx;
  height: 100rpx;
  margin-right: 20rpx;
}
.chat-headPortrait{
  width: 100%;
  height: 100%;
  border-radius: 50%;
}
.left-chat-msg{
  width: calc(100% - 200rpx);
  flex-grow: 1;
  background-color: #FFF;
  padding: 20rpx 30rpx 20rpx 40rpx;
  border-radius: 20rpx 10rpx 20rpx 50rpx;
  font-size: 28rpx;
  max-width: 65%;
  overflow: hidden;
  word-wrap: break-word;
}
.right-chat-head{
  width: 100rpx;
  height: 100rpx;
  margin-left: 20rpx;
}
.chat-headPortrait{
  width: 100%;
  height: 100%;
}
.right-chat-msg{
  width: calc(100% - 200rpx);
  flex-grow: 1;
  color: #FFF;
  background-color: #779FDE;
  padding: 20rpx 40rpx 20rpx 30rpx;
  border-radius: 20rpx 10rpx 50rpx 10rpx;
  font-size: 28rpx;
  max-width: 65%;
  overflow: hidden;
  word-wrap: break-word;
}

回到js部分,因为我之前将所有的IM方法都放到了一个JS文件中,这样可以让每个需要的页面复用,更方便,但是有个问题就是微信小程序没有一个比较好的状态管理器,类似Vuex这种跨页面的,所以就需要使用到大家比较熟悉的发布订阅模式来完成数据的实时更新了,我太懒了,这部分代码参照修改了 blog.csdn.net/weixin_4231…)

这位码友的文章,非常感谢,总的来说就是在外层utils文件里增加一个event.js

class Event {
  /**
  * on 方法把订阅者所想要订阅的事件及相应的回调函数记录在 Event 对象的 _cbs 属性中
  */
  on(event, fn) {
    if (typeof fn != "function") {
      console.error('fn must be a function')
      return
    }
    this._cbs = this._cbs || {};
    (this._cbs[event] = this._cbs[event] || []).push(fn)
  }
  /**
  * emit 方法接受一个事件名称参数,在 Event 对象的 _cbs 属性中取出对应的数组,并逐个执行里面的回调函数
  */
  emit(event) {
    this._cbs = this._cbs || {}
    var callbacks = this._cbs[event], args
    if (callbacks) {
      callbacks = callbacks.slice(0)
      args = [].slice.call(arguments, 1)
      for (var i = 0, len = callbacks.length; i < len; i++) {
        callbacks[i].apply(null, args)
      }
    }
  }
  /**
  * off 方法接受事件名称和当初注册的回调函数作参数,在 Event 对象的 _cbs 属性中删除对应的回调函数。
  */
  off(event, fn) {
    this._cbs = this._cbs || {}
    // all
    if (!arguments.length) {
      this._cbs = {}
      return
    }
    var callbacks = this._cbs[event]
    if (!callbacks) return
    // remove all handlers
    if (arguments.length === 1) {
      delete this._cbs[event]
      return
    }
    // remove specific handler
    var cb
    for (var i = 0, len = callbacks.length; i < len; i++) {
      cb = callbacks[i]
      if (cb === fn || cb.fn === fn) {
        callbacks.splice(i, 1)
        break
      }
    }
    return
  }
}
export default Event

然后在app.js中将其挂载到wx对象上方便实用

import Event from './utils/event'
//挂载发布订阅至wx对象上
wx.event = new Event();

然后在聊天对话页面订阅相关事件(代码中的数据请根据自己后台接口情况进行动态设置)

const app = getApp()

import { init_TIM,login_TIM,sendMessage_TIM,getMsgList_TIM } from '../../../js/tim-init';


Page({
  data: {
    // im实时聊天数据
    toView:'',
    userID:'test-2',
    sendMsg: '',
    msgList: [],
    nextReqMessageID: '',
    isCompleted: ''
  },
  onLoad() {
    let that = this;
    init_TIM();//初始化创建Tim实例
    wx.event.on('conversationInit',()=>{//订阅初始化获取聊天记录
      getMsgList_TIM('test-1');
    })
    wx.event.on('renderMsg',(e,newMsgForm)=>{//订阅renderMsg事件来渲染页面
      let newMsgObj = e;
      if(newMsgForm == 'test-1'){
        this.setData({
          msgList : that.data.msgList.concat([newMsgObj])
        },()=>{
          that.scrollToBottom()
        })
      }
    })
    wx.event.on('conversationRender',(e,nextReqMessageID,isCompleted)=>{//订阅初始化获取聊天记录后渲染至页面
      let list = [...e];
      this.setData({
        'msgList' : list,
        'nextReqMessageID' : nextReqMessageID,
        'isCompleted' : isCompleted
      },()=>{
        that.scrollToBottom()
      })
    })
  },
  loginTim() {//登录TIM
    login_TIM(this.data.userID);
  },
  sendMsgInput(e) {//输入框输入事件
    this.setData({
      'sendMsg' : e.detail.value
    })
  },
  sendMsgTap() {//发送消息按钮点击事件
    sendMessage_TIM('test-1',this.data.sendMsg);
    this.setData({
      'sendMsg' : ''
    })
  },
  scrollToBottom() {//聊天区域滑动至底部
    let that = this;
    this.setData({
      toView: 'msg-' + (that.data.msgList.length - 1)
    })
  },
})

然后tim-init.js中这几个地方进行了更新修改

tim.on(TIM.EVENT.SDK_READY, function(event) {

//此回调函数中发布'conversationInit'对话列表初始化事件触发页面js中的
//wx.event.on('conversationInit',()=>{})方法来调用getMsgList_TIM方法获取初始化对话列表
//需要注意的是tim.getMessageList({})这个方法必须要在SDK_READY状态才能调用,所以要放在这个回调函数中

  wx.event.emit('conversationInit') // 会话列表的监听函数
  // 收到离线消息和会话列表同步完毕通知,接入侧可以调用 sendMessage 等需要鉴权的接口
  // event.name - TIM.EVENT.SDK_READY
});
tim.on(TIM.EVENT.MESSAGE_RECEIVED, function(event) {
  // 这里收到消息,调用setGlobalMsg方法来处理数据,传入方式标记为'received'接收消息
  setGlobalMsg(event,'received');
  // 收到推送的单聊、群聊、群提示、群系统通知的新消息,可通过遍历 event.data 获取消息列表数据并渲染到页面
  // event.name - TIM.EVENT.MESSAGE_RECEIVED
  // event.data - 存储 Message 对象的数组 - [Message]
});

  function sendMessage_TIM(sendTo,msg) {
    let message = tim.createTextMessage({
      to: sendTo,
      conversationType: TIM.TYPES.CONV_C2C,
      payload: {
        text: msg
      }
    });
    let promise = tim.sendMessage(message);
    promise.then(function(imResponse) {
      // 发送成功
      console.log('发送成功')
      console.log(imResponse); 
      setGlobalMsg(imResponse,'send')  //发送消息成功后调用setGlobalMsg方法来处理数据,传入方法为'send'发送消息
    }).catch(function(imError) {
      // 发送失败
      console.log('发送失败')
      console.warn('sendMessage error:', imError);
    });
  }
  
    function setGlobalMsg(data,type) {
    //处理数据的方法
    let msgarr = app.globalData_TIM.msg;//初始化拉取全局数据
    let newMsgForm = '';
    if(type == 'received'){
      newMsgForm = data.data[0].from // 设置对话人
      if(msgarr[newMsgForm] != undefined) {//判断当前对话人是否有数据,如果有,就把新数据加进去,如果没有,就创一个键值对
        msgarr[newMsgForm].push(data.data[0])
      } else {
        msgarr[newMsgForm] = [data.data[0]]
      }
      app.globalData_TIM.msg = msgarr;
      wx.event.emit('renderMsg' , data.data[0] , newMsgForm) // 发布'renderMsg'通知页面进行数据刷新渲染
    }else if(type == 'send'){
      newMsgForm = data.data.message.to // 设置对话人
      if(msgarr[newMsgForm] != undefined) {//判断当前对话人是否有数据,如果有,就把新数据加进去,如果没有,就创一个键值对
        msgarr[newMsgForm].push(data.data.message)
      } else {
        msgarr[newMsgForm] = [data.data.message]
      }
      app.globalData_TIM.msg = msgarr;
      wx.event.emit('renderMsg' , data.data.message , newMsgForm)  // 发布'renderMsg'通知页面进行数据刷新渲染
    }
  }

  function getMsgList_TIM(userID) {
    // 打开某个会话时,第一次拉取消息列表
    let promise = tim.getMessageList({conversationID: 'C2C'+userID, count: 15});
    promise.then(function(imResponse) {
      const messageList = imResponse.data.messageList; // 消息列表。
      const nextReqMessageID = imResponse.data.nextReqMessageID; // 用于续拉,分页续拉时需传入该字段。
      const isCompleted = imResponse.data.isCompleted; // 表示是否已经拉完所有消息。
      console.log(imResponse)
      let msgarr = app.globalData_TIM.msg
      let conversationTo = userID // 定义会话键值
      if(msgarr[conversationTo] != undefined) {
        msgarr[conversationTo].concat(messageList)
      } else {
        msgarr[conversationTo] = messageList
      }
      app.globalData_TIM.msg = msgarr;
      wx.event.emit('conversationRender' , msgarr[conversationTo] , nextReqMessageID , isCompleted) // 发布'renderMsg'通知页面进行对话列表初始化渲染
    });
  }

这里实现了简单的消息互发,效果如下 在这里插入图片描述

在这里插入图片描述 注意: 这里有个提醒大家的就是,在我们进行对象或者数组赋值的时候,一定要深拷贝!!!!!一定要,我就一开始忽略了这一点,造成数据渲染一直有问题

    wx.event.on('conversationRender',(e,nextReqMessageID,isCompleted)=>{
      let list = [...e];  //下面要进行页面赋值,这里的数组一定要深拷贝下来,不然直接给下面赋值e就会出现错误
      this.setData({
        'msgList' : list,
        'nextReqMessageID' : nextReqMessageID,
        'isCompleted' : isCompleted
      },()=>{
        that.scrollToBottom()
      })
    })

二、腾讯音视频实时互动

我这里项目需求是语音聊天,所以我这里写的是语音功能集成,视频应该也差不多,请大家参考

跑通demo

依旧第一步读文档:cloud.tencent.com/document/pr…

1.依旧一样的需要一个腾讯云账号,然后创建一个运用,可以新建一个,也可以直接用集成IM实时通讯时的运用 SDKAppID密钥

2.如果是自己测试,依旧需要一个UserSig,我们如果跑他的demo,直接下它的源码,然后在他提示的地方修改相关信息就可以在这里插入图片描述

3.这一步很重要,一定要确定你的项目小程序可以打开这个东西,不然一切白瞎,因为他也是基于小程序的 <live-pusher> 和 <live-player> 标签 进行开发的 在这里插入图片描述 4.然后确保这些没问题,就可以跑他的demo了

(注意:这个音视频功能只有在真机上调试才可以哈,开发者工具不支持,必须要预览,然后真机扫码测试)

重点! 重点! 重点! 然后不出意外你将跑不通他的demo,然后出现这样的错误: 在这里插入图片描述 然后你就出现一万个问号,你跑去开发辅助工具去校验: 在这里插入图片描述 校验也是成功的!!!!这特么不就是坑了吗!!!!这个肯定是SDKAppid有问题,关键是按照他的步骤已经修改了这里的参数呀 在这里插入图片描述 我来告诉你有多坑爹!因为有个地方还需要改SDKAPPID,但是他文档里没有写!!!是真特么的坑爹,在他封装的组件TRTCCalling.js中在初始化实例的时候传入的SDKAppID是写的自己的,卧槽,你不改成你的,当然报错!!!!!!

在这里插入图片描述 然后就没问题了,你可以好好的把他的demo跑通了

三、同时集成即时通讯IM 和 音视频直播的 坑

即时通讯IM 和 音频语音电话TRTCCalling 同时实例化冲突的坑

重头戏来了,上面两个功能都已经成功的集成,测试也没有问题,完全可以正常使用,于是这边我需要将两个功能集成在一起,就会有以下问题:

首先我们集成 即时通讯IM 会初始化一个tim实例

 tim = TIM.create(options)  //这一步初始化SDK实例

然后在这个实例上监听各种事件(例举几个,全部事件请查看官方文档)

tim.on(TIM.EVENT.SDK_READY, function(event) {
      // 收到离线消息和会话列表同步完毕通知,接入侧可以调用 sendMessage 等需要鉴权的接口
      // event.name - TIM.EVENT.SDK_READY
    });
  
    tim.on(TIM.EVENT.MESSAGE_RECEIVED, function(event) {
      // 收到推送的单聊、群聊、群提示、群系统通知的新消息,可通过遍历 event.data 获取消息列表数据并渲染到页面
      // event.name - TIM.EVENT.MESSAGE_RECEIVED
      // event.data - 存储 Message 对象的数组 - [Message]
    });
  
    tim.on(TIM.EVENT.MESSAGE_REVOKED, function(event) {
      // 收到消息被撤回的通知
      // event.name - TIM.EVENT.MESSAGE_REVOKED
      // event.data - 存储 Message 对象的数组 - [Message] - 每个 Message 对象的 isRevoked 属性值为 true
    });
  
    tim.on(TIM.EVENT.MESSAGE_READ_BY_PEER, function(event) {
      // SDK 收到对端已读消息的通知,即已读回执。使用前需要将 SDK 版本升级至 v2.7.0 或以上。仅支持单聊会话。
      // event.name - TIM.EVENT.MESSAGE_READ_BY_PEER
      // event.data - event.data - 存储 Message 对象的数组 - [Message] - 每个 Message 对象的 isPeerRead 属性值为 true
    });

然后我们在集成音视频TRTCCalling的组件并进行初始化

this.TRTCCalling = this.selectComponent('#TRTCCalling-component');

这个时候实际在他的组件中也已经初始化出来了一个TIM实例 在这里插入图片描述 这样就会造成我们初始化了两个不同的实例,会使得音视频功能报错,显示一直卡在 1005 PUSH_EVT_CHANGE_RESOLUTION 推流动态调整分辨率 1102 PUSH_WARNING_RECONNECT 网络断连,已启动重连流程(重试失败超过三次会放弃) 3002 PUSH_WARNING_SEVER_CONN_FAIL 服务器连接失败,启动重试流程错误 最后查出实际就是因为实例化了两个TIM实例,重复推流造成的.

那么解决方案就是肯定只能实例化一个,那么就是要么让他封装的TRTCCallingzu组件使用我初始化的TIM实例,但是他组件中做了很多封装,要实现是很困难的, 那么就只有我们使用它的实例.

我们可以看到在他的TRTCCalling.js中实际也监听了相关事件 在这里插入图片描述 那就简单了,我们将即时通讯IM的相关操作挂到这里面来就欧克了!

不过最大的问题来了,他这里面的SDK_READY这个事件没有监听到,这个地方不会给到我反馈进入回调函数给我消息说SDK ready,而即时通讯IM中tim.getMessageList这个初始化获取历史消息列表的方法必须要在SDK ready状态后才能使用,我如果没办法监听到他SDK_READY就没办法初始化历史消息呀.目前卡在这一步,发了官方工单,等待解决中...

兄弟们有什么更好的方法能够解决吗?或者其它的集成方式?欢迎大家讨论

在这里插入图片描述

解决方式

上述问题在与官方大佬沟通之后,有几个结果:

1: 封装的TRTCCalling.js中他们监听的事件中SDK_READY确实无法给到反馈,进入回调函数 2: 上述截图中我们可以看出我们已经拿到了它组件实例化出来的tim实例:

this.TRTCCalling = this.selectComponent('#TRTCCalling-component');
this.TRTCCalling.tsignaling._tim //它初始化出来的实例

3: 我们可以拿它的实例去做即时通讯的时间监听操作,这样就可以跟我们单独集成即时通讯IM一样的操作,只是不需要初始化而是将这个实例传过去

具体代码及集成步骤如下: 1: 我们跑通了demo之后,将源文件中的TRTCCalling组件拷贝到我们的项目component目录中 在这里插入图片描述 注意:这里拷过来的时候也记得把TRTCCalling.js中的SDKAppID改成自己项目的 在这里插入图片描述 然后在需要语音通话的页面引用组件XXX.jason中

  "usingComponents": {
    "TRTCCalling": "/components/TRTCCalling/TRTCCalling"
  }

然后在XXX.wxml中引用组件模板

<view hidden="{{ !isCalling }}" class="trtc-calling-container">
  <TRTCCalling id="TRTCCalling-component" class="trtc-calling {{callingFlag ? '' : 'hidden'}}" config="{{config}}" pusherAvatar="{{pusherAvatar}}" remoteAvatar="{{invitee.avatar || inviter.avatar}}"></TRTCCalling>
  <view wx:if="{{incomingCallFlag}}" class="incoming-call">
    <image src="/image/rui.png" />
    <view class="tips">{{invitation.inviter}}</view>
    <view class="tips" >{{'邀请你' + (invitation.type === 1 ? '语音' : '视频') + '通话'}}</view>
    <view class="btn-operate">
      <view class="call-operate" style="background-color: red" bindtap="handleOnReject">
        <image src="/image/hangup.png" />
      </view>
      <view class="call-operate" style="background-color: #07c160" bindtap="handleOnAccept">
        <image src="/image/hangup.png" style="transform: rotate(-135deg); "/>
      </view>
    </view>
  </view>
  <view wx:if="{{inviteCallFlag}}" class="invite-call">
    <image src="/image/rui.png" />
    <view class="tips" >{{'等待' + invitee.userID + '接受邀请'}}</view>
    <view class="btn-operate">
      <view class="call-operate" style="background-color: red" bindtap="handleOnCancel">
        <image src="/image/hangup.png" />
      </view>
    </view>
  </view>
</view>

XXX.js中

const app = getApp()

import { sendMessage_TIM,getMsgList_TIM } from '../../../js/tim-init';
import { genTestUserSig } from '../../../tim-wx-sdk/GenerateTestUserSig'

Page({
  data: {
    // im实时聊天数据
    toView:'',
    sendMsg: '',
    msgList: [],
    nextReqMessageID: '',
    isCompleted: '',
    // 语音实时聊天页面渲染逻辑数据
    isCalling: false,
    callingFlag: false,
    invitee: null,
    inviter: null,
    invitation: null,
    incomingCallFlag: false,
    inviteCallFlag: false,
    pusherAvatar: '',
   	//语音聊天参数配置数据
    config: {
      sdkAppID: 1400413616,
      userID: '',
      userSig: '',
      type: 1,
    },
  },
  onLoad() {
	this.login()
  },
  sendMsgInput(e) {
    this.setData({
      'sendMsg' : e.detail.value
    })
  },
  sendMsgTap() {
    sendMessage_TIM(this.data.invitee.userID,this.data.sendMsg);
    this.setData({
      'sendMsg' : ''
    })
  },
  scrollToBottom() {
    let that = this;

    this.setData({
      toView: 'msg-' + (that.data.msgList.length - 1)
    })
  },
  login() {//初始化登录
    this.audioInit('123','321'); //这里的123和321分别是当前用户的ID和对话用户的ID
  },
  callAudio() {
    this.call();
  },
  // 实时语音方法
  audioInit(inviter,invitee) {
    this.setData({
      'invitee' : {
        userID : invitee
      },
      config : {
        userID : inviter
      }
    })
    const Signature = genTestUserSig(inviter)  //这里的签名和即时通讯IM一样的可以复用,当然生产环境就需要后台给你了
    this.data.config.sdkAppID = 0  //这里修改成你的项目SDKAppID
    this.data.config.userID = inviter
    this.data.config.userSig = Signature.userSig
    this.setData({
      config: this.data.config,
      loaclPhoneNumber: inviter,
      pusherAvatar: this.data.pusherAvatar,
    }, () => {
      this.TRTCCalling = this.selectComponent('#TRTCCalling-component');
      this.bindTRTCCallingRoomEvent()
      this.bindTIMChatEvent()  //绑定即时通讯IM的事件,文章前面提到的即时通讯IM使用的发布订阅模式
      init_TIM(this.TRTCCalling.tsignaling._tim)  //重点在这里,将拿到的tim实例传给即时通讯IM实例的方法中去做操作
      this.TRTCCalling.login()
    })
  },
  call: function() {
    if (this.data.config.userID === this.data.invitee.userID) {
      wx.showToast({
        title: '不可呼叫本机',
      })
      return
    }
    this.data.config.type = 1
    this.setData({
      isCalling: true,
      callingFlag: true,
      inviteCallFlag: true,
      config: this.data.config,
    })
    this.TRTCCalling.call({ userID: this.data.invitee.userID, type: 1 })
  },
  bindTRTCCallingRoomEvent: function() {
    const TRTCCallingEvent = this.TRTCCalling.EVENT
    this.TRTCCalling.on(TRTCCallingEvent.INVITED, (event) => {
      this.setData({
        isCalling: true,
        invitation: event.data,
        incomingCallFlag: true,
      })
    })
    // 处理挂断的事件回调
    this.TRTCCalling.on(TRTCCallingEvent.HANG_UP, () => {
      this.setData({
        isCalling: false,
        callingFlag: false,
      })
    })
    this.TRTCCalling.on(TRTCCallingEvent.REJECT, () => {
      this.setData({
        isCalling: false,
        callingFlag: false,
        inviteCallFlag: false,
      })
      wx.showToast({//对方拒绝通话
        title: '对方已拒绝',
      })
      this.TRTCCalling.hangup()
    })
    this.TRTCCalling.on(TRTCCallingEvent.USER_LEAVE, () => {
      this.TRTCCalling.hangup()
      wx.showToast({//通话过程中,对方挂断
        title: '对方已挂断',
      })
    })
    this.TRTCCalling.on(TRTCCallingEvent.NO_RESP, () => {
      this.setData({
        isCalling: false,
        incomingCallFlag: false,
        inviteCallFlag: false,
      })
      wx.showToast({//对方无应答
        title: '无应答超时',
      })
      this.TRTCCalling.hangup()
    })
    this.TRTCCalling.on(TRTCCallingEvent.LINE_BUSY, () => {
      this.setData({
        isCalling: false,
        incomingCallFlag: false,
        inviteCallFlag: false,
      })
      wx.showToast({//对方忙线
        title: '对方忙线中',
      })
      this.TRTCCalling.hangup()
    })
    this.TRTCCalling.on(TRTCCallingEvent.CALLING_CANCEL, () => {
      this.setData({
        isCalling: false,
        incomingCallFlag: false,
      })
      wx.showToast({//对方取消语音邀请
        title: '通话已取消',
      })
    })
    this.TRTCCalling.on(TRTCCallingEvent.USER_ENTER, () => {
      this.setData({
        inviteCallFlag: false,
      })
    })
  },
  bindTIMChatEvent: function() {
    let that = this;
    wx.event.on('renderMsg',(e,newMsgForm)=>{
      let newMsgObj = e;
      if(newMsgForm == this.data.invitee.userID){
        this.setData({
          msgList : that.data.msgList.concat([newMsgObj])
        },()=>{
          that.scrollToBottom()
        })
      }
    })
    wx.event.on('conversationInit',()=>{
      getMsgList_TIM(this.data.invitee.userID);
    })
    wx.event.on('conversationRender',(e,nextReqMessageID,isCompleted)=>{
      let list = [...e];
      this.setData({
        'msgList' : list,
        'nextReqMessageID' : nextReqMessageID,
        'isCompleted' : isCompleted
      },()=>{
        that.scrollToBottom()
      })
    })
  },
  handleOnAccept: function() {
    console.log('接受邀请')
    this.data.config.type = this.data.invitation.type
    this.setData({
      callingFlag: true,
      incomingCallFlag: false,
      config: this.data.config,
    }, () => {
      this.TRTCCalling.accept()
    })
  },
  handleOnReject: function() {
    console.log('拒绝通话')
    this.setData({
      isCalling: false,
      incomingCallFlag: false,
    }, () => {
      this.TRTCCalling.reject();
      getMsgList_TIM(this.data.invitee.userID);
    })
  },
  handleOnCancel: function() {
    console.log('挂断通话')
    this.setData({
      inviteCallFlag: false,
    },()=>{
      this.TRTCCalling.hangup()
      getMsgList_TIM(this.data.invitee.userID);
    })
  },
  onBack: function() {
    wx.navigateBack({
      delta: 1,
    })
    this.TRTCCalling.logout()
  },
})

最后是最重要的: 我们把实例传进了即时通讯IM的初始化页面

 const app = getApp()
 
  var tim = '';

  function init_TIM(TIM) {//初始化im实时聊天  这里接收从外面传入的TIM实例
    let that = this
    if(app.globalData_TIM.isLogin){
      return false
    }
    tim = TIM;

    // 监听事件,例如:
    tim.on('sdkStateReady', function(event) {
      wx.event.emit('conversationInit') // 会话列表的监听函数
      // 收到离线消息和会话列表同步完毕通知,接入侧可以调用 sendMessage 等需要鉴权的接口
      // event.name - TIM.EVENT.SDK_READY
    });
  
    tim.on('onMessageReceived', function(event) {
      console.log('接受新消息',event);
      if(event.data[0].type == 'TIMCustomElem'){
        // 语音消息
        console.log(JSON.parse(event.data[0].payload.data))
        let audioMessage = JSON.parse(event.data[0].payload.data)
        return false
      }
      setGlobalMsg(event,'received');
      // 收到推送的单聊、群聊、群提示、群系统通知的新消息,可通过遍历 event.data 获取消息列表数据并渲染到页面
      // event.name - TIM.EVENT.MESSAGE_RECEIVED
      // event.data - 存储 Message 对象的数组 - [Message]
    });
  
    tim.on('onMessageRevoked', function(event) {
      // 收到消息被撤回的通知
      // event.name - TIM.EVENT.MESSAGE_REVOKED
      // event.data - 存储 Message 对象的数组 - [Message] - 每个 Message 对象的 isRevoked 属性值为 true
    });
  
    tim.on('onMessageReadByPeer', function(event) {
      // SDK 收到对端已读消息的通知,即已读回执。使用前需要将 SDK 版本升级至 v2.7.0 或以上。仅支持单聊会话。
      // event.name - TIM.EVENT.MESSAGE_READ_BY_PEER
      // event.data - event.data - 存储 Message 对象的数组 - [Message] - 每个 Message 对象的 isPeerRead 属性值为 true
    });
  
    tim.on('onConversationListUpdated', function(event) {
      // 收到会话列表更新通知,可通过遍历 event.data 获取会话列表数据并渲染到页面
      // event.name - TIM.EVENT.CONVERSATION_LIST_UPDATED
      // event.data - 存储 Conversation 对象的数组 - [Conversation]
    });
  
    tim.on('error', function(event) {
      // 收到 SDK 发生错误通知,可以获取错误码和错误信息
      // event.name - TIM.EVENT.ERROR
      // event.data.code - 错误码
      // event.data.message - 错误信息
    });
  
    tim.on('sdkStateNotReady', function(event) {
      // 收到 SDK 进入 not ready 状态通知,此时 SDK 无法正常工作
      // event.name - TIM.EVENT.SDK_NOT_READY
    });
  
    tim.on('kickedOut', function(event) {
      // 收到被踢下线通知
      // event.name - TIM.EVENT.KICKED_OUT
      // event.data.type - 被踢下线的原因,例如:
      //    - TIM.TYPES.KICKED_OUT_MULT_ACCOUNT 多实例登录被踢
      //    - TIM.TYPES.KICKED_OUT_MULT_DEVICE 多终端登录被踢
      //    - TIM.TYPES.KICKED_OUT_USERSIG_EXPIRED 签名过期被踢 (v2.4.0起支持)。 
    });
  
    tim.on('netStateChange', function(event) { 
      //  网络状态发生改变(v2.5.0 起支持)。 
      // event.name - TIM.EVENT.NET_STATE_CHANGE 
      // event.data.state 当前网络状态,枚举值及说明如下: 
      //     \- TIM.TYPES.NET_STATE_CONNECTED - 已接入网络 
      //     \- TIM.TYPES.NET_STATE_CONNECTING - 连接中。很可能遇到网络抖动,SDK 在重试。接入侧可根据此状态提示“当前网络不稳定”或“连接中” 
      //    \- TIM.TYPES.NET_STATE_DISCONNECTED - 未接入网络。接入侧可根据此状态提示“当前网络不可用”。SDK 仍会继续重试,若用户网络恢复,SDK 会自动同步消息  
    });
    app.globalData_TIM.isLogin = true;
    console.log('初始化TIM');
  }
  
  function sendMessage_TIM(sendTo,msg) {
    let message = tim.createTextMessage({
      to: sendTo,
      conversationType: TIM.TYPES.CONV_C2C,
      payload: {
        text: msg
      }
    });
    let promise = tim.sendMessage(message);
    promise.then(function(imResponse) {
      // 发送成功
      console.log('发送成功',imResponse)
      setGlobalMsg(imResponse,'send')
    }).catch(function(imError) {
      // 发送失败
      console.log('发送失败')
      console.warn('sendMessage error:', imError);
    });
  }

  function setGlobalMsg(data,type) {
    let msgarr = app.globalData_TIM.msg;
    let newMsgForm = '';
    if(type == 'received'){
      newMsgForm = data.data[0].from // 定义会话键值
      if(msgarr[newMsgForm] != undefined) {
        msgarr[newMsgForm].push(data.data[0])
      } else {
        msgarr[newMsgForm] = [data.data[0]]
      }
      app.globalData_TIM.msg = msgarr;
      wx.event.emit('renderMsg' , data.data[0] , newMsgForm) // 详情页的函数
    }else if(type == 'send'){
      newMsgForm = data.data.message.to // 定义会话键值
      if(msgarr[newMsgForm] != undefined) {
        msgarr[newMsgForm].push(data.data.message)
      } else {
        msgarr[newMsgForm] = [data.data.message]
      }
      app.globalData_TIM.msg = msgarr;
      wx.event.emit('renderMsg' , data.data.message , newMsgForm) // 详情页的函数
    }
  }

  function getMsgList_TIM(userID) {
    // 打开某个会话时,第一次拉取消息列表
    let promise = tim.getMessageList({conversationID: 'C2C'+userID, count: 15});
    promise.then(function(imResponse) {
      const messageList = imResponse.data.messageList; // 消息列表。
      const nextReqMessageID = imResponse.data.nextReqMessageID; // 用于续拉,分页续拉时需传入该字段。
      const isCompleted = imResponse.data.isCompleted; // 表示是否已经拉完所有消息。
      console.log(imResponse)
      let msgarr = app.globalData_TIM.msg
      let conversationTo = userID // 定义会话键值
      if(msgarr[conversationTo] != undefined) {
        msgarr[conversationTo].concat(messageList)
      } else {
        msgarr[conversationTo] = messageList
      }
      app.globalData_TIM.msg = msgarr;
      wx.event.emit('conversationRender' , msgarr[conversationTo] , nextReqMessageID , isCompleted) // 初始化聊天列表至全局成功
    });
  }

module.exports = {
  init_TIM,
  sendMessage_TIM,
  getMsgList_TIM
}

可以看到有几个地方和我们单独集成即时通讯IM是有区别: 1.不用我们自己初始化实例 2.不用我们实现登录和登出操作,因为语音聊天TRTCCalling组件中已经封装好了登录和登出,直接用就行 3.我们唯一要做的就是监听事件,实现初始化消息列表,收发消息等功能 4. 非常重要: 我们监听事件的方式变了,不知道大家发现没 这是以前的方式 在这里插入图片描述 这是现在的方式: 在这里插入图片描述 这个地方一定要注意修改,不然会报错没法监听到事件,最后给到大家这种方式每个事件对应的字符串 在这里插入图片描述 至此算是真正的将两种功能集成好了,也不会冲突,其它的具体UI样式以及具体功能接口那些大家就看看官方文档吧谢谢大家!

更新---------------------------

以上其实已经能够正常的使用即时通讯和音频对话了,但是因为有好几个朋友在评论或者私信里提到了trtc-calling组件退出重进后的重复登录问题,所以我这里做一个更新,把这个问题的出现和我的解决方式发给大家参考一下

四、TRTCCalling组件退出页面重新进入的Tim重复登录问题(或者说信令重复发送的问题)

1.TRTCCalling实现解析

首先我们初步发分析一下TRTCCalling组件实现是怎么样的 在这里插入图片描述 上面的截图是腾讯TRTCCalling组件的全部文件,我们大概几个关键文件的作用:

a. constants.js //定义了我们需要用到的常量信息(订阅TIM事件名/TRTC事件名/默认用户配置) b. user-controller.js //封装了用户操作(在音视频房间的用户操作) c. tsignaling-wx.js //注意!! 这是最关键的封装了以即时通讯tim为基础的各种信令操作,通过不同的信令操作驱动不同的音视频房间TRTC进行音视频互动的各种操作

通过上面简单的分析我们可以得出:

TRTCCalling组件的实现实际就是通过以即时通讯TIM为基础,发送不同的特定自定义即时通讯消息TIMCustomElem来模拟封装成不同的信令,而用户端通过识别不同的信令来调用音视频房间的加入/退出/接受邀请等等操作,从而实现了这么一个组件

2.重复登录问题的出现(信令重复)

问题重现: 我们打开官方的TRTCCalling组件的demo,打开多账号调试分别登录进入账号userB以及账号userB 在这里插入图片描述 可以看到在这个界面实际我们还并没有登录tim,也就是我们还并没有初始化使用trtccalling组件,我们点击语音通话 在这里插入图片描述 这时,两个用户都登录了,并且都第一次初始化了tim实例,我们此时进行语音邀请操作 在这里插入图片描述 在这里插入图片描述 第一张图可以看到,我们在userA向userB发出语音邀请的时候,userB接受到了一个信令消息类型 1 ,也就是语音邀请的信令 ,这时userB的组件实例识别了信令类型1,于是渲染了组件实例有邀请进入的页面, 我们再用userB拒绝邀请,第二张图显示接受到了一个信令消息 4,也就是远端用户决绝邀请的信令,这时userA的组件实例识别信令消息4,于是取消了语音邀请的页面. 通过上面的测试我们证实了之前我们对于TRTCCalling组件实现方式的猜想 那么到目前为止,整个过程是没有问题的

但是!!一旦我们有一方退出了这个页面,也就是组件实例销毁之后,再次进入这个页面,重新初始化,再进行操作,便会出现问题了,我们看以下截图 在这里插入图片描述 这是userA,我先退出了userA,退回到了刚刚选择语音通话还是视频通话的页面,也就是退出了TRTCCalling组件实例的页面,然后再重新进入这个页面,然后再向userB发起邀请,userB这边没有任何问题,依旧能够收到userA的邀请,但是userB点击拒绝后,我们可以看到userA这里收到了 两个信令消息 4 ,而且我们发现现在的userA,接收到的任何信令消息都是两个,一旦再退出再进入,就会变成3个,这就造成会有很多重复的信令操作,让userA这边出现莫名其妙挂断/莫名其妙链接超时等bug

3.解决办法

首先我们要明白信令是从哪个地方发出来的,答案是tsignaling-wx.js 在这里插入图片描述 这个文件也是组件初始化tim的文件,要修改官方的这个文件,那还不如我们自己用底层tim写一套信令,所以暂时放弃这个想法,那我们就换个思维,我们知道造成这个原因是因为trtccalling组件所在的页面在初始化和销毁的时候,tsignaling-wx.js这个实例也进行了登录(创建)和登出(销毁)的操作,那我们就让它不要销毁,一旦初始化了,就让它保持登录状态,即时页面退出也不销毁,这样我们在其它页面也能收到tim实例的消息,而且再次进入的时候不会产生发出重复信令而造成bug

实现:

第一步:修改组件创建时的tsignaling初始化操作

在这里插入图片描述 找到TRTCCalling组件的js文件TRTCCalling.js

找到组件的生命周期函数lifetimes下的created

这里找到了tsignaling实例的初始化语句

 this.tsignaling = new TSignaling({ SDKAppID: 你的ID })

我们将它进行修改成如下

//首先在app.js中定义一个全局标识TRTCCallingInit来标记是否已经初始化tsignaling
  if(app.globalData_TIM.TRTCCallingInit){
    //已经初始化过tsignaling,则不再进行初始化,直接将存放的已经初始化的tsignaling实例挂成当前组件的tsignaling
    this.tsignaling = app.globalData_TIM.tsignaling
  }else{
    //还没有初始化tsignaling,初始化实例tsignaling,并将实例存到全局变量里
    this.tsignaling = new TSignaling({ SDKAppID: 你的ID })
    app.globalData_TIM.tsignaling = this.tsignaling;
  }

第二步:将新页面组件实例的事件监听挂到tsignaling上

首先我们看到TRTCCalling.js中的 login方法,成功后的回调有一个操作this._initEventEmitter(),这是为了将各种事件监听绑定到组件上,既然我们取消了每次进入都要登录的操作,那么我们就需要在第一次登录成功后将tsignaling的标识符变为true

![在这里插入图片描述](https://img-blog.csdnimg.cn/2021030111123648.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDYyMjk4NA==,size_16,color_FFFFFF,t_7

然后找到生命周期函数attached增加如下语句,保证新组件创建时仍然能够绑定到各种事件监听 在这里插入图片描述

第三步:修改业务页面的登录与登出操作

实例中的业务页面是audioCall.js 首先将onLoad函数中的登录操作更改为如果标识符TRTCCallingInit为true则不进行重复登录(因为一旦初始化过一次,我们的tim就保持在登录状态的,并没有登出) 在这里插入图片描述

然后将页面的登出操作取消掉 在这里插入图片描述 这样就基础的实现了保持tim登录,重复进入不会有重复信令发送 测试一下 在这里插入图片描述 无论我退出页面多少次,再次进入进行操作,只会收到一条信令消息,tim也没有一直登入登出

注:这是我自己摸索捣鼓出来的一个笨拙的解决方案,我相信一定有更好的方案,如果大家有好的方法,请提出来指正,谢谢

如果对您有帮助请点个赞或收个藏给作者一点鼓励吧! 谢谢!