背景
之所以寫這篇文章,是因為有同事使用全局變量不當導致了bug。所以在解釋標題之前,首先說一下業務背景。
很簡單,就是有一個頁面可以辦理某個業務,這個業務又分為兩種類型,可以隨意切換類型。發現問題的過程是,頁面初始化時默認是A類型,所以此時前端會按照A類型傳參調用后台大概3個接口,我們暫且稱作接口1,接口2和接口3吧。其中接口3的請求參數依賴接口1和接口2的響應參數,接口1和接口2的返回數據會展示到前端,然后調用接口3時將從接口1和接口2的返回參數中拿數據傳遞給接口3,然后將接口3返回的數據展示,到此頁面初始化加載完成。
由下面頁面草圖可以看出,接口1,2,3都依賴於類型來完成對應的邏輯處理,在接口調用上肯定是先調接口1,2(二者沒有先后順序),然后調接口3。之后在從A類型切換至B類型時又會重新按B類型重新加載一遍接口1,2,3,展示B類型對應的數據。
問題排查
大概的業務規則就是這樣的,很簡單。但是在測試中發現,當頁面初始化時,迅速切換到B類型,前端彈出一個錯誤窗口“系統錯誤,缺少必要參數”,偶現的問題但可以穩定復現。
經過排查分析發現是前端接口調用順序問題,具體點就是調用接口3時,沒有拿到需要的數據(接口3的邏輯大致是通過前端傳的參數1和參數2取接口1和接口2放在緩存的數據,緩存的Key和類型有關)
從表象上看就是在調用接口3時,接口1或接口2還沒有被調用,導致接口3從緩存拿不到需要的數據。
帶着這樣的疑問去查看前端代碼,看接口的調用順序是不是真的有問題,結果發現前端調用的順序是沒有問題的。那問題是出在哪里呢?
通過排查前端代碼,發現一個問題,前端設置了一個全局變量來記錄當期的業務類型(如A類型、B類型),調用接口1,2,3傳遞業務類型時就是傳遞的這個全局變量。看到這也許你就能想明白為什么說謹慎使用全局變量了,這個問題正是因為全局變量的使用不當導致的。
分析原因
我們來一起分析下到底是如何導致的吧。
上述也提到了初始化時快速切換到B類型,那么前端的這個記錄當前業務類型的全局變量是何時改變其值的呢?
沒錯,正是在切換業務類型時記錄當前業務類型A或B。當初始化默認是A類型時,接口會這樣調用A類型:接口1(A)->接口2(A)->接口3(A),當切換到B類型時觸發一系列接口調用,和A類型也一樣,B類型:接口1(B)->接口2(B)->接口3(B)這樣調用。
關鍵就是在切換到B類型時,可能會存在這樣的問題,接口1,2正常調用,即傳遞的業務類型都是A,但恰好在調用接口3前,切換到了業務類型B類型,那么此時記錄當前業務類型的全局變量隨之變為B,那么此時原本初始化的時候的接口3拿到的業務類型就由預期的A變成了B,而在此之前接口1,2都是按A類型傳遞的參數,故后台存儲的數據是A類型的,但此時因為全局變量的變化,接口3傳遞的業務類型就又A變為B,故在接口3的業務邏輯里,按業務類型B去緩存取數據時是取不到,后端校驗參數時就會報錯“系統錯誤,缺少必要參數”。
看到這,你是不是覺得這有點像java的多線程共享變量?多線程共享變量也會引發這樣的問題,當一個線程正在使用某一變量時,突然被別的線程修改了,導致該線程拿到了臟數據。解決辦法是,線程獨享資源的操作權,操作完畢其他線程才有權限讀取該資源,同一時間只有一個線程才能修改共享變量,即多個線程間相對於該資源是互斥的關系,java中多用鎖來保證操作的安全性。
那在這個問題中,怎么類比呢?我們可以把選中A類型時要走的一系列接口比作A線程;把B類型要走的一系列接口比作B線程,這兩個線程執行的流程、方法一樣,只是需要的參數的具體值是不一樣的,A、B線程各自執行三個步驟每個步驟都會取共享變量作為參數傳遞給后台。再把切換類型要改變當前業務類型(`biz_type`)這一操作記作C線程。那大致就是,A、B線程讀 `biz_type` ,C線程修改 `biz_type` 。這就可以理解成三個線程共享一個變量,在頁面上切換業務類型可以看做線程的輪轉,所以不加以控制難免會發生錯誤。
問題解決
弄懂了發生問題的原因之后怎么來解決呢?其實解決起來也簡單,正如標題所說[**謹慎使用全局變量**],問題的根源就是使用了全局共享變量,導致在A線程還沒走完時C線程修改了 biz_type 的值,從而導致線程A的三個步驟拿到的 biz_type 的值不相同,進而導致后台根據類型取緩存數據時拿不到,最終報錯。所以,想要解決該問題,最關鍵的就是從這個全局變量着手,經查看前端代碼而知:在切換類型時,根據當前選中的類型傳遞相應的參數,當選中時我們就能知道是哪種類型了,所以我們就能清楚的去調用接口傳遞相應的類型字段,而不是先對全局變量賦值,再在接口里自行去取全局變量。
修改前:
var biz_type = 'A';//定義全局變量,默認為A業務類型 //change radio function changeRadio(){ if(#('#bizType_A').is(':checked')){ biz_type = 'A';//修改變量值 api_1(); }else{ biz_type = 'B';//修改變量值 api_1(); } } //function1 function api_1(){ //get biz_type //send ajax with biz_typ if(data.success){ api_2(); }else{ alert(data.msg); } } //function2 function api_2(){ //get biz_type //send ajax with biz_typ if(data.success){ api_3(); }else{ alert(data.msg); } } //function3 function api_3(){ //get biz_type //send ajax with biz_type if(data.success){ jump_to_success(); }else{ alert(data.msg); } }
修改后:
//change radio function changeRadio(){ if(#('#bizType_A').is(':checked')){ api_1('A');//參數傳遞 }else{ api_1('B');//參數傳遞 } } //function1 function api_1(biz_type){ //send ajax with biz_typ if(data.success){data. api_2(biz_type); }else{ alert(data.msg); } } //function2 function api_2(biz_type){ //send ajax with biz_typ if(data.success){ api_3(biz_type); }else{ alert(data.msg); } } //function3 function api_3(biz_type){ //send ajax with biz_type if(data.success){ jump_to_success(); }else{ alert(data.msg); } }
修改后使用參數傳遞的方式,這樣可以保證一套流程走下來,拿到的 biz_type 值一樣。
另外,可以通過控制切換的方式保證A線程沒走完時不允許修改 biz_type 的值,不允許執行B線程,即當A類型下的流程沒走完時切換不了類型。可以通過標志位來判定A流程是否走完,進而判定是否可以切換到B類型上。
總結
不過這個問題不大,后端做了參數的校驗,但是為了提升用戶體驗這個問題一定是要解決的。這其實是前端開發人員一個小小的疏忽導致的,當前端在寫代碼時他肯定不會預見到會發生這樣的問題,他肯定不會想到全局變量會導致這樣的問題,更不會想到用戶在頁面沒初始化完成時就切換類型。但這些對於一個初出茅廬的前端開發來說,情有可原,權當是積累經驗了。切記能傳參的盡量不要用全局變量
出問題不可怕,在問題中成長,積累經驗,才是最重要的。