记一次低级并严重的开发失误

11,283

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.那些以为不会有,不应该犯的错。可能就在头脑不清醒的时候,就会犯这些错误,无论什么时候都得留个神,这次也算是我自己提醒自己了。

不过结局是还算是好的,当天因为时间关系,答题活动没有进行,所以服务器没有受到考验。如果当天服务器承受不住压力,崩了,我也可能要引咎辞职了!

好了,故事就是这样了,有点日记的感觉,希望大家谅解下。如果文章有什么地方写错了,也欢迎指点交流。


--------------------华丽的分割线-------------------

想了解更多,关注关注我的微信公众号:守候书阁