最近作了一个远程视音频的项目,用到的技术栈是vue+iview,就其中的网页聊天,视音频、日历、与后端配合过程当中遇到的问题以及打包过程当中遇到的问题作个分享和总结。css
效果图:
html,经过senderId来区分发送者还接受者,html
<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聊天地址
初始化方法vue
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(); }; },
发送消息node
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('链接还未创建!'); } }
接受消息,接受消息时能够作一些判断,好比是否链接成功,地方是否已经下线等,这些判断标识能够本身发也能够由后台发送,webpack
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); },
关闭链接ios
destroyed: function() { //页面销毁时关闭长链接 this.chat.websockObj.close(); }
以上并非完整的聊天逻辑代码,只记录顺序流程,还须要后端小伙伴的配合。nginx
视音频功能采用的是腾讯的实时音视频功能,使用流程为:
注册帐号——购买实时音视频服务——下载sdk——生成测试帐号——先后端联调。
须要注意的是测试时必须是https协议或者localhos下,个人这个项目须要三路视频,所以建立了三个房间,三个帐号;下面是详细代码:web
htmlsegmentfault
<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'); }, }
一、腾讯视音频服务先后台联调复杂
缘由:视音频须要localhsot或者https协议才能访问
(1)采用nginx代理,使用https在线联调,发现前端没获取静态资源,使用脚手架启动的服务静态资源都在缓存中,没有放在dist中,打包后才有,无法测试;
(2)本地测试,使用webpack-dev-server代理设置为localhost,可是后台接口就不通了,须要调用后台接口动态获取房间帐户和密钥,不过能够先写死测试,勉强能够;
(3)测试接口的话只能本地测试完打包发给开发,让开发部署到本身服务下进行测试,效率低,麻烦
若是webpack可以像eclipse自带的服务那样,点击保存就把改动发布到tomcat下就行了,随时编译
二、接口调试问题
后台只能盲写接口,无法测试,我本身写了个简陋的接口测试页面给开发用
<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.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 } ] },