免費推薦:(視訊)
在過去的開發經歷中處理了各種奇葩BUG,認識到程式碼健壯性(魯棒性)是提高工作效率、生活品質的一個重要指標,本文主要整理了提高程式碼健壯性的一些思考。
之前整理過關於程式碼健壯性相關的文章
本文將繼續探究除了單元測試、打紀錄檔之外其餘一些幫助提高JavaScript程式碼健壯性的方法。
不要相信前端傳的引數,也不要信任後臺返回的資料
比如某個api/xxx/list
的介面,按照檔案的約定
{ code: 0, msg: "", data: [ // ... 具體資料 ], };複製程式碼
前端程式碼可能就會寫成
const {code, msg, data} = await fetchList() data.forEach(()=>{})複製程式碼
因為我們假設了後臺返回的data是一個陣列,所以直接使用了data.forEach
,如果在聯調的時候遺漏了一些異常情況
[]
空陣列,但後臺的實現卻是不返回data
欄位這些時候,使用data.forEach
時就會報錯,
Uncaught TypeError: data.forEach is not a function
所以在這些直接使用後臺介面返回值的地方,最好新增型別檢測
Array.isArray(data) && data.forEach(()=>{})複製程式碼
同理,後臺在處理前端請求引數時,也應當進行相關的型別檢測。
由於JavaScript動態特性,我們在查詢物件某個屬性時如x.y.z
,最好檢測一下x
和y
是否存在
let z = x && x.y && x.y.z複製程式碼
經常這麼寫就顯得十分麻煩,dart中安全存取物件屬性就簡單得多
var z = a?.y?.z;複製程式碼
在ES2020中提出了空值合併運運算元的草案,包括??
和?.
運運算元,可以實現與dart相同的安全存取物件屬性的功能。目前開啟最新版Chrome就可以進行測試了
在此之前,我們可以封裝一個安全獲取物件屬性的方法
function getObjectValueByKeyStr(obj, key, defaultVal = undefined) { if (!key) return defaultVal; let namespace = key.toString().split("."); let value, i = 0, len = namespace.length; for (; i < len; i++) { value = obj[namespace[i]]; if (value === undefined || value === null) return defaultVal; obj = value; } return value; }var x = { y: { z: 100,},};var val = getObjectValueByKeyStr(x, "y.z");// var val = getObjectValueByKeyStr(x, "zz");console.log(val);複製程式碼
前端不可避免地要跟各種各種瀏覽器、各種裝置打交道,一個非常重要的問題就是相容性,尤其是目前我們已經習慣了使用ES2015的特性來開發程式碼,polyfill
可以幫助解決我們大部分問題。
參考:
例外處理是程式碼健壯性的首要保障,關於例外處理有兩個方面
可以通過throw語句丟擲一個自定義錯誤物件
// Create an object type UserExceptionfunction UserException (message){ // 包含message和name兩個屬性 this.message=message; this.name="UserException"; }// 覆蓋預設[object Object]的toStringUserException.prototype.toString = function (){ return this.name + ': "' + this.message + '"'; }// 丟擲自定義錯誤function f(){ try { throw new UserException("Value too high"); }catch(e){ if(e instanceof UserException){ console.log('catch UserException') console.log(e) }else{ console.log('unknown error') throw e } }finally{ // 可以做一些退出操作,如關閉檔案、關閉loading等狀態重置 console.log('done') return 1000 // 如果finally中return了值,那麼會覆蓋前面try或catch中的返回值或異常 } } f()複製程式碼
對於同步程式碼,可以使用通過責任鏈模式封裝錯誤,即當前函數如果可以處理錯誤,則在catch中進行處理:如果不能處理對應錯誤,則重新將catch拋到上一層
function a(){ throw 'error b'}// 當b能夠處理異常時,則不再向上丟擲function b(){ try{ a() }catch(e){ if(e === 'error b'){ console.log('由b處理') }else { throw e } } }function main(){ try { b() }catch(e){ console.log('頂層catch') } }複製程式碼
由於catch無法獲取非同步程式碼中丟擲的異常,為了實現責任鏈,需要把例外處理通過回撥函數的方式傳遞給非同步任務
function a(errorHandler) { let error = new Error("error a"); if (errorHandler) { errorHandler(error); } else { throw error; } }function b(errorHandler) { let handler = e => { if (e === "error b") { console.log("由b處理"); } else { errorHandler(e); } }; setTimeout(() => { a(handler); }); }let globalHandler = e => { console.log(e); }; b(globalHandler);複製程式碼
Promise只包含三種狀態:pending
、rejected
和fulfilled
let promise2 = promise1.then(onFulfilled, onRejected)複製程式碼
下面是promise丟擲異常的幾條規則
function case1(){ // 如果promise1是rejected態的,但是onRejected返回了一個值(包括undifined),那麼promise2還是fulfilled態的,這個過程相當於catch到異常,並將它處理掉,所以不需要向上丟擲。 var p1 = new Promise((resolve, reject)=>{ throw 'p1 error' }) p1.then((res)=>{ return 1 }, (e)=>{ console.log(e) return 2 }).then((a)=>{ // 如果註冊了onReject,則不會影響後面Promise執行 console.log(a) // 收到的是2 }) }function case2(){ // 在promise1的onRejected中處理了p1的異常,但是又丟擲了一個新異常,,那麼promise2的onRejected會丟擲這個異常 var p1 = new Promise((resolve, reject)=>{ throw 'p1 error' }) p1.then((res)=>{ return 1 }, (e)=>{ console.log(e) throw 'error in p1 onReject' }).then((a)=>{}, (e)=>{ // 如果p1的 onReject 丟擲了異常 console.log(e) }) }function case3(){ // 如果promise1是rejected態的,並且沒有定義onRejected,則promise2也會是rejected態的。 var p1 = new Promise((resolve, reject)=>{ throw 'p1 error' }) p1.then((res)=>{ return 1 }).then((a)=>{ console.log('not run:', a) }, (e)=>{ // 如果p1的 onReject 丟擲了異常 console.log('handle p2:', e) }) }function case4(){ // // 如果promise1是fulfilled態但是onFulfilled和onRejected出現了異常,promise2也會是rejected態的,並且會獲得promise1的被拒絕原因或異常。 var p1 = new Promise((resolve, reject)=>{ resolve(1) }) p1.then((res)=>{ console.log(res) throw 'p1 onFull error' }).then(()=>{}, (e)=>{ console.log('handle p2:', e) return 123 }) }複製程式碼
因此,我們可以在onRejected
中處理當前promise的錯誤,如果不能,,就把他拋給下一個promise
async/await
本質上是promise的語法糖,因此也可以使用promise.catch
類似的捕獲機制
function sleep(cb, cb2 =()=>{},ms = 100) { cb2() return new Promise((resolve, reject) => { setTimeout(() => { try { cb(); resolve(); }catch(e){ reject(e) } }, ms); }); }// 通過promise.catch來捕獲async function case1() { await sleep(() => { throw "sleep reject error"; }).catch(e => { console.log(e); }); }// 通過try...catch捕獲async function case2() { try { await sleep(() => { throw "sleep reject error"; }) } catch (e) { console.log("catch:", e); } }// 如果是未被reject丟擲的錯誤,則無法被捕獲async function case3() { try { await sleep(()=>{}, () => { // 丟擲一個未被promise reject的錯誤 throw 'no reject error' }).catch((e)=>{ console.log('cannot catch:', e) }) } catch (e) { console.log("catch:", e); } }複製程式碼
在實現一些比較小功能的時候,比如日期格式化等,我們可能並不習慣從npm找一個成熟的庫,而是自己順手寫一個功能包,由於開發時間或者測試用例不足,當遇見一些未考慮的邊界條件,就容易出現BUG。
這也是npm上往往會出現一些很小的模組,比如這個判斷是否為奇數的包:isOdd,周下載量居然是60來萬。
使用一些比較成熟的庫,一個很重要原因是,這些庫往往經過了大量的測試用例和社群的考驗,肯定比我們順手些的工具程式碼更安全。
一個親身經歷的例子是:根據UA判斷使用者當前存取裝置,正常思路是通過正則進行匹配,當時為了省事就自己寫了一個
export function getOSType() { const ua = navigator.userAgent const isWindowsPhone = /(?:Windows Phone)/.test(ua) const isSymbian = /(?:SymbianOS)/.test(ua) || isWindowsPhone const isAndroid = /(?:Android)/.test(ua) // 判斷是否是平板 const isTablet = /(?:iPad|PlayBook)/.test(ua) || (isAndroid && !/(?:Mobile)/.test(ua)) || (/(?:Firefox)/.test(ua) && /(?:Tablet)/.test(ua)) // 是否是iphone const isIPhone = /(?:iPhone)/.test(ua) && !isTablet // 是否是pc const isPc = !isIPhone && !isAndroid && !isSymbian && !isTablet return { isIPhone, isAndroid, isSymbian, isTablet, isPc } }複製程式碼
上線後發現某些小米平板使用者的邏輯判斷出現異常,調紀錄檔看見UA為
"Mozilla/5.0 (Linux; U; Android 8.1.0; zh-CN; MI PAD 4 Build/OPM1.171019.019) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.108 Quark/3.8.5.129 Mobile Safari/537.36複製程式碼
即使把MI PAD
新增到正則判斷中臨時修復一下,萬一後面又出現其他裝置的特殊UA呢?所以,憑藉自己經驗寫的很難把所有問題都考慮到,後面替換成mobile-detect這個庫。
使用模組的缺點在於
當然在進行模組選擇的時候也要進行各種考慮,包括穩定性、舊版本相容、未解決issue等問題。當選擇了一個比較好的工具模組之後,我們就可以將更多的精力放在業務邏輯中。
在開發環境下,我們可能需要一些原生的開關組態檔,這些設定只在本地開發時存在,不進入程式碼庫,也不會跟其他同事的設定起衝突。
我推崇將mock模板託管到git倉庫中,這樣可以方便其他同事開發和偵錯介面,帶來的一個問題時本地可能需要一個引入mock檔案的開關
下面是一個常見的做法:新建一個原生的組態檔config.local.js
,然後匯出相關設定資訊
// config.local.jsmodule.exports = { needMock: true}複製程式碼
記得在.gitignore中忽略該檔案
config.local.js複製程式碼
然後通過try...catch...
載入該模組,由於檔案未進入程式碼庫,在其他地方拉程式碼更新時會進入catch流程,本地開發則進入正常模組引入流程
// mock/entry.jstry { const { needMock } = require('./config.local') if (needMock) { require('./index') // 對應的mock入口 console.log('====start mock api===') } } catch (e) { console.log('未引入mock,如需要,請建立/mock/config.local並匯出 {needMock: true}') }複製程式碼
最後在整個應用的入口檔案判斷開發環境並引入
if (process.env.NODE_ENV === 'development') { require('../mock/entry') }複製程式碼
通過這種方式,就可以在本地開發時愉快地進行各種設定,而不必擔心忘記在提交程式碼前註釋對應的設定修改~
參考:
Code Review應該是是上線前一個必經的步驟,我認為CR主要的作用有
能夠確認需求理解是否出現偏差,避免扯皮
優化程式碼品質,包括冗餘程式碼、變數命名和過分封裝等,起碼除了寫程式碼的人之外還得保證稽核的人能看懂相關邏輯
對於一個需要長期維護迭代的專案而言,每一次commit和merge都是至關重要的,因此在合併程式碼之前,最好從頭檢查一遍改動的程式碼。即使是在比較小的團隊或者找不到稽核人員,也要把合併認真對待。
本文主要整理了提高JavaScript程式碼健壯性的一些方法,主要整理了
此外,還需要要養成良好的程式設計習慣,儘可能考慮各種邊界情況。
以上就是javascript提高前端程式碼強大的一些方法,很好!的詳細內容,更多請關注TW511.COM其它相關文章!