背景:在最近接的一个app项目中,需要有即时聊天的功能;经过技术选型后敲定使用融云IM
。
融云IM
聊天会话、聊天记录默认存到本地,和微信同理
据我接入后经验来说,融云IM
还是做的不错的,即时聊天的功能也一应俱全,但就是uniapp
的文档写的很不友好。
版本: uniapp 5.x, 无UI集成(IMLib)
1. 安装相关插件、依赖
文档地址:https://doc.rongcloud.cn/im/uni-app/5.X/quick/include/web
github地址:https://github.com/rongcloud/rongcloud-uniapp-imlib (哪里不明白了,可以参考此)
去uniapp插件市场安装
RCUnilM
原生插件(我是自动安装,非手动安装此插件)RCUnilM
原生插件地址:https://ext.dcloud.net.cn/plugin?id=6120在
manifest.json -> APP
原生插件配置 -> 加入原生插件RCUniIM
制作自定义调试基座
使用原生插件需要先制作基座, 制作完成后, 运行前 选刚刚打包好的基座哦
安装依赖包
npm i @rongcloud/imlib-uni -S
项目集成引用
import { init, connect } from '@rongcloud/imlib-uni'
2. 初始化IM、全局监听IM状态、连接IM
初始化IM
只需全局初始化一次IM 即可,切勿多次初始化,比如在用户登录后,或首页进行一次
appKey
: 在融云开发者账号后台获取
import { init } from '@rongcloud/imlib-uni'
init('<Your-App-Key>');
全局监听IM状态
初始化完成后,应在建立连接之前添加事件监听器,及时获取相关事件通知!, 监听器仅支持添加一次,多次添加后前者会被覆盖。
我添加的监听有如下(还有许多监听状态可添加,见文档):
// 连接状态监听
addConnectionStatusListener(listener => {
if (listener.data.status === 3 || listener.data.status === 4) {
let _t = listener.data.status === 3 ? '您的账号在其他设备已登录' : '登录过期,请重新登录'
uni.showToast({title: _t, icon: 'none'})
uni.reLaunch({url: '/pages/login/login'})
}
if (listener.data.status === -1) {
uni.showToast({title: "当前网络不可用"})
}
if (listener.data.status === 14) {
uni.showToast({title: "连接超时"})
}
if (listener.data.status === 4) {
uni.showToast({title: "Token 过期"})
}
if (listener.data.status === 0) {
console.log("连接成功")
}
if (listener.data.status === 2) {
console.log("未链接 去链接")
}
})
// 监听新消息
addReceiveMessageListener(message => {
console.log(message)
}
连接IM
在登录成功后,接收到服务端传输来的用户token后建立IM连接
import { connect } from '@rongcloud/imlib-uni'
connect(this.selfUser.rcloudToken, (result) => {
if (result.code === 0) {
console.log('连接成功')
// 获取历史会话、未读消息数量
this.getConversation()
this.getAllUnRead()
} else {
console.log('连接失败')
}
})
3. 发送消息(文字、表情、多媒体)
表情也是文本,发送表情和文本是一样的,故先介绍如何引入表情
引入表情
此处参考:融云IM web版4.x 文档:https://doc.rongcloud.cn/im/Web/4.X/guide/private/msgmanage/msgsend/web#emoji
// 首先,在聊天室页面引入emoji插件
onLoad() {
var config = {
size: 52,
url: '//f2e.cn.ronghub.com/sdk/emoji-48.png',
lang: 'en',
extension: {
dataSource: {
u1F914: { // 自定义 u1F914 对应的表情
en: 'thinking face', // 英文名称
zh: '思考', // 中文名称
tag: '🤔', // 原生 Emoji
position: '0 0' // 所在背景图位置坐标
}
},
url: '//cdn.ronghub.com/thinking-face.png' // 新增 Emoji 背景图 url
}
};
// 初始化
RongIMEmoji.init(config);
this.emoji = RongIMEmoji.list;
}
this.emoji
是表情列表,拿此值渲染展示即可;(有的表情是一个字符 有的是两个)
发送文本消息
handleSend() {
var self = this
// 去除空格和换行
let _s = self.draft.toString()
_s = _s.replace(/[\r\n]/g,"");
_s = _s.replace(/\ +/g,"");
if (!_s || _s.length === 0) { // 输入框为空不发送
return
}
const msg = {
conversationType: ConversationType.PRIVATE,// 会话类型
targetId: self.friendAccount,// 目标ID
content: {
objectName: 'RC:TxtMsg', // 消息类型
userInfo: {
userId: self.selfUser.rongCloudUserId,
name: self.selfUser.nickName,
portraitUrl: defaultAvatar,
},
content: self.draft, // 内容
extra: `附加信息`
}
}
sendMessage(msg,(res) => {
// 消息发送成功
if (res.code === 0) {
// 清空输入框
self.draft = ''
} else {
uni.showToast("消息发送失败")
console.log(res)
}
})
},
发送图片消息
// 选择图片
openPhoto() {
var self = this
uni.chooseImage({
count: 9,
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
sourceType: ['camera', 'album']
success: function (res) {
// 发送多媒体
for(let i=0;i<res.tempFilePaths.length;i++){
// 这一步重要,必须这样后才可以发送
uni.getImageInfo({
src: res.tempFilePaths[i],
success: (imgInfo) => {
self.senMedia(imgInfo, res.tempFilePaths[i])
}
})
}
}
})
},
// 发送
senMedia(imgInfo) {
var self = this
let avatar = this.selfUser.avatar ? this.selfUser.avatar : this.defaultAvatar
const msg = {
conversationType: ConversationType.PRIVATE,// 会话类型
targetId: this.friendAccount, // 对方融云账号
content: {
objectName: 'RC:ImgMsg', // 消息类型
local: imgInfo.path, // 图片地址
userInfo: {
userId: this.selfUser.rongCloudUserId,
name: this.selfUser.nickName,
portraitUrl: avatar,
},
extra: `附加信息`
},
pushContent: '[图片]',
pushData: '',
}
sendMediaMessage(msg, {
success: (messageId) => {
// 发送成功回调
console.log('发送成功回调', messageId)
},
progress: (progress, messageId) => {
// 发送进度回调
console.log('发送进度回调', messageId, progress)
},
cancel: (messageId) => {
// 发送取消回调
console.log('发送取消回调', messageId)
},
error: (errorCode, messageId) => {
// 发送错误回调
console.log('发送错误回调', messageId, errorCode)
}
})
}
4. 获取历史消息记录、聊天已读回执
在聊天页面中获取与对方的历史消息
// 获取本地历史消息记录
getLocaMessage() {
const self = this
const conversationType = ConversationType.PRIVATE // 会话类型
const targetId = self.friendAccount // 对方的融云账号
const objectNames = ['RC:TxtMsg', 'RC:ImgMsg', 'RC:GIFMsg'] // 要获取的消息类型
const timeStamp = new Date().getTime()
const count = 1000 // 获取的消息数量
const isForward = true // 是否向前获取
getHistoryMessagesByTimestamp(conversationType, targetId, objectNames, timeStamp, count, isForward,
(result) => {
// 获取历史消息成功
if(result.code === 0){
result.messages.forEach(item => {
// 根据自己显示需求,来调整该消息对象
let obj = {}
obj['objectName'] = item.objectName
obj['content'] = item.content
obj['sentTime'] = item.sentTime
obj['messageId'] = item.messageId
if (item.senderUserId === self.selfUser.rongCloudUserId) {
obj['who'] = 'me'
obj['avatar'] = this.selfUser.avatar || this.defaultAvatar
} else {
obj['who'] = 'other'
obj['avatar'] = this.friendAvatar
}
self.history.unshift(obj)
})
// 聊天已读回执
if (result.messages.length > 0) {
self.setRead()
}
})
})
},
// 聊天已读回执
setRead() {
const conversationType = ConversationType.PRIVATE // 会话类型
const targetId = this.friendAccount // 对方的融云账号
const timestamp = this.history[this.history.length - 1].sentTime
// 已读回执
sendReadReceiptMessage(conversationType, targetId, timestamp)
// 清除当前会话未读数
clearMessagesUnreadStatus(conversationType, targetId, timestamp, (res) =>{
// 清除未读数成功
console.log("清除未读数:::", res)
})
},
5. 获取历史会话记录及显示
直接获取,并显示即可 ps:用户头像昵称在发送消息时已包含到消息体中,渲染时候通过getWho
、getAvatar
、getNickname
对应获取
会话类型:https://doc.rongcloud.cn/im/uni-app/5.X/conversation/get/web
template
<view class="message" v-for="(m, index) in messageList" :key="index" @click="toChatPage(m)">
<view class="avatar">
<view class="count" v-if="m.unreadMessageCount > 0">{{ m.unreadMessageCount }}</view>
<image :src="getAvatar(m)"></image>
</view>
<view class="content">
<view class="title">
<view class="username">{{getNickname(m)}}</view>
<view class="time">{{$u.timeFrom(m.sentTime, 'yyyy年mm月dd日 hh:MM')}}</view>
</view>
<view class="detail">{{getWho(m)}}{{ m.objectName === 'RC:TxtMsg' ? m.latestMessage.content : '[图片]' }}</view>
</view>
</view>
script
getConversation() {
const conversationTypes = [ConversationType.PRIVATE] // 会话类型列表
const count = 1000 // 获取的会话数量
const timestamp = 0 // 会话的时间戳(获取这个时间戳之前的会话列表,0 表示从最新开始获取)
getConversationList(conversationTypes, count, timestamp, (res) => {
if (res.code === 0 && this.messageList !== res.conversations) {
this.messageList = res.conversations
uni.setStorageSync('messageList', this.messageList)
}
})
},
6. 监听新消息(好友请求、聊天信息)
思考:1. 监听到新消息后判断其类型,是聊天信息还是好友请求还是系统公告等
在监听新消息返回的对象中, 通过message.objectName
字段可判断消息类型
addReceiveMessageListener(message => {
let data = message.data // 新消息
let chatType = ['RC:ImgMsg', 'RC:TxtMsg'] // 普通聊天信息类型
if (data.message.objectName === 'RC:ContactNtf') {
console.log("好友请求信息")
} else if(chatType.includes(data.message.objectName)){
console.log(普通聊天信息)
}
})
思考:2. 监听到新消息后如何反馈给用户
通过手机振动+底部tabbar角标来反馈的(没有加下拉提示和顶部弹窗那种)
// 手机震动
uni.vibrateShort({success: function () {console.log('短震动---震动成功!');}})
uni.vibrateLong({success: function () {console.log('长震动--震动成功');}})
// 设置和移除底部tabbar角标
// index:tabbar索引 text:显示内容
uni.setTabBarBadge({index: 0, text: 10})
思考:3. 如何把当前新消息及时更新到当前聊天的界面
在监听到新消息时,主动设置vuex
中特定字段, 并在聊天页面监听vuex
中该字段的变化而同步都当前聊天界面(app同时只能打开一个页面)
// 获取到时候主动设置
this.$store.commit("setNewMessage", _message)
// 聊天页面监听获取
computed: {
newMessage() {
return this.$store.state.currentNewMessage
},
}
watch: {
newMessage(n, o) {
if (n) {
console.log("有信息")
}
}
完整代码
listenForNewMessages() {
// 监听新消息
addReceiveMessageListener(message => {
// 普通消息类型
let chatType = ['RC:ImgMsg', 'RC:TxtMsg']
let data = message.data // 新消息
if (data.message.objectName === 'RC:ContactNtf') {
// 加好友请求
if (data.message.content.operation === 'Request') {
uni.vibrateShort({success: function () {console.log('短--震动成功!');}})
// 设置底部tabbar角标 以提示
uni.setTabBarBadge({index: 1, text: 1})
}
// 本地存储记录
uni.setStorageSync('newFrienCount', _c)
} else if(chatType.includes(data.message.objectName)){
// 更新会话列表
this.getConversation()
let currenChat = this.$store.state.currenChat
// 当前聊天页面者 不是此新消息发送者,则震动
if (currenChat !== message.data.message.senderUserId) {
uni.vibrateLong({success: function () {console.log('震动成功');}})
uni.setStorageSync('messageUnreadCount', 1)
// 设置底部tabbar角标 以提示
uni.setTabBarBadge({index: 0, text: 1})
} else { // 当前聊天页面者 是此新消息发送者,同步信息到聊天页面
let _message = {
objectName : data.message.objectName,
content : data.message.content,
sentTime : data.message.sentTime,
messageId : data.message.messageId,
who : 'other',
avatar : ''
}
this.$store.commit("setNewMessage", _message)
}
}
})
},
zhanghaoran
2021.12.23