React中如何優雅的捕捉事件錯誤


React中如何優雅的捕捉事件錯誤

前話

人無完人,所以代碼總會出錯,出錯並不可怕,關鍵是怎么處理。
我就想問問大家react的錯誤怎么捕捉呢? 這個時候:

  • 小白:怎么處理?
  • 小白+: ErrorBoundary
  • 小白++: ErrorBoundary, try catch
  • 小白#: ErrorBoundary, try catch, window.onerror
  • 小白##: 這個是個嚴肅的問題,我知道*種處理方式,你有什么好的方案?

正題

小白#回答的基本就是解決思路。我們來一個一個簡單說說。

1. EerrorBoundary

EerrorBoundary是16版本出來的,有人問那我的15版本呢,我不聽我不聽,反正我用16,當然15有unstable_handleError。
關於EerrorBoundary官網介紹比較詳細,這個不是重點,重點是他能捕捉哪些異常。

Error boundaries在rendering,lifeCyclemethod或處於他們樹層級之下的構造函數中捕獲錯誤
哦,原來如此。 怎么用

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  componentDidCatch(error, info) {
    // Display fallback UI
    this.setState({ hasError: true });
    // You can also log the error to an error reporting service
    logErrorToMyService(error, info);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}


<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

重點:error boundaries並不會捕捉這些錯誤:

  • 事件處理器
  • 異步代碼
  • 服務端的渲染代碼
  • 在error boundaries區域內的錯誤

2. try catch

簡單有效的捕捉

handleClick = () => {
    try {
      // Do something that could throw
    } catch (error) {
      this.setState({ error });
    }
}

3. window.onerror

超級奧特曼,只是錯誤信息比較不好分析。

4.其他

  1. http請求
    封裝后,專門處理
  2. 狀態管理redux,mobx等
    封裝攔截 ,我們在項目的應用mobx-state-tree基本都是在model里面攔截的
  3. 其他
    自己看着辦啊,都找我,我很忙的。

問題

啊?這么多事件處理和方法都要加try catch啊。 你笨啊window.onerror啊。
onerror是非常好,但是有個問題,錯誤細節不好分析,有大神說,正則解析。
我不扶牆扶你。

解決

decorator特性,裝飾器。 create-react-app創建的app默認是不知此的裝飾器的。
不要和我爭,github地址上人家寫着呢can-i-use-decorators?

那問題又來了,如何支持裝飾器。

const {injectBabelPlugin} = require('react-app-rewired');

/* config-overrides.js */
module.exports = {
    webpack: function override(config, env) {       
        // babel  7
        config = injectBabelPlugin('transform-decorators-legacy',config)
        // babel 6
        config = injectBabelPlugin('transform-decorators',config)
        return config;
    }
}

關於裝飾器這里不做過多的說明,修改類的行為。
這里又有幾個點

  1. 裝飾方法 裝飾類 裝飾getter, setter都可以,我們選在裝飾方法和類
  2. 裝飾類,如何排除系統內置方法和繼承的方法
  3. 裝飾的時候有參和無參數怎么處理

我們先寫一個來檢查內置方法的方法, 不夠自己補全

const PREFIX = ['component', 'unsafe_']
const BUILTIN_METHODS = [
    'constructor',
    'render',
    'replaceState',
    'setState',
    'isMounted',
    'replaceState'
]
// 檢查是不是內置方法
function isBuiltinMethods(name) {
    if (typeof name !== 'string' || name.trim() === '') {
        return false
    }
    // 以component或者unsafe_開頭
    if (PREFIX.some(prefix => name.startsWith(prefix)))) {
        return true
    }
    // 其他內置方法
    if (BUILTIN_METHODS.includes(name)) {
        return true
    }
    return false
}

再弄一個裝飾方法的方法, 這個方法參考了autobind.js
handleError是自己的錯誤處理函數,這里沒有寫出來

// 監聽方法

 function createDefaultSetter(key) {
    return function set(newValue) {
        Object.defineProperty(this, key, {
            configurable: true,
            writable: true,
            // IS enumerable when reassigned by the outside word
            enumerable: true,
            value: newValue
        });

        return newValue;
    };
}


