最近作了一個遠程視音頻的項目,用到的技術棧是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 } ] },