記一次低階並嚴重的開發失誤

守候i發表於2018-03-12

1.前言

前端從事了超過兩年,修復了無數的bug,寫了無數的bug;挖了很多次坑,填了很多次坑;犯了很多次錯,彌補了很多次,學習了很多次。一般而言,對於bug、坑,都是修復完了或者填完了,並且記住為什麼會產生bug,為什麼有坑,為什麼犯錯,怎麼解決的,下次怎麼避免,就行了,就學習到了。而這一次的專案,原本以為開發挺順利的,但是開發完了,才發現自己犯了一個低階而嚴重的錯,這樣的一個失誤,我一直耿耿於懷。

2.起因

在3月9號的這一天,公司有個活動,希望用答題活動推廣自己的小程式。結果因為開發時間太緊,小程式在3月5號才提審。在3月8號早上,小程式還沒有稽核,在不得已的情況下,只能把答題活動以網頁的形式進行,使用vue開發。由於在3月9號要用到這個答題活動,所以3月8號必須要完成開發,測試,驗收。

開發的過程,都挺順利,只是把小程式的一些程式碼,改成vue開發移動端網站的方式,把標籤換了,樣式稍微重寫一下,專案就跑起來了,至於一些互動邏輯,由於不能使用小程式的API,只能另找良方代替,但問題基本不大。

麻煩的一個需求就是:當使用者沒答完題中途退出的時候,要記錄使用者的答題狀態。比如答了哪些題目,哪些題目錯了,哪些題目正確了,拿了多少分等資料。在小程式裡面,很輕鬆可以利用生命週期函式 unload() 進行監聽。當使用者沒答完題退出頁面的時候,把使用者當前的答題資料,傳給後臺,讓後臺進行儲存。在使用者下次進入頁面的時候,我可以根據後臺返回的使用者答題狀態,進行資訊的展示。如果使用者沒答過題目,就重新開始答題,如果使用者上次退出的時候,沒答完題目,就按照退出時的進度,讓使用者重新答題,如果答完了題目,直接顯示答題結果頁面。

這個需求不難實現,小程式有 onload() unload() 兩個生命週期函式,只是在這兩個函式裡面,調兩次介面而已。

但在網頁裡面,監聽使用者進入頁面簡單。但是監聽使用者退出頁面(微信瀏覽器上面的那個‘返回’或者‘關閉’按鈕)卻死活不行。網上最多的解決方案是這個,但是不知道是我使用方式有問題還是人品問題,壓根沒用,無論是微信開發者工具,還是安卓或者蘋果真機。

答案來自知乎:微信自帶瀏覽器環境內左上角返回、關閉按鈕事件監控?

pushHistory(); 
window.addEventListener("popstate", function(e) { 
    alert("我監聽到了瀏覽器的返回按鈕事件啦");//根據自己的需求實現自己的功能 
}, false); 
function pushHistory() { 
    var state = { 
        title: "title", 
        url: "#"
    }; 
    window.history.pushState(state, "title", "#"); 
}
複製程式碼

根據網上的方案,試了幾個(包括vue的生命週期函式),沒一個可行的。最後無奈之下,只能用一個蠢方法,使用者點選每一題選項的時候,就把使用者當前的記錄,通過介面發給後臺,讓後臺記錄。這個就是我該文章說的低階嚴重的失誤,想必大家也知道是怎麼回事了。

3.失誤分析

這次的答題活動,一共有三輪,每輪10道題,現場大概有500人答題。本來使用小程式開發,不管使用者是沒答題就讓使用者可以開始答題,答題途中退出就記錄狀態,答完題就顯示結果。在這個過程中,我跟後臺互動的只有兩次:一次是使用者進來的時候獲取使用者答題進度,一次是使用者答完了最後一題,傳送使用者成績,讓後臺記錄;或者中途退出,傳送使用者答題進度給後臺,讓後臺記錄。

但是後來我在網頁中,由於暫時沒法監聽使用者是否退出,所以選擇了使用者回答完每一題的時候,把資料發給後臺,讓後臺答題進度。這樣請求數就多了N倍。伺服器的壓力就大了很多。

