本文详细介绍如何建立一个简单的项目并使用声网 RTM SDK 实现消息发送与接收。
https://miniapp.agoraio.cn
https://webcollector-rtm.agora.io:6443
https://logservice-rtm.agora.io
https://ap-web-1.agoraio.cn
https://ap-web-2.agoraio.cn
https://ap-web-3.agoraio.cn
https://ap-web-4.agoraio.cn
wss://miniapp.agoraio.cn
参考以下步骤创建一个声网项目:
声网会给每个项目自动分配一个 App ID 作为项目唯一标识。
在声网控制台的项目管理页面,找到你的项目,点击 App ID 右侧的 图标,即可获取项目的 App ID。
参考以下步骤获取 App 证书:
在声网控制台的项目管理页面,找到你的项目,点击配置。
点击主要证书下面的复制图标,即可获取项目的 App 证书。
为提高项目的安全性,声网推荐使用 Token 对即将登录 RTM 系统的用户进行鉴权。
为了方便测试,声网服务器提供部署签发 RTM Token 的功能。参考以下步骤获取 RTM Token:
login
时,请确保填入的用户 ID 与生成 RTM Token 时填入的用户 ID 一致。在微信开发者工具中,创建一个小程序。
创建完成后,微信开发者工具会生成一个基本的 Hello World 微信小程序项目。
下载最新版云信令微信小程序 SDK 并解压。
在将 SDK 包中的 .js
文件复制到 miniprogram-rtm/libs
目录下,并重命名为 rtm.miniapp.js
。
在 miniprogram-rtm/Utils
文件夹中新建 agora-rtm.js
,并通过以下方式集成 SDK:
const AgoraRTM = require("../libs/rtm.miniapp.js")
在 miniprogram-rtm/Utils
文件夹中新建 config.js
,设置 App ID 和 Token。文件内容如下:
// 填入 App ID
const APPID = "<Your App ID>";
// 填入 Token
const TOKEN = "<Your Token>";
module.exports = {
APPID: APPID,
TOKEN: TOKEN
}
在 miniprogram-rtm/Utils
文件夹中新建 event-bus.js
,本文使用 iny-bus 开源项目 提供的 EventBus 机制随时刷新页面信息。event-bus.js
文件内容可直接复制 index.js 的内容。
在 Utils/agora-rtm.js
中新增如下引用:
const eventBus = require("./event-bus.js")
const { APPID } = require("./config.js")
在引用语句下面增加以下逻辑:
class RTMClient {
constructor () {
// 创建客户端实例
this._client = AgoraRTM.createInstance(APPID)
this._accountName = ''
this._channel = null
this.isLogin = false
this.isOff = false
this.messageCache = []
this._eventBus = eventBus
this.subscribeLoginEvents()
}
// 登录 RTM 系统
login(token, accountName) {
this._accountName = accountName
const object = {
token: token,
uid: accountName
}
return this._client.login(object)
}
// 登录后订阅连接状态变化回调和收到点对点消息回调
subscribeLoginEvents() {
this._client.on('ConnectionStateChanged', (newState, reason) => {
this._eventBus.emit('ConnectionStateChanged', newState, reason)
})
this._client.on('MessageFromPeer', (message, peerId, { isOfflineMessage }) => {
console.log('MessageFromPeer')
if(isOfflineMessage) {
let object = {
message: message.text,
peerId: peerId,
isOfflineMessage: isOfflineMessage
}
this.messageCache.push(object)
}
this._eventBus.emit('MessageFromPeer', message, peerId, isOfflineMessage)
})
}
// 登出 RTM 系统
logout() {
return this._client.logout().then(() => {
this._eventBus.clear()
this.messageCache = []
})
}
// 发送点对点消息
sendPeer(msg, peerId) {
return this._client.sendMessageToPeer({ text: msg }, peerId, {
enableHistoricalMessaging: false,
enableOfflineMessaging: this.isOff
})
}
// 加入 RTM 频道后订阅频道事件
joinChannel(id) {
this._channel = this._client.createChannel(id)
this.subscribeChannelEvents(id)
return this._channel.join()
}
subscribeChannelEvents() {
this._channel.on('MemberJoined', (e) => {
this._eventBus.emit('MemberJoined', e)
})
this._channel.on('MemberLeft', (e) => {
this._eventBus.emit('MemberLeft', e)
})
this._channel.on('MemberCountUpdated', (e) => {
this._eventBus.emit('MemberCountUpdated', e)
})
this._channel.on('ChannelMessage', (message, memberId) => {
this._eventBus.emit('ChannelMessage', message, memberId)
})
}
// 发送频道消息
sendChannel(msg) {
return this._channel.sendMessage({text: msg})
}
// 获取频道成员列表
getMembers() {
return this._channel.getMembers()
}
// 离开频道
leaveChannel() {
return this._channel.leave()
}
on(event, callback) {
this._eventBus.on(event, callback)
}
}
module.exports = RTMClient
在 Utils/util.js
中新增 debounce 相关逻辑:
const formatTime = date => {
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
const hour = date.getHours()
const minute = date.getMinutes()
const second = date.getSeconds()
return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':')
}
const formatNumber = n => {
n = n.toString()
return n[1] ? n : '0' + n
}
const debounce = function(foo, t) {
let timer
return function() {
if (timer !== undefined) {
clearTimeout(timer)
}
timer = setTimeout(() => {
foo.apply(this, arguments)
}, t)
}
}
module.exports = {
formatTime: formatTime,
debounce: debounce
}
我们将 index/index.js
定位为登录页面。清空 pages/index/index.js
文件的内容并实现以下页面逻辑:
const RTMClient = require("../../utils/agora-rtm.js")
const { debounce } = require("../../utils/util.js")
const { TOKEN } = require("../../utils/config.js")
const app = getApp()
Page({
data: {
accountName: ''
},
login: function () {
return debounce(this.bindLogin(), 500)
},
bindLogin: function () {
if(!this.data.accountName) {
console.log('accountName is null')
if(this.rtm.isLogin) {
console.log('already login')
return
}
return
}
this.rtm.login(TOKEN, this.data.accountName).then(() => {
console.log('login success')
this.rtm.isLogin = true
wx.navigateTo({
url: `../message/message`
});
}).catch((err) => {
console.log('login failed', err)
})
},
onInputAccount: function (e) {
this.setData({
accountName: e.detail.value
})
},
onLoad: function () {
this.rtm = new RTMClient()
this.rtm.on('ConnectionStateChanged', (newState, reason) => {
console.log('The connection status', newState)
console.log('The reason for the state change', reason)
})
app.globalData.agoraRtm = this.rtm
},
onUnload: function () {
}
})
清空 pages/index/index.wxml
文件的内容并实现以下页面设计:
<!--index.wxml-->
<view class=" agora-bg">
<view class="content flex-center-column">
<view class="logo-section flex-center-column">
<h1>小程序 Demo</h1>
</view>
<view class="form-section flex-center-column">
<view class="inputWrapper">
<input placeholder-style='color:#A3D1E0' class="channelInput" placeholder='在此处填入账号名'
bindinput="onInputAccount" value="{{accountName}}" >
</input>
</view>
<button plain="true" bindtap="login" class="loginBtn">登录</button>
</view>
</view>
</view>
清空 pages/index/index.wxss
文件的内容并实现以下页面设计:
page {
height: 100%;
}
.content {
position: absolute;
left: 80rpx;
right: 80rpx;
width: auto;
height: 100%;
}
.content .logo-section .logo{
margin-bottom: 20rpx;
}
.content .logo-section .h1{
margin-bottom: 150rpx;
}
.content .form-section{
width: 100%;
}
.content .inputWrapper{
width: 100%;
border-radius: 20rpx;
margin-top: 16rpx;
background-color: rgba(255,255,255,0.4);
border: 1px solid #98BECA;
}
.account {
height: 80rpx;
}
.content .channelInput{
font-size: 28rpx;
padding: 0 30rpx;
height: 80rpx;
color: #5083AA;
}
.content .loginBtn{
background-color: #FEFFFE;
color: #5083AA;
width: 100%;
height: 80rpx;
font-size: 28rpx;
margin-top: 32rpx;
line-height: 80rpx;
box-shadow: 0px 4px 4px rgba(84,163,186,0.15);
font-weight: bold;
border: 0;
}
.content .envBtn{
/* background-color: white; */
color: white;
height: 80rpx;
font-size: 28rpx;
margin-top: 32rpx;
line-height: 80rpx;
border: 0;
margin-bottom: -10rpx;
}
.content .footer{
justify-content: flex-end;
flex-grow: 1;
font-size: 24rpx;
margin-bottom: 32rpx;
color: #63C5E6;
}
.agora-user {
display: flex;
}
新建 pages/message
文件夹并在文件夹中新建 Page,定位为消息收发页面。Page 名设为 message
。文件夹中会生成 message.js
, message.json
, message.wxml
, 和 message.wxss
文件。
设置 message.js
内容为以下代码:
Page({
data: {
channelName: '',
channelMessage: '',
peerId: '',
peerMessage: '',
accountName: '',
channelMessageArray: [],
peerMessageArray: [],
joinStatus: false,
bottom: '',
currentTab: 0,
channelMembers: [],
queryMembers: false,
showMsg: false,
msgDetails: '',
},
onChannelName: function (e) {
this.setData({
channelName: e.detail.value
})
},
onChannelMessage: function (e) {
this.setData({
channelMessage: e.detail.value
})
},
onPeerId: function (e) {
this.setData({
peerId: e.detail.value
})
},
onPeerMessage: function (e) {
this.setData({
peerMessage: e.detail.value
})
},
bindJoin: function () {
if(!this.data.channelName) {
wx.showModal({
title: 'message',
content: 'Please enter the channel name',
})
return
}
this.setData({
joinStatus: true,
})
this.rtm.joinChannel(this.data.channelName).then(() => {
console.log('join channel success')
}).catch((err) => {
console.log('join channel failed', err)
})
},
bindLeave: function () {
this.setData({
joinStatus: false
})
this.rtm.leaveChannel().then(() => {
console.log('leave success')
}).catch((err) => {
console.log('leave failed', err)
})
},
bindQuery: function() {
if(!this.data.joinStatus) {
console.log('Please join the channel')
return
}
this.setData({
queryMembers: true
})
this.rtm.getMembers().then((members) => {
console.log('channel members', members)
this.setData({
channelMembers: members
})
}).catch((err) => {
console.log('get members failed', err)
})
},
bindChannelSend: function () {
if(!this.data.channelMessage) {
return
}
if(!this.data.joinStatus) {
wx.showModal({
title: 'message',
content: 'Please join the channel',
})
return
}
this.rtm.sendChannel(this.data.channelMessage).then(() => {
console.log('send channel message success')
}).catch((err) => {
console.log('send channel message failed', err)
})
let object = {
uid: this.rtm._accountName,
message: this.data.channelMessage
}
this.data.channelMessageArray.push(object)
this.setData({
channelMessageArray: this.data.channelMessageArray
})
this.setData({
bottom: 'scrollBottom',
channelMessage: ''
})
},
openOffMsg: function() {
console.log('open offline message')
this.rtm.isOff = true
},
bindPeerSend: function () {
if(!this.data.peerMessage) {
return
}
if(!this.data.peerId) {
wx.showModal({
title: 'message',
content: 'Please enter the peer id',
})
return
}
this.rtm.sendPeer(this.data.peerMessage, this.data.peerId).then((e) => {
console.log('send peer message success', e)
if(e.hasPeerReceived) {
console.log('peer received success')
this.msgBounced('peer received success')
} else {
console.log('peer received failed')
this.msgBounced('peer received failed')
}
}).catch((err) => {
console.log('send peer message failed', err)
})
let object = {
peerId: this.data.peerId,
message: this.data.peerMessage,
isLocal: true
}
this.data.peerMessageArray.push(object)
this.setData({
peerMessageArray: this.data.peerMessageArray
})
this.setData({
bottom: 'scrollBottom',
peerMessage: ''
})
},
swichNav: function(e) {
if( this.data.currentTab === e.target.dataset.current ) {
return false
} else {
this.setData({
currentTab: e.target.dataset.current
})
}
},
bindChange: function(e) {
this.setData({
currentTab: e.detail.current
})
},
msgBounced: function(details) {
this.setData({
showMsg: true,
msgDetails: details,
})
setTimeout(() => {
this.setData({
showMsg: false
})
}, 2000)
},
peerOffMsg: function() {
this.rtm.messageCache.forEach((item) => {
item.isLocal = false
this.data.peerMessageArray.push(item)
})
this.setData({
peerMessageArray: this.data.peerMessageArray
})
},
onPeerMsgEvent: function() {
this.rtm.on('MessageFromPeer', (message, peerId, isOfflineMessage) => {
let object = {
message: message.text,
peerId: peerId,
isLocal: false,
isOfflineMessage: isOfflineMessage
}
this.data.peerMessageArray.push(object)
this.setData({
peerMessageArray: this.data.peerMessageArray
})
})
},
onChannelEvent: function() {
this.rtm.on('ChannelMessage', (message, memberId) => {
let object = {
uid: memberId,
message: message.text
}
this.data.channelMessageArray.push(object)
this.setData({
channelMessageArray: this.data.channelMessageArray
})
})
this.rtm.on('MemberJoined', (memberId) => {
console.log('memberId: ', memberId)
this.msgBounced(`${memberId} join channel`)
})
this.rtm.on('MemberLeft', (memberId) => {
console.log('memberId: ', memberId)
this.msgBounced(`${memberId} already left`)
})
},
onLoad: function() {
this.rtm = getApp().globalData.agoraRtm
this.setData({
accountName: this.rtm._accountName
})
this.peerOffMsg()
this.onPeerMsgEvent()
this.onChannelEvent()
this.rtm.on('ConnectionStateChanged', (newState, reason) => {
console.log('The connection status', newState)
console.log('The reason for the state change', reason)
})
},
onShow: function() {
},
onReady: function() {
},
onHide: function() {
},
onUnload: function() {
if(this.rtm.isLogin) {
this.rtm.logout().then(() => {
this.rtm.isLogin = false
console.log('logout success')
}).catch((err) => {
console.log('logout failed', err)
})
}
},
onPullDownRefresh: function() {
},
onReachBottom: function() {
},
onShareAppMessage: function () {
},
onPageScroll: function() {
},
onResize: function() {
}
})
设置 message.wxml
内容为以下代码:
<view class="agora-box">
<view class="swiper-tab">
<view class="swiper-tab-list {{currentTab===0 ? 'on' : ''}}" data-current="0" bindtap="swichNav">Channel</view>
<view class="swiper-tab-list {{currentTab===1 ? 'on' : ''}}" data-current="1" bindtap="swichNav">Peer</view>
</view>
<scroll-view class="message-box" scroll-y scroll-into-view="{{bottom}}">
<view wx:if="{{showMsg}}" class="msg-box">
{{msgDetails}}
</view>
<view wx:if="{{currentTab === 0}}" >
<view wx:if="{{queryMembers}}">
当前频道用户: {{channelMembers}}
</view>
<view wx:for="{{channelMessageArray}}" wx:key="index" wx:for-item="channelItem">
<view>
uid: {{channelItem.uid}}
<view class="mes">
{{channelItem.message}}
</view>
</view>
</view>
</view>
<view wx:if="{{currentTab === 1}}">
<view wx:for="{{peerMessageArray}}" wx:key="index" wx:for-item="peerItem">
<view wx:if="{{peerItem.isLocal}}">
accountName: {{accountName}}
</view>
<view wx:else>
peerId: {{peerItem.peerId}}
</view>
<view class="mes mesPeer">
{{peerItem.message}}
</view>
</view>
</view>
<view id="scrollBottom">
</view>
</scroll-view>
<swiper current="{{currentTab}}" class="swiper-box" duration="300" bindchange="bindChange">
<swiper-item>
<view class="operate-box">
<view class="information-box">
<view class="input-message">
<input placeholder-style='color:#A3D1E0' class="shortInput" placeholder='channel name'
bindinput="onChannelName" value="{{channelName}}">
</input>
</view>
<view class="button-box">
<button wx:if="{{!joinStatus}}" class="mini-btn" bindtap="bindJoin" type="default" size="mini">Join</button>
<button wx:else class="mini-btn" bindtap="bindLeave" type="default" size="mini">Leave</button>
<button class="mini-btn query" bindtap="bindQuery" type="default" size="mini">Query</button>
</view>
</view>
<view class="information-box">
<view class="input-message">
<input placeholder-style='color:#A3D1E0' class="channelInput" placeholder='channel message'
maxlength="-1" bindinput="onChannelMessage" cursor="1" value="{{channelMessage}}">
</input>
</view>
<view class="button-box">
<button class="mini-btn" bindtap="bindChannelSend" type="default" size="mini">Send</button>
</view>
</view>
</view>
</swiper-item>
<swiper-item>
<view class="operate-box">
<view class="information-box">
<view class="input-message">
<input placeholder-style='color:#A3D1E0' class="shortInput" placeholder='peer id'
bindinput="onPeerId" value="{{peerId}}">
</input>
</view>
<view class="body-view">Open offline
<switch style='zoom:.75;' bindchange="openOffMsg"/>
</view>
</view>
<view class="information-box">
<view class="input-message">
<input placeholder-style='color:#A3D1E0' class="channelInput" placeholder='peer message'
maxlength="-1" bindinput="onPeerMessage" value="{{peerMessage}}">
</input>
</view>
<view class="button-box">
<button class="mini-btn" bindtap="bindPeerSend" type="default" size="mini">Send</button>
</view>
</view>
</view>
</swiper-item>
</swiper>
</view>
设置 message.wxss
为以下内容:
page {
height: 100%;
}
.agora-bg {
width: 100%;
height: 100%;
position: absolute;
background: linear-gradient(to bottom, #8FD3F5, #D3FDFF);
color: #5083AA;
}
.h1{
font-size: 32rpx;
}
.h2{
font-size: 24rpx;
}
.flex-center-column{
display: flex;
flex-direction: column;
align-items: center;
}
.agora-box {
width: 100%;
height: 100%;
background: linear-gradient(to bottom, #8FD3F5, #D3FDFF);
color: #5083AA;
}
#scrollBottom {
height: 1rpx;
}
.operate-box {
border-top: 2rpx solid #5083AA;
}
.input-message {
border-radius: 15rpx;
background-color: rgba(255,255,255,0.4);
border: 1px solid #98BECA;
}
.channelInput{
width: 500rpx;
font-size: 32rpx;
padding: 12rpx;
color: #5083AA;
}
.shortInput {
width: 280rpx;
font-size: 32rpx;
padding: 12rpx;
color: #5083AA;
}
.information-box {
display: flex;
justify-content: space-between;
margin: 10rpx 15rpx 20rpx 15rpx;
}
.button-box {
padding-top: 10rpx;
}
.query {
margin-left: 10rpx;
}
.joinButton {
margin-right: 20rpx;
}
.message-box {
height: 75%;
}
.channel-box {
display: flex;
justify-content: space-between;
}
.members {
width: 100%;
height: 100rpx;
}
.body-view {
margin-top: 10rpx;
}
.mes {
border-radius: 10rpx;
font-size: 35rpx;
min-height: 65rpx;
/* display: flex;
flex-wrap: wrap; */
min-width: 50rpx;
max-width: 350rpx;
/* 控制消息显示换行 */
word-break: break-all;
word-wrap: break-word;
background-color: white;
padding-left: 10rpx;
padding-top: 10rpx;
margin-left: 10rpx;
}
.mesPeer {
}
.swiper-tab{
width: 100%;
text-align: center;
line-height: 65rpx;
}
.swiper-tab-list{
font-size: 30rpx;
display: inline-block;
width: 50%;
color: #777777;
}
.on {
color: #5083AA;
border-bottom: 3rpx solid #5083AA;
}
.swiper-box{
display: block;
height: 200rpx;
}
.msg-box{
min-width: 280rpx;
height: 90rpx;
margin-top: 20rpx;
margin-right: 10rpx;
line-height: 90rpx;
background-color: #8FD3F5;
opacity: 0.8;
border-radius: 10rpx;
z-index: 99;
visibility: 1;
float: right;
padding-left: 5rpx;
}
将 app.js
中 globalData
中的 userInfo
更改为 agoraRtm
:
globalData: {
agoraRtm: null
}
如果你在编译项目时,微信开发者工具报告渲染层错误,请检查使用的调试基础库版本并尝试选择较早的版本,例如 2.15.0。
使用微信小程序 SDK 开发过程中,你还可以参考如下文档: