javascript提高前端程式碼強大的一些方法,很好!

2020-11-19 18:01:00

欄目介紹提高前端程式碼強大的一些方法。

免費推薦:(視訊)

在過去的開發經歷中處理了各種奇葩BUG,認識到程式碼健壯性(魯棒性)是提高工作效率、生活品質的一個重要指標,本文主要整理了提高程式碼健壯性的一些思考。

之前整理過關於程式碼健壯性相關的文章

  • 正兒八經地寫JavaScript之單元測試
  • 如何在程式碼中打紀錄檔

本文將繼續探究除了單元測試、打紀錄檔之外其餘一些幫助提高JavaScript程式碼健壯性的方法。

更安全地存取物件

不要相信介面資料

不要相信前端傳的引數,也不要信任後臺返回的資料

比如某個api/xxx/list的介面,按照檔案的約定

{
    code: 0,
    msg: "",
    data: [     // ... 具體資料
    ],
};複製程式碼

前端程式碼可能就會寫成

const {code, msg, data} = await fetchList()
data.forEach(()=>{})複製程式碼

因為我們假設了後臺返回的data是一個陣列,所以直接使用了data.forEach,如果在聯調的時候遺漏了一些異常情況

  • 預期在沒有資料時data會返回[]空陣列,但後臺的實現卻是不返回data欄位
  • 後續介面更新,data從陣列變成了一個字典,跟前端同步不及時

這些時候,使用data.forEach時就會報錯,

Uncaught TypeError: data.forEach is not a function

所以在這些直接使用後臺介面返回值的地方,最好新增型別檢測

Array.isArray(data) && data.forEach(()=>{})複製程式碼

同理,後臺在處理前端請求引數時,也應當進行相關的型別檢測。

空值合併運運算元

由於JavaScript動態特性,我們在查詢物件某個屬性時如x.y.z,最好檢測一下xy是否存在

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可以幫助解決我們大部分問題。

記得例外處理

參考:

  • JS錯誤處理 MDN
  • js構建ui的統一例外處理方案,這個系列的文章寫得非常好

例外處理是程式碼健壯性的首要保障,關於例外處理有兩個方面

  • 合適的錯誤處理可以提要使用者體驗,在程式碼出錯時優雅地提示使用者
  • 將錯誤處理進行封裝,可以減少開發量,將錯誤處理與程式碼解耦

錯誤物件

可以通過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);複製程式碼

Prmise的例外處理

Promise只包含三種狀態:pendingrejectedfulfilled

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

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這個庫。

使用模組的缺點在於

  • 可能會增加檔案依賴體積,增加打包時間等,這個問題可以通過打包設定解決,將不會經常變更的第三方模組打包到vendor檔案中設定快取
  • 在某些專案可能會由於安全考慮需要減少第三方模組的使用,或者要求先進行原始碼code review

當然在進行模組選擇的時候也要進行各種考慮,包括穩定性、舊版本相容、未解決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

參考:

  • Code Review 是苦澀但有意思的修行

Code Review應該是是上線前一個必經的步驟,我認為CR主要的作用有

  • 能夠確認需求理解是否出現偏差,避免扯皮

  • 優化程式碼品質,包括冗餘程式碼、變數命名和過分封裝等,起碼除了寫程式碼的人之外還得保證稽核的人能看懂相關邏輯

對於一個需要長期維護迭代的專案而言,每一次commit和merge都是至關重要的,因此在合併程式碼之前,最好從頭檢查一遍改動的程式碼。即使是在比較小的團隊或者找不到稽核人員,也要把合併認真對待。

小結

本文主要整理了提高JavaScript程式碼健壯性的一些方法,主要整理了

  • 安全地存取物件屬性,避免資料異常導致程式碼報錯
  • 捕獲異常,通過責任鏈的方式進行例外處理或上報
  • 使用更穩定更安全的第三方模組,
  • 認真對待每一次合併,上線前先檢查程式碼

此外,還需要要養成良好的程式設計習慣,儘可能考慮各種邊界情況。

以上就是javascript提高前端程式碼強大的一些方法,很好!的詳細內容,更多請關注TW511.COM其它相關文章!