由於使用者進來,無論是小程式還是網站,都要請求介面,獲取使用者答題資料,這次不在對比範圍。這樣原本小程式只需要和後臺進行一次握手,但是在網頁中,採用了不合適的方式,和後臺握手次數變成了10次。足足多了9倍。如果是500人,每一輪從原本的500次,變成了5000次,三輪就從原本的1500次,變成了15000次!一般而言,10道題選擇題,是兩分鐘左右的回答時間,就相當於在2分鐘內伺服器要響應的次數多了9倍,這個擔子突然重了很多。而已這些請求,基本都有沒什麼意義的,因為絕大部分的人,10道題,大概兩分鐘的答題時間裡面,不會中途退出,相當於我做了一件沒意義,又消耗伺服器效能的事情。

讓我耿耿於懷的原因,我一向對請求數嚴格的控制,雖然現在公司不怎麼考慮效能,伺服器壓力。但是這會引起我的強迫症。

4.解壓方案

由於答題活動,9號要使用,而我是8號晚上洗完澡的時候和同事聊天的時候才想起,所以我沒時間改了,因為改了也是需要時間開發,測試。9號由於同事請假,他的專案也由我負責,也是比較趕的專案,我也沒那麼多時間改。只能委屈一下伺服器了。

說是這樣說,但是關於其他的給伺服器減輕負擔的方案,還是有比較講一下,算是給自己提個醒,也算是給大家提個醒。開發要注意一點:不要急,不要急,不要急。

PS:當時就是看著時間差不多是下午四點半了,然後還有兩個零散功能沒做,又要測試。找了很久的解決方案(監聽微信的‘返回’或者‘關閉按鈕’)都沒下落的情況下,一下急了,腦袋放空,就想了那個方法。

cookie或者localstore

記錄使用者的狀態,這個應該是最好的解決方案了,也應該是最簡單的解決方案。

比如使用cookie記錄使用者的答題進度。在使用者每答一題的時候,就把cookie記錄到的資料,更新一次。這樣只需要在使用者答完了最後一題的時候再把使用者的成績發給後臺就好,至於使用者中途退出也沒有,根據cookie判斷就好,如果cookie有記錄到使用者的資料。就顯示上次使用者退出時候的題目,讓使用者繼續答題。

原始碼:

/**
* @dedependson 點選選項
* @index 題目索引  number
* @item 當前選項物件 object
*/
chooseDo(index,item){
    /*其他程式碼略*/
    let _this=this;
    let _data={
        qid:_this.qid,//答題輪次,如'2'代表第二輪答題
        questions:_this.questions,//已答題目,'1,2,3'這個表示id為1,2,3的題目已經回答了
        totalScore:_this.totalScore//當前得分
    }
    //傳送請求,讓後臺記錄使用者答題進度
    this.$http.post(http_url.submit,_data,{emulateJSON:true}).then(res=>{
            
    })
}

複製程式碼

然後再到頁面載入的時候

mounted(){
    this.$http.get(http_url.getQuestions,{
        params:{
            qid:this.qid
        }
    }).then(res=>{
        res=res.body;
        //如果請求成功
        if(res.code===0){
            //如果使用者沒答完題 0-沒開始答題 1-沒答完題   2-答完題目
            if(res.datas.status!==2){
                //獲取答題的題目
                this.questionList=res.datas.entryList;
                //如果題目長度小於10,就是開始答題了,但是沒答完(中途退出的原因)
                if(this.questionList.length<10){
                    //顯示答題頁面,讓使用者答題
                    this.questionListShow=true;
                }
                //否則就是沒答過題目,讓使用者答題
                else{
                    //顯示開始答題頁面(答題首頁,使用者需要點選開始答題)
                }
            }
            //如果使用者已經答完題,顯示結果頁
            else{
                //程式碼略
            }
        }
        else{
            alert(res.msg)
        }
    })
}
       複製程式碼

cookie方案

chooseDo(index,item){
    /*其他程式碼略*/
    let _this=this;
    let _data={
        qid:_this.qid,//答題輪次,如'2'代表第二輪答題
        questions:_this.questions,//已答題目,'1,2,3'這個表示id為1,2,3的題目已經回答了
        totalScore:_this.totalScore//當前得分
    }
    //儲存cookie一天
    //_this.qid作為答題輪次的標識
    setCookie('answer-qid'+_this.qid,_this.qid,1);
    setCookie('answer-questions'+_this.qid,_this.questions,1);
    setCookie('answer-totalScore'+_this.qid,_this.totalScore,1);
}    
複製程式碼

cookie函式參考:ec-do

