想分享這個主題很久了,前些天和好朋友聊天的時候也提到過,我覺得新手最像新手的地方就在於新手考慮事情總是不夠全面,這體現在很多方面,比如設計上、技術選型上等等。今天我想分享給大家的是其中一個方面:異常處理。
正好前些時間在手機上看到這麼一則小故事:
故事很好笑,可能大家看完都會覺得這群程式設計師也太蠢了,連這種狀況都沒有考慮到。其實在我們工作中,也常常會出現只關注於正常流程處理的開發,而忽略一些非正常的流程的情況。
其實討論程式異常處理的話題比較大,我大概分成三個方面來說,一個是程式的容錯,一個是業務上的容錯,另一個是安全方面。
程式的容錯
程式的容錯是我們寫程式碼過程中應該首要考慮的地方,比如當你要編寫一個模組給別人使用時,如果沒有考慮到一些特殊情況的輸入,可能模組就無法正常使用了。
需要考慮容錯其實是相當常見的場景,比如這裡我寫了一個好玩的“睡排序”,假設要給別人使用。
程式碼比較簡單,大家可能也能看出裡面常見的幾個可能拋異常的地方。
首先是 data.whatever.inner.array
巢狀太深了,很可能讀取不到。這裡我們常常使用預設引數。如 ES6 中的寫法:
第二個可能有問題的地方是 done 函式的呼叫,done 很可能外部沒有傳遞。這裡我們常常使用短路運算的方式避免:
最後還有一個可能比較隱晦的地方,這裡是對數字進行排序,所以我們應該考慮陣列中每一項的型別是否合法,也就是型別校驗。型別校驗又是另外一個話題,這裡只提一下對數字的校驗方法。
大家用的比較多的應該是 isNaN 方法了,這是掛載在 window 上的方法。其實 ES6 也提供了一個 isNaN 方法,並掛載在了 Number 上,也就是 Number.isNaN。這兩者的區別在於 window.isNaN 傳入 undefined、非空字串等等其實並不是 NaN 的引數時也返回 true,而 Number.isNaN 會首先判斷引數是否為 number,所以我們更推薦使用 Number.isNaN。
上面是一個比較簡單的例子,我們針對性地提出了三點改進。有了這些改進,我們的程式不會報錯,但在我們平常的開發過程中其實有些異常是不可避免的,我們必須要針對可能出現的異常進行異常捕獲,說到這裡,大家可能最先想到的就是 try...catch 語句了。
try...catch
try catch 是大家用的比較多的,這裡就不詳細展開說了,只提一下大家可能忽略的地方,就是 try catch 其實有三種形式:
- try...catch
- try...finally
- try...catch...finally
finally 塊是不管異常與否都會進入的地方,我們常常在這裡面做一些清理工作,在前端可能是把一些變數置為 null 避免記憶體洩露,後端可能是關閉資料庫連線等等。
另外需要注意的是在 finally 中 return 的值將作為 try catch 語句的整體返回值,不管在 try 或者 catch 中是否已經 return 了。
Promise
Promise 的異常捕獲是我在面試中比較常問的問題。Promise 是非同步的,所以對其 try catch 是沒有作用的。
我們一般使用 catch 方法去處理 Promise 的 reject 狀態,這裡需要知道的是 Promise 的 catch 方法只是一種特殊的 then 方法,catch 方法等同於呼叫 then 方法但把第一個引數置為 undefined。
另外提一個新手可能會犯的錯誤,在 then 方法中的 onResolved 回撥中丟擲的異常只會讓返回的新 Promise 的狀態置為 reject,而不會讓同一 then 方法中的 onRejected 方法執行。
async/await
async/await 是比 Promise 使用起來更方便也更容易理解的語法,它讓非同步的執行有了同步的寫法。對於 async/await 的異常捕獲,我覺得只要理解兩個地方就行:
- async 函式的返回值是 Promise 物件
- await 命令就是該 Promise 內部 then 命令的語法糖
async 函式可以當做普通函式呼叫,也可以使用 await 表示式呼叫。當作為普通函式呼叫時,如上所述,該函式返回一個 Promise,我們使用 Promise 的異常捕獲即可:
當使用 await 表示式呼叫時,會使 async 函式暫停執行,等待表示式中的 Promise 解析完成後繼續執行 async 函式並返回解決結果。所以我們可以使用 try catch 進行捕獲:
window.onerror
其實以上說到的都算針對性的異常捕獲,但在實際開發中,我們總會碰到我們沒考慮到的程式異常,這裡可以使用一個全域性的異常捕獲方法進行處理。
在瀏覽器裡,提供了 window.onerror 事件,它會捕獲程式中出現的未被捕獲到的同步或非同步的錯誤。
在事件處理函式中,能獲取到錯誤資訊、當前 URL、程式碼行數、列數等,非常詳細。
nodejs
篇幅原因,nodejs 的異常捕獲就不展開說了,在 process 上有三個事件:
- uncaughtException 捕捉全域性未捕獲異常,一般用作使程式優雅退出
- unhandledRejection 當有 Promise rejected 但沒有 onRejected 函式進行處理時觸發
- rejectionHandled 當有 Promise rejected 但被 onRejected 函式進行處理時觸發
業務上的容錯
說完程式裡的容錯,我們接著說一下業務上的容錯。其實在本文開頭提到的例子就是一個典型的業務上沒有進行容錯的例子。其實業務上的容錯更多的是產品需要考慮的問題,但作為開發,我們也需要去理解業務,並能敏銳地發現一些業務中可能碰到的問題,避免開發到一半需要推翻重來。
程式碼上丟擲異常只會讓你的程式不可用,說白了使用者體驗就不會好。但一旦業務上發生問題,那麼影響的可能就是產品的業務線了。
舉個例子,我們之前做了個專案,因為訂單的狀態太多,使用者會有很多不同的操作來使訂單轉移到不同的狀態,不同的狀態又會反應出不同的操作。最後就在不同狀態的切換中亂套了,導致上線之後收到了很多使用者狀態錯誤的反饋。
這跟開發往往沒有什麼太大的聯絡,而是對業務的理解。
安全
最後跟大家聊一下安全。我們說了這麼多,其實側重的都是正常使用者的使用,但在實際生產環境中,我們必須還要考慮一些惡意的輸入。
篇幅原因也不展開說了。。簡單提一下這幾種攻擊。
CSRF 攻擊
面試的時候我也會嘗試性問一下對方在安全方面考慮的問題。一般會從跨域開始問,然後自然而然提到 jsonp,jsonp 可以讓我們進行跨域請求,但其中是否會有安全問題?畢竟別人也能通過 jsonp 訪問你的網站了。
另外,當使用者登入了 a.com 之後,開啟了一個 evil.com,在 evil.com 裡向 a.com 傳送請求,是會攜帶 a.com 的 cookie 的,包括 jsonp 請求,這裡的安全問題又要如何避免?
一般我們會使用新增 token 的方式來防止 CSRF 攻擊,token 可以隱藏在表單中,或者在請求頭裡攜帶,作為一個合法請求的校驗。
XSS 攻擊
這個可能是前端接觸比較多的攻擊,一般分為反射型的 XSS 攻擊和儲存型的 XSS 攻擊。區別在於儲存型的 XSS 攻擊會使惡意程式碼存放在服務端,導致所有能看到這段程式碼的使用者都受影響。
XSS 攻擊的誘導常見於一些惡意連結,當用於訪問一些奇怪的網址(比如郵箱裡的垃圾郵件、廣告連結),可能會跳轉到具有 XSS 漏洞的網站,從而引發安全問題。
一般我們需要考慮對使用者的輸入進行合法校驗,包括前端和後端。
SQL 注入
SQL 注入是後端同學需要著重考慮的問題,對於一些小白程式碼,特別常見於一些學校的管理後臺,很可能有 SQL 注入的漏洞,舉個最簡單的例子:
connection.query(`SELECT username from user where username = "${username}" and password = "${password}"`, function () {
// ...
})
複製程式碼
如果後端對使用者輸入沒有進行任何過濾,直接是這麼校驗使用者登入的話,那麼只要使用者猜出使用者名稱,如常見的 admin,然後使用 " or "1" = "1,就能正常登入了。
總結
說了這麼多,其實今天的主題就一個,那就是希望大家在以後的開發中,一定要多考慮異常的情況,不要想當然地以為使用者一定會按照正常的流程走進來。當然,這也需要產品同學考慮各種可能出現的業務情況、測試同學進行各種 case 的測試。在大家共同的努力下,才能保證整個產品不會出現大的漏洞。
如果發現以上內容有任何不正確的地方,或者想一起探討的,歡迎在評論區留言~