首先,说一下使用情况。因为需求,需要做一个聊天室页面,因为不是专门的点对点聊天,是类似直播,但是是文字直播平台的那种。现在一般的课堂,可能会需要这种。分为2个端,一个是讲师端,一个是用户端。讲师端可能是单独的APP。用户端的页面可能是内嵌到专门的APP,或者是微信公众平台。我这次做的就是一个用户端。讲师端由原生来写,因为需要H5页面能兼容微信还有在手机端都能用。

  然后,说明一下,用了vue的UI框架是vux(下次可能就不会用这个了,其中Scroller竟然不维护了!),用的webpack打包。

  一切就绪,画页面,就是类似直播平台。用vux快速画好。




先确认需求,1.需要老师能撤回,然后客户端同时将数据从数组删除。2.能播放语音。3.讲师区是下拉加载更多,观众区是上拉加载更多。4.因为ios的输入法有联想词一栏,所以为了能正常输入文字,最后决定使用一个弹框,弹框里面有textarea进行输入文字。5.讲师发的图片。需要客户端能点开,变成大图。(简易版本,暂时无双指放大和双击放大)。6.讲师可以禁言。而客户端需要相应的改样式,禁止输入。差不多就是这些需求。

开始准备:

1.去融云官网注册账号。其实对我们前端来说,只是需要一个appkey。



2.拿到appkey,就相当于拿到大门钥匙。这个时候可以开始写js文件进行连接了。po上一张文件的内容。



主要是在views里面的index.vue写的文件。因为页面只有一个,所以我的路由写在了main.js(别说我没写路由,哈哈哈哈)。

好了,接下来我们要先进行连接。那么我写在utils.js就是从官网上扣下来的js文件,顺便封装一下,为了能在index.vue中取值,这边添加了一个回调函数。
export default { init(params, callbacks, modules) { var appKey =
params.appKey; var token = params.token; // var navi = params.navi || "";
modules = modules || {}; var RongIMLib = modules.RongIMLib || window.RongIMLib;
var RongIMClient = RongIMLib.RongIMClient; // var protobuf = modules.protobuf
|| null; var config = {}; var dataProvider = null; var imClient =
params.imClient; if (imClient) { dataProvider = new
RongIMLib.VCDataProvider(imClient); } RongIMLib.RongIMClient.init(appKey,
dataProvider, config); //语音播放初始化 RongIMLib.RongIMVoice.init(); var instance =
RongIMClient.getInstance(); // 连接状态监听器
RongIMClient.setConnectionStatusListener({ onChanged: function(status) { //
console.log(status); switch (status) { case
RongIMLib.ConnectionStatus["CONNECTED"]: case 0: console.log("连接成功");
callbacks.getInstance && callbacks.getInstance(instance); break; case
RongIMLib.ConnectionStatus["CONNECTING"]: case 1: console.log("连接中"); break;
case RongIMLib.ConnectionStatus["DISCONNECTED"]: case 2:
console.log("当前用户主动断开链接"); break; case
RongIMLib.ConnectionStatus["NETWORK_UNAVAILABLE"]: case 3:
console.log("网络不可用"); break; case
RongIMLib.ConnectionStatus["CONNECTION_CLOSED"]: case 4:
console.log("未知原因,连接关闭"); break; case
RongIMLib.ConnectionStatus["KICKED_OFFLINE_BY_OTHER_CLIENT"]: case 6:
alert("用户账户在其他设备登录,本机会被踢掉线"); break; case
RongIMLib.ConnectionStatus["DOMAIN_INCORRECT"]: case 12:
console.log("当前运行域名错误,请检查安全域名配置"); break; } } }); //开始链接 RongIMClient.connect(
token, { onSuccess: function(userId) { callbacks.getCurrentUser &&
callbacks.getCurrentUser({ userId: userId }); console.log("链接成功,用户id:" +
userId); }, onTokenIncorrect: function() { console.log("token无效"); }, onError:
function(errorCode) { console.log(errorCode); } }, params.userId ); /*
文档:http://www.rongcloud.cn/docs/web.html#3、设置消息监听器 注意事项:
1:为了看到接收效果,需要另外一个用户向本用户发消息 2:判断会话唯一性 :conversationType + targetId
3:显示消息在页面前,需要判断是否属于当前会话,避免消息错乱。
4:消息体属性说明可参考:http://rongcloud.cn/docs/api/js/index.html */
RongIMClient.setOnReceiveMessageListener({ // 接收到的消息 onReceived:
function(message) { // 判断消息类型 // console.log("新消息: " + message.targetId); //
console.log(message); // 判断消息类型 switch (message.messageType) { case
RongIMClient.MessageType.TextMessage: // message.content.content => 消息内容 break;
case RongIMClient.MessageType.VoiceMessage: // message.content.content 格式为 AMR
格式的 base64 码 break; case RongIMClient.MessageType.ImageMessage: //
message.content.content => 图片缩略图 base64。 // message.content.imageUri => 原图 URL。
break; case RongIMClient.MessageType.DiscussionNotificationMessage: //
message.content.extension => 讨论组中的人员。 break; case
RongIMClient.MessageType.RichContentMessage: // message.content.content =>
文本消息内容。 // message.content.imageUri => 图片 base64。 // message.content.url => 原图
URL。 break; } callbacks.receiveNewMessage &&
callbacks.receiveNewMessage(message); } }); } };
这样就算把发动机预热好了,这个时候,去我们的index.vue开车了。


