首先,说一下使用情况。因为需求,需要做一个聊天室页面,因为不是专门的点对点聊天,是类似直播,但是是文字直播平台的那种。现在一般的课堂,可能会需要这种。分为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)