最近做了一个远程视音频的项目,用到的技术栈是vue+iview,就其中的网页聊天,视音频、日历、与后端配合过程中遇到的问题以及打包过程中遇到的问题做个分享和总结。
网页聊天
效果图:
html,通过senderId来区分发送者还接受者,
<div class="chat-wrap">
<div class="abs chat-main">
<div class="abs chat-header">
<div class="ml10 mt10">
<h2>{{chat.consultroName}}</h2>
<div class="grey">{{chat.consulImTime}}</div>
</div>
</div>
<div class="abs ovauto chat-body" id='chat-body'>
<ul class="chat-list">
<template v-for='item in chat.chatList'>
<li class="tc grey" v-if=" item.hasOwnProperty('tip') " v-html='item.message'></li></li>
<li class="chat-other" v-if='item.senderId == chat.consultroId'>
<div class="photo-arrow fl">
<span class="chat-photo fl">
<img :src="chat.consultroAvr" v-if=" chat.consultroAvr != '' ">
<Icon type="person" size='40' color='#f0f0f0' v-else></Icon>
</span>
<span class="chat-arrow fl">
<em class="arrow-l"></em>
</span>
</div>
<div class="chat-content fl">
{{item.message}}
<span>{{item.time.split(' ')[1]}}</span>
</div>
</li>
<li class="chat-me" v-if='item.senderId == chat.visitorId'>
<div class="photo-arrow fr">
<span class="chat-photo fr">
<img :src="chat.visitorAvr" v-if=" chat.visitorAvr != '' ">
<Icon type="person" size='40' color='#f0f0f0' v-else></Icon>
</span>
<span class="chat-arrow fr">
<em class="arrow-r"></em>
</span>
</div>
<div class="chat-content fr">
{{item.message}}
<span>{{item.time.split(' ')[1]}}</span>
</div>
<div class="message-error fr" title="发送失败" v-if="item.hasOwnProperty('isSuccess')"><Icon type="alert-circled" color='red'></Icon></div>
</li>
</template>
</ul>
</div>
<div class="abs chat-footer">
<Input type="text" v-model='chat.text' placeholder="输入文字信息" :maxlength='200' @on-enter="sendMsg" autofocus>
<Button :loading='loading2' slot="append" icon="paper-airplane" title='发送' @click='sendMsg'></Button>
</Input>
</div>
</div>
</div>
采用HTML5的WebSocket协议,首先定义一个url地址:
websockUrl:'ws://192.168.1.119:11000/aa/roomChat',//webSocket聊天地址
初始化方法
initWebSocket(){//网页聊天
let that=this;
//判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
try {
that.chat.websockObj = new WebSocket(that.chat.websockUrl);
} catch(e) {
console.log(e);
}
}else {
that.$Message.warning('您当前浏览器不支持在线聊天!');
return false;
}
//连接成功建立的回调方法
that.chat.websockObj.onopen = function () {
that.chat.chatList.push({tip:true,message:'正在连接中...',sendId:''})
that.chat.websockObj.send('testMsg201811121726');
};
//连接发生错误的回调方法
that.chat.websockObj.onerror = function () {
that.chat.chatList.push({tip:true,message:'WebSocket连接发生错误',sendId:''});
};
//接收到消息的回调方法
that.chat.websockObj.onmessage = function (event) {
that.receiveMsg(event.data);
// that.heartCheck();
}
//连接发生错误重连
that.chat.websockObj.onclose = function () {
// that.reconnect();
};
},
发送消息
sendMsg(){//发送消息
let that=this;
if(that.chat.text.length == 0){
that.$Message.destroy();
that.$Message.warning('您还没有输入内容!');
return false;
}
if((new Date().getTime() - that.chat.sendTimeInterval)/1000 < 2){
that.$Message.destroy();
that.$Message.warning('信息发送太频繁了,请休息一会再发!');
return false;
}
that.chat.sendTimeInterval=new Date().getTime();
that.loading2=true;
if(that.chat.websockObj.readyState == 1){
that.chat.websockObj.send(that.chat.text);
that.chat.text='';
setTimeout(function(){
that.loading2=false;
},50);
}else{
that.$Message.warning('连接还未建立!');
}
}
接受消息,接受消息时可以做一些判断,比如是否连接成功,地方是否已经下线等,这些判断标识可以自己发也可以由后台发送,
receiveMsg(str){//接受消息
let obj=document.querySelector('#chat-body');
let h=obj.clientHeight;
let conObj=JSON.parse(str);
// 检测连接是否成功
if(conObj.message.indexOf('testMsg201811121726') != -1){
if(conObj.message.indexOf('18ece5e7e003423da379aba5da84cdbc') == -1){
let t=new Date();
let time=initDate(t.getHours())+':'+initDate(t.getMinutes());
this.chat.chatList.push({tip:true,message:time+'<br>连接已经建立,可以开始聊天了',sendId:''});
this.chat.chatList.splice(0,1);
}
return false;
}
// 检测对方是否下线
if(conObj.message.indexOf('offLineMsg201811121808') != -1){
this.chat.chatList.push({tip:true,message:'对方已下线',sendId:''});
return false;
}
// 检测是否发送失败
if(conObj.message.indexOf('18ece5e7e003423da379aba5da84cdbc') != -1){
conObj.message=conObj.message.replace('18ece5e7e003423da379aba5da84cdbc', '');
conObj.isSuccess=false;
}
// 保持最新内容在视野内
setTimeout(function(){
if(obj.scrollHeight > h){
obj.scrollTop = obj.scrollHeight;
}
}, 50);
},
关闭连接
destroyed: function() {
//页面销毁时关闭长连接
this.chat.websockObj.close();
}
以上并不是完整的聊天逻辑代码,只记录顺序流程,还需要后端小伙伴的配合。
腾讯视音频
视音频功能采用的是腾讯的实时音视频功能,使用流程为:
注册账号——购买实时音视频服务——下载sdk——生成测试账号——前后端联调。
需要注意的是测试时必须是https协议或者localhos下,我的这个项目需要三路视频,因此创建了三个房间,三个账号;下面是详细代码:
html
<video id="maxVideo" autoplay playsinline></video><!-- A正面 -->
<video id="localVideo" width="100%" muted autoplay playsinline></video><!-- B正面 -->
<video id="remoteVideo2" width="100%" autoplay playsinline></video><!-- A侧面 -->
<video id="remoteVideo3" width="100%" autoplay playsinline></video><!-- 抓屏 -->
初始化:
initRTC(){
var that=this;
that.user1.objectRTC = new WebRTCAPI({
"userId": that.user1.userId,
"userSig": that.user1.userSig,
"sdkAppId":that.sdkappid
});
that.user1.objectRTC.getLocalStream({
video:true,
audio:true,
},function( info ){
var stream = info.stream;
that.user1.objectRTC.enterRoom({
roomid : that.user1.roomId,
privateMapKey:that.user1.privateMapKey
},function(){
// 枚举音频设备
that.user1.objectRTC.getAudioDevices( function(devices){
if(devices.length > 0){
that.user1.objectRTC.chooseAudioDevice( devices[0] );
}else{
that.$Message.warning('没有检测到音设备!');
}
});
// 枚举视频设备
that.user1.objectRTC.getVideoDevices( function(devices){
if(devices.length > 0){
that.user1.objectRTC.chooseVideoDevice( devices[0] );
}else{
that.$Message.warning('没有检测到视频设备!');
}
});
//进房成功,音视频推流
that.user1.objectRTC.startRTC({
role : "user", //画面设定的配置集名 (见控制台 - 画面设定 )
stream: stream
});
},function(){
});
},function ( error ){
console.error( error )
});
// 本地
that.user1.objectRTC.on("onLocalStreamAdd", function(data){
if( data && data.stream){
document.querySelector("#localVideo").srcObject = data.stream;
}
});
//远端流 新增/更新
that.user1.objectRTC.on("onRemoteStreamUpdate", function(data){
if( data && data.stream){
document.querySelector("#maxVideo").srcObject = data.stream;
}
});
},
日历
类似于tower的日历功能,只不过我这只需要选择时间范围
<div class="cal-wrap">
<div class="cal-top">
<Affix :offset-top="80">
<div class="cal-YM">
<Spin v-if='calLoading' fix>
<Icon type="load-c" size=18 class="demo-spin-icon-load"></Icon>
</Spin>
<div class="YM-text ovh">
<div title='上一月' class="cal-left hand fl" @click="getPrevMonth"><Icon type="ios-arrow-left"></Icon></div>
{{calendar.year}}年-{{calendar.month}}月<span @click="backToday" class='hand' title="返回今天">今</span>
<div title='下一月' class="cal-right hand fr" @click="getNextMonth"><Icon type="ios-arrow-right"></Icon></div>
</div>
</div>
<div class="cal-week-wrap ovh">
<div class="cal-week red">日</div>
<div class="cal-week" v-for="(item,index) in calendar.weeks" :key="index">{{item}}</div>
<div class="cal-week red">六</div>
</div>
</Affix>
</div>
<table class="cal-table mb20">
<tr v-for="(item,itemIndex) in calendar.dayList" :key='itemIndex'>
<td v-for="(key,keyIndex) in item" :key='key.date' :class="{'bg-grey':key.disable}" @click='dayModal(key.date,itemIndex,keyIndex,key.disable)'>
<div class="cal-item" :class="{'cal-active':calendar.isDay == key.date}">
<span>{{key.day}}</span>
<div class="cal-time-list" v-if='key.timeList.length > 0'>
<p v-for='(data,dataIndex) in key.timeList' :key="'time'+dataIndex">
<span v-if='data.usedFlag == 0' class='red'>
<Icon type="ios-checkmark-empty fr" size='20'></Icon>
{{data.time}}
</span>
<span v-else>
{{data.time}}
</span>
</p>
</div>
</div>
</td>
</tr>
</table>
<div class="greey f12">*从本日起往后30天内可自由安排时间</div>
</div>
//data
calendar:{//日历
dayList:[],//二维数组,循环行,循环列
prev:[],
current:[],
next:[],
year:'',
month:'',
weeks:['一','二','三','四','五'],
isDay:''//判断是否是'今天'
},
methods:{
initDate:(val){
if(val < 10){
return '0'+val;
}else{
return val;
}
},
getLastDate(year,month){
return new Date(year,month,0);
},
getmonthDays(){//获取上月 当前月和下月天数
let that=this;
let y=that.calendar.year;
let m=that.calendar.month;
let preYear;//上一年
let preMonth;//上一月
let nextYear;//下一年
let nextMonth;//下一月
that.calendar.current=[];
that.calendar.prev=[];
that.calendar.next=[];
// 当前月天数
for(let i=1; i<=that.getLastDate(y,m).getDate(); i++){
//date用于日期判断,day用于显示,flag用于状态判断
that.calendar.current.push({date:y+'-'+m+'-'+initDate(i),day:i,timeList:[],disable:true});
}
/*上月*/
let d=that.getLastDate(y,m - 1).getDate();//上月一共多少天
preYear= m == 1 ? y-1 : y;//当前月是1月,那么上一月的年份要-1
preMonth= m == 1 ? 12 : initDate(parseInt(m)-1);//当前月是1月,那么上一月是12月
for(let j=(that.getLastDate(y,m - 1).getDay()); j >= 0; j--){
that.calendar.prev.push({date:preYear+'-'+preMonth+'-'+(d-j),day:d-j,timeList:[],disable:true});
}
/*下月*/
nextYear= m == 12 ? y+1 : y;//当前月是12月,那么下一月的年份要+1
nextMonth= m == 12 ? '01' : initDate(parseInt(m)+1);//当前月是12月,那么下一月是1月
for(let k=1; k <= 42- that.calendar.current.length - that.calendar.prev.length; k++){
that.calendar.next.push({date:nextYear+'-'+nextMonth+'-'+initDate(k),day:k,timeList:[],disable:true});
}
that.calendar.dayList=[];
// 数组合并
let tempArr=that.calendar.prev.concat(that.calendar.current,that.calendar.next);
// 数组分组,每7个一组
for(let i = 0;i < tempArr.length; i+=7){
that.calendar.dayList.push(tempArr.slice(i, i+7));
}
that.getTimetable(that.calendar.dayList[0][0].date,that.calendar.dayList[5][that.calendar.dayList[5].length - 1].date);
},
getPrevMonth(){//上一月
if(this.calendar.month != 1){
this.calendar.month = initDate(--this.calendar.month);
}else{
this.calendar.month = 12;
this.calendar.year = --this.calendar.year;
}
this.getmonthDays();
this.currentDay();
},
getNextMonth(){//下一月
if(this.calendar.month < 12){
this.calendar.month = initDate(++this.calendar.month);
}else{
this.calendar.month = '01';
this.calendar.year = ++this.calendar.year;
}
this.getmonthDays();
this.currentDay();
},
currentDay(){//获取今天,高亮显示今天
let that=this;
$.post(psyBase.path+'/qd/welcome/getCurrTime',null, function(seconds, textStatus, xhr) {
let date=new Date(parseInt(seconds));
let y=that.calendar.year;
let m =that.calendar.month;
if(y === date.getFullYear() && m == date.getMonth()+1){//如果是当年当月
that.calendar.isDay = y+'-'+initDate(m)+'-'+initDate(date.getDate());//获取到今天的号数
}else{
that.calendar.isDay=-1;
}
},'text');
},
backToday(){//返回今天
let that=this;
$.post(psyBase.path+'/qd/welcome/getCurrTime',null, function(seconds, textStatus, xhr) {
let d=new Date(parseInt(seconds));
that.calendar.year=d.getFullYear();
that.calendar.month=initDate(d.getMonth()+1);
that.currentDay();
that.getmonthDays();
},'text');
},
}
与后端配合过程中遇到的问题
1、腾讯视音频服务前后台联调复杂
原因:视音频需要localhsot或者https协议才能访问
(1)采用nginx代理,使用https在线联调,发现前端没获取静态资源,使用脚手架启动的服务静态资源都在缓存中,没有放在dist中,打包后才有,没法测试;
(2)本地测试,使用webpack-dev-server代理设置为localhost,但是后台接口就不通了,需要调用后台接口动态获取房间账户和密钥,不过可以先写死测试,勉强可以;
(3)测试接口的话只能本地测试完打包发给开发,让开发部署到自己服务下进行测试,效率低,麻烦
如果webpack能够像eclipse自带的服务那样,点击保存就把改动发布到tomcat下就好了,随时编译
2、接口调试问题
后台只能盲写接口,没法测试,我自己写了个简陋的接口测试页面给开发用
<template>
<div>
<Form ref="suggessForm" >
<FormItem>
<Input type="text" v-model="psyUrl.url" placeholder="/qd/user/getUserPic"></Input>
</FormItem>
<FormItem >
<Input v-model="psyUrl.content" type="textarea" :rows="4" class='mb10' :maxlength='100' placeholder="name=aa&age=10&sex=男"></Input>
</FormItem>
<FormItem >
<Button type="primary" :loading="loading" @click="interface">
<span v-if="!loading">测试</span>
<span v-else>Loading...</span>
</Button>
</FormItem>
</Form>
</div>
</template>
<script>
export default {
mounted(){
},
data() {
return {
loading:false,
psyUrl:{
url:'/qd/user/getUserPic',
content:'name=aa&age=10&sex=男'
}
};
},
methods: {
interface(){
let that=this;
let params={};
let arr = that.psyUrl.content.split('&');
that.loading=true;
$.each(arr, function(index, val) {
params[val.split('=')[0]]=val.split('=')[1];
});
console.log(params)
$.post(psyBase.path+that.psyUrl.url,params, function(data, textStatus, xhr) {
that.loading=false;
console.log(data)
},'text');
}
}
};
</script>
应该有在线mock数据的吧?
打包过程中遇到的问题
静态资源比较多,有图片、字体文件、js文件、css文件,我想把静态资源放在单独一个目录文件夹内,打包后期望是这样:
resource文件夹内文件
参考网上的设置不论怎么改都不行,每次都要手动修改main.css文件的路径
把dist/resource
改成./resource
下面是配置:
entry: {
main: './src/main',
vendors: './src/vendors'
},
devServer: {
host:'192.168.1.230',
// host:'localhost',
disableHostCheck: true,
proxy:[
{
context: ['/qd', '/logout','/sys','/roomChat'],
target: 'http://192.168.1.119:11000/psycholConsult/',
secure: false,
changeOrigin:true
}
]
},
output: {
path: path.join(__dirname, './dist'),
publicPath:'./resource/',
},
module: {
rules: [{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loaders: {
css: ExtractTextPlugin.extract({
use: ['css-loader', 'autoprefixer-loader'],
fallback: 'vue-style-loader'
})
}
}
},
{
test: /iview\/.*?js$/,
loader: 'babel-loader'
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.(less|css)$/,
use: ExtractTextPlugin.extract({
use: ['css-loader?minimize', 'autoprefixer-loader','less-loader'],
fallback: 'style-loader'
})
},
// 小于10K的图片将直接以base64的形式内联在代码中,可以减少一次http请求。
// 大于10k的呢?则直接file-loader打包,
{
test: /\.(gif|jpg|png|woff|svg|eot|ttf)\??.*$/,
loader: 'url-loader',
options:{
limit: 10240,//图片大小
name: 'resource/[name].[hash].[ext]'//图片名称规则
}
},
{
test: /\.(html|tpl)$/,
loader: 'html-loader'
}
]
},
webpack-dev-server代理
编辑webpack.base.config.js文件
devServer: {
host:'192.168.1.230',
// host:'localhost',
disableHostCheck: true,
proxy:[
{
context: ['/qd', '/logout','/sys','/roomChat'],
target: 'http://192.168.1.119:11000/psy/',
secure: false,
changeOrigin:true
}
]
},
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。