在inde.vue里面首先初始化。写个startInit函数,顺手挂载到mounted上。因为初始化,融云是需要我们传token的,这个token是融云的token,那理所当然是需要后台给我们传。我们公司是这样写的,在进入页面的时候,先发2个请求,一个是用户个人信息请求,一个是聊天室详情请求。用户个人信息分为:这个人登录了,我把验证消息给后台,后台给我一个token,我去请求融云。第二种是这个人没有登录,那我这个时候的验证消息是空的,需要后台去判断,然后也给我传一个token,顺便需要传一些其他判断的依据,告诉我这个人没有登录,那我就要让他没办法发言。(扯远了_(:з」∠)_)。拿到token,我就开始初始化。

同时我们要加入聊天室:
startInit(){ let token = getCookie('tk1') var params = { appKey: this.appKey,
token: token, }; const that = this; var userId = ""; var callbacks = {
getInstance: function (instance) { //注册 PersonMessage var propertys = ["name",
"age", "gender"]; // 消息类中的属性名。 that.registerMessage("PersonMessage",
propertys); //注册 ProductMessage var propertys = ["price", "title", "desc",
"images"]; // 消息类中的属性名。 that.registerMessage("ProductMessage", propertys); },
getCurrentUser(userInfo) { const userId = getCookie('uId') document.titie =
"链接成功;userid=" + userInfo.userId; //加入聊天室 that.joinChatRoom(); }, // 收到融云的最新消息
receiveNewMessage(message) { // 禁言 if (message.objectName == 'ChatRoom:state')
{ if (message.content.message.content.roomState == "BANNED_TO_POST") {
$('.weui-input').attr('placeholder', '全员禁言中,不能发言') } else { let rightControl =
getCookie('rightControl') if (rightControl == '1020') {
$('.weui-input').attr('placeholder', '登录之后才能向讲师提问') } else {
$('.weui-input').attr('placeholder', '输入您的问题') } } } // 自定义消息,判断是否为主持人 let tag
if (message.content.extra) { tag = JSON.parse(message.content.extra).tag } let
messageBig1 = {}; // 判断消息类型 if (!tag) { //没有值是观众 if (message.objectName ==
'RC:TxtMsg') { // 文字信息 messageBig1.sendTimeStamp = message.sentTime;
messageBig1.sendUserPortrait = JSON.parse(message.content.extra).portrait;
messageBig1.sendUserName = JSON.parse(message.content.extra).userName;
messageBig1.content = message.content.content; messageBig1.messageId =
JSON.parse(message.content.extra).messageId; that.listBox.unshift(messageBig1)
} that.totalCount = parseInt(getCookie('totalCount')) + 1; if (that.totalCount
>= 999) { that.totalCount = "999+" } let expireDays = 1000 * 60 * 60; //
存好讨论区的历史信息数 setCookie('totalCount', that.totalCount, expireDays) //
发言之后会滚到上方(vux方法) that.$nextTick(() => { let initTop; if (
document.getElementById("conversationListH").scrollHeight > 300 ) { initTop =
-(document.getElementById("conversationListH").scrollHeight - 300); } else {
initTop = 0; } that.$refs.scrollerBottom.reset({ bottom: initTop }); }); if
(!that.chatDiscussion) { that.hasMsg = true } } else { //有值是主持人 let messageBig
= { messageType: { code: null } }; messageBig.duration =
message.content.duration; messageBig.sendTimeStamp = message.sentTime;
messageBig.sendUserPortrait = JSON.parse(message.content.extra).portrait;
messageBig.sendUserName = JSON.parse(message.content.extra).userName;
messageBig.messageId = JSON.parse(message.content.extra).messageId;
messageBig.content = message.content.content; if (message.objectName ==
'RC:TxtMsg') { messageBig.messageType.code = 1010;
that.listBox1.push(messageBig) } else if (message.objectName == 'RC:VcMsg') {
messageBig.messageType.code = 1020; messageBig.hasRead = false;
that.listBox1.push(messageBig) } else if (message.objectName == 'RC:ImgMsg') {
messageBig.messageType.code = 1030; messageBig.attachmentUrl =
message.content.imageUri; that.listBox1.push(messageBig) } that.withdraw(); }
// 消息撤回 if (message.objectName == 'message:recall') { that.hasMsg = false let
messageIds = message.content.message.content.messageIds; messageIds.map((item)
=> { that.listBox1 = that.listBox1.filter(e => e.messageId !== item)
that.withdraw(); }) } }, }; utils.init(params, callbacks) },

这个里面要注意一点,就是获取token会是异步,所以,我最后决定,将这个函数,丢在获取个人信息函数里面,这样就不会出现异步了。(大家可以试着写中间件,或者是用其他处理异步方法。)。因为vue是双向绑定,所以只需要我改变数组的值,就会自动出现撤回以及新增数据丢到页面。对了,这个里面有个很重要的方法,我还没说。从案列上扒下来的,老实说,我并不知道为什么要写这个函数。这个函数在初始化里面调用了。
registerMessage(type, propertys) { var messageName = type; // 消息名称。 var
objectName = "s:" + type; // 消息内置名称,请按照此格式命名 *:* 。 var mesasgeTag = new
RongIMLib.MessageTag(true, true); //true true 保存且计数,false false 不保存不计数。
RongIMClient.registerMessageType( messageName, objectName, mesasgeTag,
propertys ); },
上面差不多就是发言和收到消息了。


最后说一下语音。因为融云语音是用base64位,正常的办法没有办法解读。这个时候要用融云自己的文件。但是融云目前官网上的语音版本,在安卓微信端不能播放语音,但是新版本的还没有更新到官网,我先把这个文件复制上来。
var RongIMLib; (function(RongIMLib) { var RongIMVoice = (function() { function
RongIMVoice() {} RongIMVoice.init = function() { if (this.notSupportH5) { var
div = document.createElement("div"); div.setAttribute("id", "flashContent");
document.body.appendChild(div); var script = document.createElement("script");
script.src = (RongIMLib.RongIMClient && RongIMLib.RongIMClient._memoryStore &&
RongIMLib.RongIMClient._memoryStore.depend &&
RongIMLib.RongIMClient._memoryStore.depend.voiceSwfobjct) ||
"//cdn.ronghub.com/swfobject-2.0.0.min.js"; var header =
document.getElementsByTagName("head")[0]; header.appendChild(script); var
browser = navigator.appName; var b_version = navigator.appVersion; var version
= b_version.split(";"); var trim_Version = version[1].replace(/[ ]/g, "");
script.onload = function() { var swfVersionStr = "11.4.0"; var flashvars = {};
var params = {}; params.quality = "high"; params.bgcolor = "#ffffff";
params.allowscriptaccess = "always"; params.allowScriptAccess = "always";
params.allowfullscreen = "true"; var attributes = {}; attributes.id = "player";
attributes.name = "player"; attributes.align = "middle";
swfobject.embedSWF((RongIMLib.RongIMClient &&
RongIMLib.RongIMClient._memoryStore &&
RongIMLib.RongIMClient._memoryStore.depend &&
RongIMLib.RongIMClient._memoryStore.depend.voicePlaySwf) ||
"//cdn.ronghub.com/player-2.0.2.swf", "flashContent", "1", "1", swfVersionStr,
null, flashvars, params, attributes) var f_version =
swfobject.getFlashPlayerVersion(); if (f_version['major'] <= 0) {
console.error("You haven't installed the flash Player yet."); } } if (!(browser
== "Microsoft Internet Explorer" && trim_Version == "MSIE9.0" || trim_Version
== "MSIE10.0")) { script.onreadystatechange = script.onload; } } this.isInit =
true }; RongIMVoice.play = function(data, duration) { this.checkInit("play");
var me = this; if (me.notSupportH5) { if (me.thisMovie().doAction) {
me.thisMovie().doAction("init", data); } else { setTimeout(function() {
me.play(data, duration); }, 500); } } else { var key = data.substr(-10); if
(this.element[key]) { this.element[key].play(); } me.onCompleted(duration) } };
RongIMVoice.stop = function(base64Data) { this.checkInit("stop"); var me =
this; if (me.notSupportH5) { me.thisMovie().doAction("stop") } else { if
(base64Data) { var key = base64Data.substr(-10); if (me.element[key]) {
me.element[key].pause(); me.element[key].currentTime = 0 } } else { for (var
key_1 in me.element) { me.element[key_1].pause(); me.element[key_1].currentTime
= 0 } } } }; RongIMVoice.preLoaded = function(base64Data, callback) { var str =
base64Data.substr(-10), me = this; if (me.element[str]) { callback &&
callback(); return } // if (/android/i.test(navigator.userAgent) &&
/MicroMessenger/i.test(navigator.userAgent)) { if (false) { var audio = new
Audio(); audio.src = "data:audio/amr;base64," + base64Data; me.element[str] =
audio; callback && callback() } else { if (!me.notSupportH5) { if (str in
me.element) { return } var audio = new Audio(); audio.src = ""; var nopromise =
{ catch: new Function() }; (audio.play() || nopromise).catch(function() {});
//解决浏览器报错 The play() request was interrupted by a new load request var blob =
me.base64ToBlob(base64Data, "audio/amr"); var reader = new FileReader();
reader.onload = function(e) { var data = new Uint8Array(e.target.result); var
samples = AMR.decode(data); var pcm = PCMData.encode({ sampleRate: 8000,
channelCount: 1, bytesPerSample: 2, data: samples }); audio.src =
"data:audio/wav;base64," + btoa(pcm); me.element[str] = audio; callback &&
callback() }; reader.readAsArrayBuffer(blob) } else { callback && callback() }
} }; RongIMVoice.onprogress = function() {}; RongIMVoice.checkInit =
function(position) { if (!this.isInit) { throw new Error("RongIMVoice is not
init,position:" + position) } }; RongIMVoice.thisMovie = function() { return
eval("window['player']") }; RongIMVoice.onCompleted = function(duration) { var
me = this; var count = 0; var timer = setInterval(function() { count++;
me.onprogress(); if (count >= duration) { clearInterval(timer) } }, 1000); if
(me.notSupportH5) { me.thisMovie().doAction("play") } };
RongIMVoice.base64ToBlob = function(base64Data, type) { var mimeType; if (type)
{ mimeType = { type: type } } base64Data = base64Data.replace(/^(.*)[,]/, "");
var sliceSize = 1024; var byteCharacters = atob(base64Data); var bytesLength =
byteCharacters.length; var slicesCount = Math.ceil(bytesLength / sliceSize);
var byteArrays = new Array(slicesCount); for (var sliceIndex = 0; sliceIndex <
slicesCount; ++sliceIndex) { var begin = sliceIndex * sliceSize; var end =
Math.min(begin + sliceSize, bytesLength); var bytes = new Array(end - begin);
for (var offset = begin, i = 0; offset < end; ++i, ++offset) { bytes[i] =
byteCharacters[offset].charCodeAt(0) } byteArrays[sliceIndex] = new
Uint8Array(bytes) } return new Blob(byteArrays, mimeType) };
RongIMVoice.notSupportH5 = /Trident/.test(navigator.userAgent);
RongIMVoice.element = {}; RongIMVoice.isInit = false; return RongIMVoice }());
RongIMLib.RongIMVoice = RongIMVoice; if ("function" === typeof require &&
"object" === typeof module && module && module.id && "object" === typeof
exports && exports) { module.exports = RongIMVoice } else { if ("function" ===
typeof define && define.amd) { define("RongIMVoice", [], function() { return
RongIMVoice }) } } })(RongIMLib || (RongIMLib = {}));
在index.vue中调用play方法。友情提醒,我已经在util.js中初始化语音了,你们注意一下,不然会报错没有初始化哦。
play(voice) { RongIMLib.RongIMVoice.stop(); if (voice) { var duration =
voice.length / 1024; // 音频持续大概时间(秒) RongIMLib.RongIMVoice.preLoaded(voice,
function () { RongIMLib.RongIMVoice.play(voice, duration); }); } else {
console.error('请传入 amr 格式的 base64 音频文件'); } },
好像要记录的就这些了。有不清楚的,可以告诉我,知无不言哦~(仅限聊天室,我实力也有限QAQ)