您好,登錄后才能下訂單哦!
這篇文章給大家分享的是有關javascript提高前端代碼強大的案例分析的內容。小編覺得挺實用的,因此分享給大家做個參考。一起跟隨小編過來看看吧。
在過去的開發經歷中處理了各種奇葩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提高前端代碼強大的案例分析就分享到這里了,希望以上內容可以對大家有一定的幫助,讓大家可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。