function observerHandler(fn, callback) {
    return (...args) => {
        try {
            fn(...args)
        } catch (err) {
            callback(err)
        }
    }
}
//方法的裝飾器, params是額外的參數
function catchMethod(target, key, descriptor, ...params) {

    if (typeof descriptor.value !== 'function') {
        return descriptor
    }
    const { configurable, enumerable, value: fn } = descriptor
    return {
        configurable,
        enumerable,

        get() {
            // Class.prototype.key lookup
            // Someone accesses the property directly on the prototype on which it is
            // actually defined on, i.e. Class.prototype.hasOwnProperty(key)
            if (this === target) {
                return fn;
            }

            // Class.prototype.key lookup
            // Someone accesses the property directly on a prototype but it was found
            // up the chain, not defined directly on it
            // i.e. Class.prototype.hasOwnProperty(key) == false && key in Class.prototype
            if (this.constructor !== constructor && getPrototypeOf(this).constructor === constructor) {
                return fn;
            }

            const boundFn = observerHandler(fn.bind(this), err => {
                handleError(err, target, key, ...params)
            })

            defineProperty(this, key, {
                configurable: true,
                writable: true,
                // NOT enumerable when it's a bound method
                enumerable: false,
                value: boundFn
            });

            boundFn.bound = true
            return boundFn;
        },
        set: createDefaultSetter(key)
    };
}

再來一個裝飾類的

/**
 * 檢查是不是需要代理
 * @param {*} method 
 * @param {*} descriptor 
 */
function shouldProxy(method, descriptor) {
    return typeof descriptor.value === 'function'
        && !isBuiltinMethods(method)
        && descriptor.configurable
        && descriptor.writable
        && !descriptor.value.bound
}

function catchClass(targetArg, ...params) {
    // 獲得所有自定義方法,未處理Symbols
    const target = targetArg.prototype || targetArg
    let descriptors = getOwnPropertyDescriptors(target)
    for (let [method, descriptor] of Object.entries(descriptors)) {
        if (shouldProxy(method, descriptor)) {
            defineProperty(target, method, catchMethod(target, method, descriptors[method], ...params))
        }
    }
}

最后暴露一個自動識別方法和類的方法

/**
 * 
 * 未攔截getter和setter
 * 未攔截Symbols屬性
 */
export default function catchError(...args) {
    const lastArg = args[args.length - 1]
    // 無參數方法
    if (isDescriptor(lastArg)) {
        return catchMethod(...args)
    } else {
        // 無參數class?? 需要改進
        if (args.length === 1 && typeof args[0] !== 'string') {
            return catchClass(...args)
        }
        // 有參
        return (...argsList) => {
            // 有參數方法
            if (isDescriptor(argsList[argsList.length - 1])) {
                return catchMethod(...[...argsList, ...args])
            }
            // 有參數class
            return catchClass(...[...argsList, ...args])
        }
    }
}

基本成型。
怎么調用

裝飾類

@catchError('HeHe')
class HeHe extends React.Component {
    
    state = {
        clicked: false
    }
    
    onClick(){
        this.setState({
            clicked:true
        })
        this.x.y.z.xxx
    }

    render(){
        return (
            <input type="button" value="點擊我" onClick={this.onClick}/>
        )
    }

}

裝飾方法

class HeHe extends React.Component {    
    state = {
        clicked: false
    }
    
    @catchError('HeHe onClick')
    onClick(){
        this.setState({
            clicked:true
        })
        this.x.y.z.xxx
    }

    render(){
        return (
            <input type="button" value="點擊我" onClick={this.onClick}/>
        )
    }

}

當然你還可以既裝飾類又裝飾方法, 這個時候方法的裝飾優先於類的裝飾,不會重復裝飾

@catchError('HeHe')
class HeHe extends React.Component {
    
    state = {
        clicked: false
    }
    
    @catchError('HeHe onClick')
    onClick(){
        this.setState({
            clicked:true
        })
        this.x.y.z.xxx
    }

    onClick2(){

    }

    render(){
        return (
            <React.Fragment>
                <input type="button" value="點擊我" onClick={this.onClick}/>
                <input type="button" value="點擊我2" onClick={this.onClick2}/>
            </React.Fragment>
        )
    }

}

如上,細心的人可以發現, 沒有 onClick.bind(this), 是的, catchError會自動完成bind,是不是很cool。

如上,現在的所有的事件處理都會被catchError里面定義的handleError處理,怎么處理就看你自己了。
有人就問了,我要是想捕捉后還要有額外處理的了,比如來個提示框之類的。
這個就取決你的需求和怎么處理,你依舊可以在你的事件處理器try catch。
二是,你沒看到@catchError里面可以傳遞參數么,可以提供額外的錯誤信息,比如場景,是不是致命錯誤等等信息。

她解決了你未顯示處理的事件處理錯誤,有沒有很優雅,有沒有。
你們都說沒有的話, 我就放棄前端了,可是我還有老婆孩子要養,所以你們一定要有人說有。

error-boundaries
React異常處理
catching-react-errors

react進階之異常處理機制-error Boundaries

decorator
core-decorators
autobind.js


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM