你是怎麼處理vue專案中的錯誤的?

林恒發表於2024-05-14

一、錯誤型別

任何一個框架,對於錯誤的處理都是一種必備的能力

Vue 中,則是定義了一套對應的錯誤處理規則給到使用者,且在原始碼級別,對部分必要的過程做了一定的錯誤處理。

主要的錯誤來源包括:

  • 後端介面錯誤
  • 程式碼中本身邏輯錯誤

二、如何處理

後端介面錯誤

透過axiosinterceptor實現網路請求的response先進行一層攔截

apiClient.interceptors.response.use(
  response => {
    return response;
  },
  error => {
    if (error.response.status == 401) {
      router.push({ name: "Login" });
    } else {
      message.error("出錯了");
      return Promise.reject(error);
    }
  }
);

程式碼邏輯問題

全域性設定錯誤處理

設定全域性錯誤處理函式

Vue.config.errorHandler = function (err, vm, info) {
  // handle error
  // `info` 是 Vue 特定的錯誤資訊,比如錯誤所在的生命週期鉤子
  // 只在 2.2.0+ 可用
}

errorHandler指定元件的渲染和觀察期間未捕獲錯誤的處理函式。這個處理函式被呼叫時,可獲取錯誤資訊和 Vue 例項

不過值得注意的是,在不同Vue 版本中,該全域性 API 作用的範圍會有所不同:

從 2.2.0 起,這個鉤子也會捕獲元件生命週期鉤子裡的錯誤。同樣的,當這個鉤子是 undefined 時,被捕獲的錯誤會透過 console.error 輸出而避免應用崩

從 2.4.0 起,這個鉤子也會捕獲 Vue 自定義事件處理函式內部的錯誤了

從 2.6.0 起,這個鉤子也會捕獲 v-on DOM 監聽器內部丟擲的錯誤。另外,如果任何被覆蓋的鉤子或處理函式返回一個 Promise 鏈 (例如 async 函式),則來自其 Promise 鏈的錯誤也會被處理

生命週期鉤子

errorCaptured是 2.5.0 新增的一個生命鉤子函式,當捕獲到一個來自子孫元件的錯誤時被呼叫

基本型別

(err: Error, vm: Component, info: string) => ?boolean

此鉤子會收到三個引數:錯誤物件、發生錯誤的元件例項以及一個包含錯誤來源資訊的字串。此鉤子可以返回 false 以阻止該錯誤繼續向上傳播

參考官網,錯誤傳播規則如下:

  • 預設情況下,如果全域性的 config.errorHandler 被定義,所有的錯誤仍會傳送它,因此這些錯誤仍然會向單一的分析服務的地方進行彙報
  • 如果一個元件的繼承或父級從屬鏈路中存在多個 errorCaptured 鉤子,則它們將會被相同的錯誤逐個喚起。
  • 如果此 errorCaptured 鉤子自身丟擲了一個錯誤,則這個新錯誤和原本被捕獲的錯誤都會傳送給全域性的 config.errorHandler
  • 一個 errorCaptured 鉤子能夠返回 false 以阻止錯誤繼續向上傳播。本質上是說“這個錯誤已經被搞定了且應該被忽略”。它會阻止其它任何會被這個錯誤喚起的 errorCaptured 鉤子和全域性的 config.errorHandler

下面來看個例子

定義一個父元件cat

Vue.component('cat', {
    template:`
        <div>
			<h1>Cat: </h1>
        	<slot></slot>
        </div>`,
    props:{
        name:{
            required:true,
            type:String
        }
    },
    errorCaptured(err,vm,info) {
        console.log(`cat EC: ${err.toString()}\ninfo: ${info}`); 
        return false;
    }

});

定義一個子元件kitten,其中dontexist()並沒有定義,存在錯誤

Vue.component('kitten', {
    template:'<div><h1>Kitten: {{ dontexist() }}</h1></div>',
    props:{
        name:{
            required:true,
            type:String
        }
    }
});

頁面中使用元件

<div id="app" v-cloak>
    <cat name="my cat">
        <kitten></kitten>
    </cat>
</div>

在父元件的errorCaptured則能夠捕獲到資訊

cat EC: TypeError: dontexist is not a function
info: render

三、原始碼分析

異常處理原始碼

原始碼位置:/src/core/util/error.js

// Vue 全域性配置,也就是上面的Vue.config
import config from '../config'
import { warn } from './debug'
// 判斷環境
import { inBrowser, inWeex } from './env'
// 判斷是否是Promise,透過val.then === 'function' && val.catch === 'function', val !=== null && val !== undefined
import { isPromise } from 'shared/util'
// 當錯誤函式處理錯誤時,停用deps跟蹤以避免可能出現的infinite rendering
// 解決以下出現的問題https://github.com/vuejs/vuex/issues/1505的問題
import { pushTarget, popTarget } from '../observer/dep'

export function handleError (err: Error, vm: any, info: string) {
    // Deactivate deps tracking while processing error handler to avoid possible infinite rendering.
    pushTarget()
    try {
        // vm指當前報錯的元件例項
        if (vm) {
            let cur = vm
            // 首先獲取到報錯的元件,之後遞迴查詢當前元件的父元件,依次呼叫errorCaptured 方法。
            // 在遍歷呼叫完所有 errorCaptured 方法、或 errorCaptured 方法有報錯時,呼叫 globalHandleError 方法
            while ((cur = cur.$parent)) {
                const hooks = cur.$options.errorCaptured
                // 判斷是否存在errorCaptured鉤子函式
                if (hooks) {
                    // 選項合併的策略,鉤子函式會被儲存在一個陣列中
                    for (let i = 0; i < hooks.length; i++) {
                        // 如果errorCaptured 鉤子執行自身丟擲了錯誤,
                        // 則用try{}catch{}捕獲錯誤,將這個新錯誤和原本被捕獲的錯誤都會傳送給全域性的config.errorHandler
                        // 呼叫globalHandleError方法
                        try {
                            // 當前errorCaptured執行,根據返回是否是false值
                            // 是false,capture = true,阻止其它任何會被這個錯誤喚起的 errorCaptured 鉤子和全域性的 config.errorHandler
                            // 是true capture = fale,元件的繼承或父級從屬鏈路中存在的多個 errorCaptured 鉤子,會被相同的錯誤逐個喚起
                            // 呼叫對應的鉤子函式,處理錯誤
                            const capture = hooks[i].call(cur, err, vm, info) === false
                            if (capture) return
                        } catch (e) {
                            globalHandleError(e, cur, 'errorCaptured hook')
                        }
                    }
                }
            }
        }
        // 除非禁止錯誤向上傳播,否則都會呼叫全域性的錯誤處理函式
        globalHandleError(err, vm, info)
    } finally {
        popTarget()
    }
}
// 非同步錯誤處理函式
export function invokeWithErrorHandling (
handler: Function,
 context: any,
 args: null | any[],
    vm: any,
        info: string
        ) {
            let res
            try {
                // 根據引數選擇不同的handle執行方式
                res = args ? handler.apply(context, args) : handler.call(context)
                // handle返回結果存在
                // res._isVue an flag to avoid this being observed,如果傳入值的_isVue為ture時(即傳入的值是Vue例項本身)不會新建observer例項
                // isPromise(res) 判斷val.then === 'function' && val.catch === 'function', val !=== null && val !== undefined
                // !res._handled  _handle是Promise 例項的內部變數之一,預設是false,代表onFulfilled,onRejected是否被處理
                if (res && !res._isVue && isPromise(res) && !res._handled) {
                    res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
                    // avoid catch triggering multiple times when nested calls
                    // 避免巢狀呼叫時catch多次的觸發
                    res._handled = true
                }
            } catch (e) {
                // 處理執行錯誤
                handleError(e, vm, info)
            }
            return res
        }

//全域性錯誤處理
function globalHandleError (err, vm, info) {
    // 獲取全域性配置,判斷是否設定處理函式,預設undefined
    // 已配置
    if (config.errorHandler) {
        // try{}catch{} 住全域性錯誤處理函式
        try {
            // 執行設定的全域性錯誤處理函式,handle error 想幹啥就幹啥💗
            return config.errorHandler.call(null, err, vm, info)
        } catch (e) {
            // 如果開發者在errorHandler函式中手動丟擲同樣錯誤資訊throw err
            // 判斷err資訊是否相等,避免log兩次
            // 如果丟擲新的錯誤資訊throw err Error('你好毒'),將會一起log輸出
            if (e !== err) {
                logError(e, null, 'config.errorHandler')
            }
        }
    }
    // 未配置常規log輸出
    logError(err, vm, info)
}

// 錯誤輸出函式
function logError (err, vm, info) {
    if (process.env.NODE_ENV !== 'production') {
        warn(`Error in ${info}: "${err.toString()}"`, vm)
    }
    /* istanbul ignore else */
    if ((inBrowser || inWeex) && typeof console !== 'undefined') {
        console.error(err)
    } else {
        throw err
    }
}

小結

  • handleError在需要捕獲異常的地方呼叫,首先獲取到報錯的元件,之後遞迴查詢當前元件的父元件,依次呼叫errorCaptured 方法,在遍歷呼叫完所有 errorCaptured 方法或 errorCaptured 方法有報錯時,呼叫 globalHandleError 方法
  • globalHandleError呼叫全域性的 errorHandler 方法,再透過logError判斷環境輸出錯誤資訊
  • invokeWithErrorHandling更好的處理非同步錯誤資訊
  • logError判斷環境,選擇不同的拋錯方式。非生產環境下,呼叫warn方法處理錯誤

參考文獻

  • https://juejin.cn/post/6844904096936230925

  • https://segmentfault.com/a/1190000018606181

如果對您有所幫助,歡迎您點個關注,我會定時更新技術文件,大家一起討論學習,一起進步。

你是怎麼處理vue專案中的錯誤的?

相關文章