//設定cookie
setCookie(name, value, iDay) {
    let oDate = new Date();
    oDate.setDate(oDate.getDate() + iDay);
    document.cookie = name + '=' + value + ';expires=' + oDate;
},
//獲取cookie
getCookie(name) {
    let arr = document.cookie.split('; '),arr2;
    for (let i = 0; i < arr.length; i++) {
        arr2 = arr[i].split('=');
        if (arr2[0] == name) {
            return arr2[1];
        }
    }
    return '';
},
//刪除cookie
removeCookie(name) {
    this.setCookie(name, 1, -1);
}, 
複製程式碼

然後再到頁面載入的時候,處理方式的改變。

mounted(){
    this.$http.get(http_url.getQuestions,{
        params:{
            qid:this.qid
        }
    }).then(res=>{
        res=res.body;
        //如果請求成功
        if(res.code===0){
            //如果使用者沒答完題 0-沒開始答題 1-沒答完題   2-答完題目
            if(res.datas.status!==2){
                //記錄答題輪次
                this.qid=res.datas.qid; 
                //獲取答題的題目
                this.questionList=res.datas.entryList; 
                //如果使用者中途退出,我們沒有和後臺對介面,後臺無法記錄使用者答題進度,所以這次請求,返回的結果要麼是沒開始答題,要麼是答完題了。
                //要還原使用者答題記錄,要使用cookie
                //如果存在cookie記錄,那麼使用者肯定是至少答過一題,還原使用者答題進度
                let _answerQid=getCookie('answer-qid'+this.qid)
                _answerQuestions=getCookie('answer-qid'+this.qid).split(',');
                //字串轉整數
                _answerQuestions.map(item=>+item);
                
                if(_answerQid&&_answerQuestions){
                    this.questionList.fifler(item=>{
                        //item.id是題目的id
                        //如果題目的id存在,就過濾掉
                        _answerQuestions.indexOf(item.id)===-1
                    });
                    //顯示答題頁面,讓使用者答題
                    this.questionListShow=true;  
                }
                //否則就是沒答過題目,讓使用者答題
                else{
                    //顯示開始答題頁面(答題首頁,使用者需要點選開始答題)
                }
            }
            //如果使用者已經答完題,顯示結果頁
            else{
                //程式碼略
            }
        }
        else{
            alert(res.msg)
        }
    })
}
複製程式碼

程式碼上面,可能用了 cookie 會複雜些,但是就多了幾行而已,差不了多少,反倒是減輕了很多請求。

在小程式沒有使用這個方案,就是考慮到使用者退出小程式,可能會清除快取,雖然這個機率不大,所以使用生命週期函式進行unload()進行監聽,使用者退出就把使用者答題進度提交給後臺,讓後臺記錄,這樣的情況不會很多,甚至沒有,請求不會很多,所以當時就用了這個方案。沒有使用cookie或者localstore。

注意幾點:

1.無論什麼情況,開發都需要一個清醒的頭腦,因為頭腦不清醒,寫的都是bug,那個活動是一個一次性的專案,如果是長期的,我肯定會重構的,因為當時寫的程式碼太爛了。也容易犯一些低階的錯誤。

2.不要為了小概率的事件想得太多,給自己,同事,伺服器都帶來麻煩,也影響專案進度。這次就是想得太多,結果提測的時間晚了,驗收的時間晚了,自己也犯了錯誤。想太多的後果可能就是撿了芝麻,漏了西瓜,甚至是偷雞不成蝕把米。

2.小結

這次的的失誤就告一段落了,我也總結了一下,自己為什麼會對這次失誤更更於懷。

1.最近一直在看怎麼優化程式碼,讓程式碼更有可讀性,可維護性。卻犯了請求數過多的錯。顧此失彼啊。

2.第二個就是因為這次失誤,導致的後果太嚴重了,直接多了90%的請求。以往失誤導致的後果沒怎麼嚴重。

3.以往犯錯的時候,在專案上線之前能夠發現,並且有時候改,這次不一樣,這次是發現了,但是沒時間改了。

4.那些以為不會有,不應該犯的錯。可能就在頭腦不清醒的時候,就會犯這些錯誤,無論什麼時候都得留個神,這次也算是我自己提醒自己了。

不過結局是還算是好的,當天因為時間關係,答題活動沒有進行,所以伺服器沒有受到考驗。如果當天伺服器承受不住壓力,崩了,我也可能要引咎辭職了!

好了,故事就是這樣了,有點日記的感覺,希望大家諒解下。如果文章有什麼地方寫錯了,也歡迎指點交流。


--------------------華麗的分割線-------------------

想了解更多,關注關注我的微信公眾號:守候書閣

記一次低階並嚴重的開發失誤


相關文章