本文是基于web平台对接腾讯IM的一些体会和总结,对于没有对接IM经验或者是刚接触IM项目的小伙伴来说,看到这么多可选的平台,这么丰富的接口和看似如此庞大的项目,你的心里可能会发怵,但是,当你看到这篇文章的时候,你应该不会慌了,因为这里整理了web端跑通整个demo对接的基本流程很一些问题,话不多说,继续往下看。
腾讯IM提供在线demo和本地demo,在线demo可以查看官方案例的最完整的功能,本地demo是供本地开发调试时使用,以web平台为例,去SDK下载区下载对应平台的demo,如果是Web(通)平台找到这个H5/dist/debug/GenerateTestUserSig.js
目录,把文件中的SDKAPPID
和SECRETKEY
项换成自己的,在项目目录下安装依赖npm install
,启动本地项目npm start
,注意,官方不推荐直接访问http://localhost:8080
,而是在项目的dist
目录下直接打开index.html
,这时候我们可以在user0-user29中随便登陆一个了,重新打开一次,再登录一个账户,两个账户就可以即时通讯,至此,官方demo在本地跑起来了。那么,我们如何集成SDK到我们自己的web项目呢?
目前,很多前端web项目都采用MV*框架,比如官方的demo就采用Vue+ElementUI技术,因此,先安装SDK依赖
// IM Web SDK
npm install tim-js-sdk --save
// 发送图片、文件等消息需要的 COS SDK
npm install cos-js-sdk-v5 --save
为了实现模块化,我们新建tim.js
文件作为tim模块独立出来,引入对应的包,并导出tim
import TIM from 'tim-js-sdk';
import COS from "cos-js-sdk-v5";
let options = {
SDKAppID: 0 // 接入时需要将0替换为您的即时通信 IM 应用的 SDKAppID
};
// 创建 SDK 实例,`TIM.create()`方法对于同一个 `SDKAppID` 只会返回同一份实例
let tim = TIM.create(options); // SDK 实例通常用 tim 表示
// 设置 SDK 日志输出级别,详细分级请参见 setLogLevel 接口的说明
tim.setLogLevel(0); // 普通级别,日志量较多,接入时建议使用
// tim.setLogLevel(1); // release 级别,SDK 输出关键信息,生产环境时建议使用
// 注册 COS SDK 插件
tim.registerPlugin({'cos-js-sdk': COS});
export default tim
为了能在本地登录账户,需要利用客户端计算UserSig生成签名,再配上userID,就可以登录到IM系统,这里需要借助官方demo的两个文件GenerateTestUserSig.js
和lib-generate-test-usersig.min.js
来生成签名,由于模块化开发,需要对GenerateTestUserSig.js
进行修改
//首先导入lib-generate-test-usersig.min.js
import LibGenerateTestUserSig from './lib-generate-test-usersig.min'
//......
//修改lib的调用方法,官方案例是注入在window里面new window.LibGenerateTestUserSig(...)
var generator = new LibGenerateTestUserSig(SDKAPPID, SECRETKEY, EXPIRETIME);
//......
//导出
export {
genTestUserSig
}
为了方便后续使用,我们可以在window和Vue中全局注入tim
//main.js
import tim from './tim'
import TIM from 'tim-js-sdk'
window.tim = tim
window.TIM = TIM
Vue.prototype.tim = tim
Vue.prototype.TIM = TIM
接下来需要添加事件监听,查看所有的事件绑定,执行登录操作,顺便提下退出操作
//登录
tim.login({userID: 'your userID', userSig: 'your userSig'}).then((imRespone)=>{
console.log(imResponse.data); // 登录成功
}).catch((imError)=>{
console.warn('login error:', imError); // 登录失败的相关信息
})
//退出
tim.logout().then((imResponse)=>{
console.log(imResponse.data); // 退出成功
}).catch((imError)=>{
console.warn('logout error:', imError);
});
登录之后我们可以获取会话列表,获取每个会话下面的消息列表,其中涉及到的数据状态和细节操作是比较复杂的,先看看文本中的表情处理,这里简要说下思路。
//emojiMap.js
export const emojiUrl = 'https://imgcache.qq.com/open/qcloud/tim/assets/emoji/'
export const emojiMap = {
'[调皮]': 'emoji_113@2x.png',
'[龇牙]': 'emoji_141@2x.png'
}
export const emojiName = [
'[龇牙]',
'[调皮]'
]
据官方demo来看,所有的表情都是图片的映射,比如[微笑](https://imgcache.qq.com/open/qcloud/tim/assets/emoji/emoji_49@2x.png
,只需要更改结尾,拼凑图片链接即可,显示
供选择的时候就通过img标签遍历显示,发送消息的时候,我们选择了某个表情,只需要把对于的emojiName比如'[微笑]'追加到文本消息里就行。接下来是接收带有表情的文本消息解析的问题,官方给出了解析代码,只需要导入emoji映射,再把这个解析函数导出就可以了。
然后就是发送文件信息,获取对应的文件DOM,调用发送文件消息的接口即可,显示的时候只需要显示文件名和文件大小即可,然后点击下载,可参考下面三个函数
size() {
const size = this.payload.fileSize
if (size > 1024) {
if (size / 1024 > 1024) {
return `${this.toFixed(size / 1024 / 1024)} Mb`
}
return `${this.toFixed(size / 1024)} Kb`
}
return `${this.toFixed(size)}B`
},
toFixed(number, precision = 2) {
return number.toFixed(precision)
}
downloadFile() {
// 浏览器支持fetch则用blob下载,避免浏览器点击a标签,跳转到新页面预览的行为
if (window.fetch) {
fetch(this.fileUrl)
.then(res => res.blob())
.then(blob => {
let a = document.createElement('a')
let url = window.URL.createObjectURL(blob)
a.href = url
a.download = this.fileName
a.click()
})
} else {
let a = document.createElement('a')
a.href = this.fileUrl
a.target = '_blank'
a.download = this.filename
a.click()
}
}
图片消息和文件消息大同小异,需要注意的是这里图片消息是可以点击放大缩小旋转查看的,下面贴上官方demo中写的图片操作,经供参考,icon是Iview的
<template>
<div class="image-previewer-wrapper" v-show="showPreviewer" @mousewheel="handleMouseWheel">
<div class="image-wrapper">
<img
class="image-preview"
:style="{transform: `scale(${zoom}) rotate(${rotate}deg)`}"
:src="previewUrl"
@click="close"
/>
</div>
<Icon type="md-close" class="close-button" @click="close" />
<Icon type="md-arrow-back" class="prev-button" @click="goPrev" />
<Icon type="md-arrow-forward" class="next-button" @click="goNext"></Icon>
<div class="actions-bar">
<Icon type="ios-remove-circle-outline" @click="zoomOut"></Icon>
<Icon type="ios-add-circle-outline" @click="zoomIn"></Icon>
<Icon type="md-undo" @click="rotateLeft"></Icon>
<Icon type="md-redo" @click="rotateRight"></Icon>
<span class="image-counter">{{index+1}} / {{imgUrlList.length}}</span>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'ImagePreviewer',
data() {
return {
url: '',
index: 0,
visible: false,
zoom: 1,
rotate: 0,
minZoom: 0.1
}
},
computed: {
...mapGetters(['imgUrlList']),
showPreviewer() {
return this.url.length > 0 && this.visible
},
imageStyle() {
return {
transform: `scale(${this.zoom});`
}
},
previewUrl() {
return this.formatUrl(this.imgUrlList[this.index])
}
},
mounted() {
this.$bus.$on('image-preview', this.handlePreview)
},
methods: {
handlePreview({ url }) {
this.url = url
this.index = this.imgUrlList.findIndex(item => item === url)
this.visible = true
},
handleMouseWheel(event) {
if (event.wheelDelta > 0) {
this.zoomIn()
} else {
this.zoomOut()
}
},
zoomIn() {
this.zoom += 0.1
},
zoomOut() {
this.zoom =
this.zoom - 0.1 > this.minZoom ? this.zoom - 0.1 : this.minZoom
},
close() {
Object.assign(this, { zoom: 1 })
this.visible = false
},
rotateLeft() {
this.rotate -= 90
},
rotateRight() {
this.rotate += 90
},
goNext() {
this.index = (this.index + 1) % this.imgUrlList.length
},
goPrev() {
this.index =
this.index - 1 >= 0 ? this.index - 1 : this.imgUrlList.length - 1
},
formatUrl(url) {
if (!url) {
return ''
}
return url.slice(0, 2) === '//' ? `https:${url}` : url
}
}
}
</script>
<style scoped>
.image-previewer-wrapper {
position: fixed;
width: 100%;
left: 0;
top: 0;
height: 100%;
display: flex;
justify-content: center;
align-items: flex-start;
background: rgba(14, 12, 12, 0.7);
z-index: 2000;
cursor: zoom-out;
}
.close-button {
cursor: pointer;
font-size: 28px;
color: #000;
position: fixed;
top: 50px;
right: 50px;
background: rgba(255, 255, 255, 0.8);
border-radius: 50%;
padding: 6px;
}
.image-wrapper {
position: relative;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.image-preview {
transition: transform 0.1s ease 0s;
}
.actions-bar {
display: flex;
justify-content: space-around;
align-items: center;
position: fixed;
bottom: 50px;
left: 50%;
margin-left: -100px;
padding: 12px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.8);
}
.actions-bar i {
font-size: 24px;
cursor: pointer;
margin: 0 6px;
}
.prev-button,
.next-button {
position: fixed;
cursor: pointer;
background: rgba(255, 255, 255, 0.8);
border-radius: 50%;
font-size: 24px;
padding: 12px;
}
.prev-button {
left: 0;
top: 50%;
}
.next-button {
right: 0;
top: 50%;
}
.image-counter {
background: rgba(20, 18, 20, 0.53);
padding: 3px;
border-radius: 3px;
color: #fff;
}
</style>