本文是基于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> 复制代码