壹 ❀ 引
上篇文章算是開了一個新系列,因為工作緣故,我基本每天都在跟各式各樣的bug打交道。其實站在一個開發的角度,我想每個人應該都更喜歡創造新代碼,創造新bug,而不是每天都泡在茫茫代碼海洋中定位和修復問題。
當然,當產品部資源不夠時,我偶爾也會接手做做需求,比如上周有個比較急的需求沒資源投入,產品管理那邊就從幾個組都抽出了一個人湊成了一個臨時小組,前端這邊工作就是由我來負責了。當我做完前端方案去評審的時候,技術委員會的大佬(入職的比較晚)對我發出靈魂拷問,你也做需求??
所以有時候真的需要對這份工作調整好心態,以前我也經常問自己,做前端不做需求寫大量的代碼,這真能學到東西嗎,因此我也跟我所在的部門負責人抱怨過這件事,他對我說,你在一家公司從來就沒有人能限制你學到什么。
我當時突然想起了一部看過的電視劇東京大飯店(我基本不看電視劇,因為沒什么耐心...但這個是真好看= =),影片講述了男主為打造世界最棒的米其林三星餐廳找回曾經的餐廳伙伴一起追夢的故事,而開店前期缺人因為機緣巧合招了個菜鳥新人(除了這個新人其余的人都算頂級廚師了)。男主與其他人苦思冥想研究新品菜系,新人不是在訓練切5mm的蘿卜丁就是在切5mm蘿卜丁,每次想要得到表現的機會去做菜,一次次被拒絕,也因此覺得自己在這學不到東西得不到認可,差點跟男主鬧翻。我當時就想,不是吧大哥,你深處一個頂級廚師的團隊,身邊都是世界級的大佬,哪怕不做菜,天天在這個氛圍中花點心思學一學,出去都能吊打各種廚師了!!這么難得的經歷,完全就是死腦筋!!
正所謂當局者迷旁觀者清,我自己何嘗不像是這個電視劇的菜鳥新人呢?項目代碼就放在那,想學到什么想了解什么,完全是由自己的心來決定的,除了自己沒有人能限制你學到什么。
所以后來我回顧自己經手的一些bug,才發現確實有很多值得研究和深思的問題(技術委員會大佬也覺得很多問題是值得搜集起來講一講),這也是為什么我開始寫這類修復經歷的原因。
那么東京大飯店的菜鳥新人最后到底有沒有得到男主認可,男主一行人有沒有追夢成功呢?本文肯定也不會提,而我也在接受當下,一步步改寫自己的"命運"。
另外,本文所闡述的bug並不是專屬於react,而是一個很常見的前端搜索場景問題,所以即便不會react我也推薦了解下,萬一面試被問場景問題遇到了呢?好了,閑話說了一堆,本文正式開始。
貳 ❀ 問題場景再現
在上周csm就給我提了一個客戶反饋的bug單,因為這個客戶是一個大客戶,所以反饋的bug我們都比較看重。但因為上周確實比較忙一直沒時間跟進,結果本周一我剛出地鐵還在路上,csm企業微信就滴滴我了,問我今天有沒有時間遠程下這個問題,想與我同步下方便約客戶。
我其實之前也嘗試復現過這個問題,但不管是我本地環境還是客戶側私有部署測試環境,都沒能復現這個問題。所以我跟他說,上午給我點時間先熟悉下這塊功能的代碼,下午2點就可以遠程。
遠程其實就是遠程鏈接客戶的電腦,操作客戶的電腦來排查問題,一般只有我們這邊實在復現不了問題時才會提出遠程。遠程的環境因為都是正式環境,隨便一個文件打開都是十幾萬行高度壓縮的代碼,可以說毫無閱讀體驗,不提前做下准備到時候遠程了找不出原因,那我不得尷尬死。
等到下午遠程鏈接了客戶也是一番操作....對方演示就是能復現,我接管鼠標操作就是不能復現,幾番摸索,終於還是在客戶電腦上復現了這個問題,因為客戶數據安全的問題,這里就不能直接貼問題原圖了,但我們還是先來還原下問題場景。
bug現象其實很簡單,在成員頁用戶可以通過搜索查找到符合條件的所有成員,然后可以勾選成員加入到當前項目。所以每次輸入或者修改輸入框內容,一定都會發起請求,然后前端響應讓展示區域的列表發生改變。
比如上圖中,一開始有個A,那么一開始內容區域展示的都是A相關的成員。緊接着用戶做了一次清空操作,然后又輸入了一個B,理論上來說,最終內容區域應該展示B相關的人員,但很遺憾,B相關的成員只展示了一會,緊接着區域的數據又變成了未加任何搜索字段的全部數據,也就是初始數據了。所以客戶提單說,搜索輸入框的結果每次都只能展示一會,過會自己就變了。
我在復現問題后,打開了客戶電腦控制台的Network,看了眼復現操作中的請求調用,問題的原因馬上就清楚了。我用一個圖來表示這個過程。
這個行為中一共發起了兩次請求,在我遠程查看了兩次請求耗時發現,第一次請求耗時了4s,第二次耗時只用了1S。也就是說這就是個后發先至的問題,后發的請求B先回來了,因此前端先展示了B的內容,沒多久,第一次請求也就是空條件的結果又回來了,覆蓋了B的請求結果,這就是這個問題的根因。
而后端對於這種請求處理都是並發的,相互之間並不會有所感知,你前端發幾次請求過來,肯定是先處理完的我先反饋給你,如果后端加個隊列嚴格按先后順序處理,那要是炸一個接口半天沒響應,其余的請求都沒法玩了。
而為什么我自己沒能復現這個問題呢?這是因為客戶側數據有幾十萬,而我自己的測試賬號一共就3條user數據....沒有這個數據量支撐,復現幾率就是0。
同時我又想到了第二個問題,為什么沒有篩選條件的請求反而比有篩選條件的慢?理論上來說我不傳篩選條件,你都不用查了,直接給數據我,這邊得到的結論是,后端那邊的查詢是越接近底層查詢越快,越接近業務層查詢速度越慢,當篩選條件為空時,因為沒篩選,所以后端業務層面對的是幾十萬的數據量;而有了篩選條件,到業務層的數據已經被篩選過一次可能就只有幾百條了,用時反而更少了。
那怎么解決呢?讓后端解決?后端也明說了,數據量擺在那,接口又是並發不可能給你排隊處理,所以問題還是得前端來解決,下面說說方案。
叄 ❀ 解決方案
讓我們回到問題本身,一個看似簡單的搜索居然能引發這樣的有趣問題,假設這是個面試題又該如何解決呢?其實思路可分為兩步。
我一開始看這個問題,懷疑是濫用防抖造成的,結果一看代碼,好家伙根本沒用防抖= =,也就是說假設用戶是光速A-空-B,那確實會發起兩個請求,第一次肯定是給這種高頻修改加一個防抖,無意義的請求能不發就不發。
但事實上防抖並沒有從根本解決問題,問題的根因是數據量太大,查詢確實要那么久,我們設置防抖一般也就是300ms左右,假設用戶A-空-B的間隔超過了你設置的防抖時間,前端還是會發起兩次請求,而后端還是會有后發先至的可能性,所以單一個防抖解決不了問題。而這個時候,我們還需要加一步操作,那就是加個開關去取消上一次的請求,畫個圖:
(PS:防抖還是要加,假設現在數十萬用戶同時訪問,不加防抖造成的無意義請求那就是數十萬個了,還是會造成服務器資源大量浪費)
我們來解釋下這個過程,一開始有個請求開關,默認值是false。
模擬一次請求:請求發起-->請求開關默認是false-->發起請求-->修改請求開關為true-->請求結束-->修改請求開關為false-->結束。
模擬上面的bug場景:請求發起-->開關是false-->發起請求-->修改請求開關為true-->請求還沒結束又發起了第二次請求-->請求開關是true-->取消上次請求-->繼續走正常請求路線...(假設過程中又操作了多次繼續重復取消操作)...-->結束。
大家可以思考下這個過程,對於同一請求,如果用戶確實操作了多次,對於用戶而言TA關心的其實就是最后一次操作的結果,因此當前面一次請求沒回來,我們完全可以舍棄掉這次請求,直接發起新的請求,后續操作同理。
當然,假設接口響應巨快,快到超出了用戶操作間隔,那我們其實啥也不用干,畢竟后端返回數據先后順序完全符合用戶預期,requestSwitch開發自然會被合理切換,咱也不用做額外處理。
有同學可能要問了,你說的我都懂,那這個請求取消我該怎么做,其實axios就有提供一個API叫CancelToken,這就是解決上述問題的妙葯,而取消的底層原理與XMLHttpRequest.abort()有很大關系。因為篇幅問題,關於取消原理還是另起一篇文章來介紹吧。
這篇文章其實說到這也沒貼一點代碼,因為前面也說了,這個問題並不屬於react專屬的問題,而是每一個搜索面對大數據量時都可能遇到的問題,重要的是方案,有了方案看看axios文檔我還不信你還做不出來。
肆 ❀ 總
OK,那么到這里又介紹了一個有趣的bug排查經歷。我想搜索功能大家應該都做過,但不一定都有遇到過這種場景問題,比如我前面前端三年還真沒處理過此類問題,畢竟項目太簡單了,這也是為什么我要寫這類博客的原因。bug永遠有的修,所以這類文章應該還會更新很多篇,也算是一個小科普了,下一個bug已經在安排中了,那么本文就到這里了!
