React Native 異常處理

hujiao發表於2018-09-15

原文地址:github.com/HuJiaoHJ/bl…

React Native頁面出現錯誤時:

1、開發模式下,會出現紅色背景的頁面,展示當前程式碼錯誤資訊

2、bundle模式下,則會出現白屏或者閃退

開發模式

rn_dev_error

bundle模式

rn_bundle_error

在生產環境下,因RN頁面異常導致整個APP白屏或者閃退,使用者體驗並不好,所以應該對異常進行捕獲並處理,提高使用者體驗

主要使用兩種方法對RN頁面的異常進行捕獲並處理:

1、React Error Boundaries (異常邊界元件)

2、React Native ErrorUtils 模組

React Error Boundaries (異常邊界元件)

React Error Boundaries (異常邊界元件)是React 16 引入的新概念,為了避免React的元件內的UI異常導致整個應用的異常

對React的異常邊界元件不熟悉的小夥伴可以看看我的文章:從原始碼看React異常處理

這裡簡單介紹下:

Error Boundaries(異常邊界)是React元件,用於捕獲它子元件樹種所有元件產生的js異常,並渲染指定的兜底UI來替代出問題的元件

它能捕獲子元件生命週期函式中的異常,包括建構函式(constructor)和render函式

而不能捕獲以下異常:

  • Event handlers(事件處理函式)
  • Asynchronous code(非同步程式碼,如setTimeout、promise等)
  • Server side rendering(服務端渲染)
  • Errors thrown in the error boundary itself (rather than its children)(異常邊界元件本身丟擲的異常)

所以可以通過異常邊界元件捕獲元件生命週期內的所有異常並渲染兜底UI,防止APP白屏或閃退,提高使用者體驗,也可在兜底UI中指引使用者反饋截圖反饋問題,方便問題的排查和修復

直接上程式碼:

with_error_boundary.js

...
function withErrorBoundary(
    WrappedComponent: React.ComponentType <CatchCompProps> ,
    errorCallback: Function,
    allowedInDevMode: boolean,
    opt: Object = {}) {
    return class extends React.Component <CatchCompProps, CatchCompState> {
        state = {
            error: null,
            errorInfo: false,
            visible: false,
        }
        componentDidCatch(error: Error, errorInfo: any) {
            this.setState({
                error,
                errorInfo,
                visible: true,
            })
            errorCallback && errorCallback(error, errorInfo)
        }
        handleLeft = () => {
            ...
        }
        render() {
            const { title = 'Unexpected error occurred', message = 'Unexpected error occurred' } = opt
            return (
                this.state.visible && (allowedInDevMode ? true : process.env.NODE_ENV !== 'development') ? (
                <Modal 
                    visible
                    transparent
                    animationType={'fade'}>
                    <View style={styles.container}>
                        <View style={styles.header}>
                        <NavBar
                            title={title}
                            leftIcon={'arrow-left'}
                            handleLeft={this.handleLeft}/>
                        </View>
                        <View style={styles.info}>
                            <Text>{message}</Text>
                        </View> 
                        <ScrollView style={styles.content}>
                            <Text> { this.state.error && this.state.error.toString()} </Text>
                            <Text> { this.state.errorInfo && this.state.errorInfo.componentStack } </Text> 
                        </ScrollView>
                    </View>
                </Modal>
                ) : <WrappedComponent {...this.props} />
            );
        }
    }
}

export default withErrorBoundary;
複製程式碼

上面是一個React高階元件,返回的元件定義了componentDidCatch生命週期函式,當其子元件出現異常時,會執行此componentDidCatch生命週期函式,渲染兜底UI

使用

...
import withErrorBoundary from 'rn_components/exception_handler/with_error_boundary.js';
...
class ExceptionHandlerExample extends React.Component {
    state = {
        visible: false,
    }
    catch = () => {
        console.log('catch');
        this.setState({
            visible: true,
        });
    }
    render () {
        if (this.state.visible) {
            const a = d
        }
        return (
            <View style={styles.container}>
                <Navbar 
                    title={'Exception Handler'}
                    handleLeft={() => this.props.history.go(-1)}/>
                <View style={styles.content}>
                    <TouchableOpacity onPress={this.catch}>
                        <View>
                            <Text>Click me</Text>
                        </View>
                    </TouchableOpacity>
                </View>
            </View>
        );
    }
}
// 異常邊界元件的使用
export default withErrorBoundary(ExceptionHandlerExample, (error, errorInfo) => {
    console.log('errorCallback', error, errorInfo);
}, true);
複製程式碼

上面我們也說過,異常邊界元件能捕獲子元件生命週期函式中的異常,包括建構函式(constructor)和render函式

而不能捕獲以下異常:

  • Event handlers(事件處理函式)
  • Asynchronous code(非同步程式碼,如setTimeout、promise等)
  • Server side rendering(服務端渲染)
  • Errors thrown in the error boundary itself (rather than its children)(異常邊界元件本身丟擲的異常)

所以需要使用 React Native ErrorUtils 模組對這些異常進行捕獲並處理

React Native ErrorUtils 模組

React Native ErrorUtils 是負責對RN頁面中異常進行管理的模組,功能很類似Web頁面中的 window.onerror

首先我們看看怎麼利用 React Native ErrorUtils 進行非同步捕獲和處理,直接上程式碼:

error_guard.js

const noop = () => {};

export const setJSExceptionHandler = (customHandler = noop, allowedInDevMode = false) => {
    if (typeof allowedInDevMode !== "boolean" || typeof customHandler !== "function") {
        return;
    }
    const allowed = allowedInDevMode ? true : !__DEV__;
    if (allowed) {
        // !!! 關鍵程式碼
        // 設定錯誤處理函式
        global.ErrorUtils.setGlobalHandler(customHandler);
        // 改寫 console.error,保證報錯能被 ErrorUtils 捕獲並呼叫錯誤處理函式處理
        console.error = (message, error) => global.ErrorUtils.reportError(error);
    }
};

export const getJSExceptionHandler = () => global.ErrorUtils.getGlobalHandler();

export default {
    setJSExceptionHandler,
    getJSExceptionHandler,
};
複製程式碼

上面關鍵的程式碼就兩行,在註釋中已標明

使用

import { setJSExceptionHandler } from './error_guard';
import { Alert } from 'react-native';

setJSExceptionHandler((e, isFatal) => {
    if (isFatal) {
        Alert.alert(
            'Unexpected error occurred',
            `
            ${e && e.stack && e.stack.slice(0, 300)}...
            `,
            [{
                text: 'OK',
                onPress: () => {
                    console.log('ok');
                }
            }]
        );
    } else {
        console.log(e);
    }
}, true);
複製程式碼

使用很簡單,下面我們來看看 ErrorUtils 模組的原始碼

ErrorUtils 原始碼

本文原始碼是2018年9月10日拉取的React Native倉庫master分支上的程式碼

error_guard.js

首先看看 ErrorUtils 的定義,原始碼位置:Libraries/polyfills/error_guard.js

let _inGuard = 0;

let _globalHandler = function onError(e) {
  throw e;
};

const ErrorUtils = {
  setGlobalHandler(fun) {
    _globalHandler = fun;
  },
  getGlobalHandler() {
    return _globalHandler;
  },
  reportError(error) {
    _globalHandler && _globalHandler(error);
  },
  reportFatalError(error) {
    _globalHandler && _globalHandler(error, true);
  },
  ...
};

global.ErrorUtils = ErrorUtils;
複製程式碼

上面只展示了我們使用了的方法,我們可以看到我們改寫的 console.error,即 (message, error) => global.ErrorUtils.reportError(error) ,最終是執行的 _globalHandler

所以通過這種方法可以捕獲到所有使用了 console.error 的異常,我們來看看 React Native 原始碼中什麼地方使用了 ErrorUtils 來做異常捕獲和處理

MessageQueue.js

來到 MessageQueue 原始碼,位置:Libraries/BatchedBridge/MessageQueue.js

__guard(fn: () => void) {
    if (this.__shouldPauseOnThrow()) {
        fn();
    } else {
        try {
            fn();
        } catch (error) {
            ErrorUtils.reportFatalError(error);
        }
    }
}
複製程式碼

我們可以看到上面這個__guard方法中使用了try...catch...對函式的執行進行守護,當發生異常時,會呼叫 ErrorUtils.reportFatalError(error); 對錯誤進行處理

使用了__guard的地方這裡就不一一列舉了,我們可以看看 MessageQueue 這個模組在RN中處於什麼位置

React Native 異常處理

因為沒有系統的看過RN的原始碼,在網上找了個介紹 Native 和 JS 之間通訊的圖,我們可以看到 MessageQueue 在 Native 和 JS 之間通訊是很重要的模組

BatchedBridge.js

來到 BatchedBridge 原始碼,位置:Libraries/BatchedBridge/BatchedBridge.js

'use strict';

const MessageQueue = require('MessageQueue');

const BatchedBridge = new MessageQueue();

Object.defineProperty(global, '__fbBatchedBridge', {
  configurable: true,
  value: BatchedBridge,
});

module.exports = BatchedBridge;
複製程式碼

熟悉RN的同學應該知道,BatchedBridge是 Native 和 JS 之間通訊的關鍵模組,從上面的原始碼我們可以知道,BatchedBridge實際就是MessageQueue例項

所以在 MessageQueue 模組中使用 ErrorUtils 能捕獲到所有通訊過程中的異常並呼叫_globalHandler處理

以上所有程式碼可在個人開發的RN元件庫的專案中檢視到:rn_components ExceptionHandler,元件庫現在才剛開始建設,後續會不斷完善

寫在最後

以上就是我對 React Native 異常處理分享,希望能對有需要的小夥伴有幫助~~~

喜歡我的文章小夥伴可以去 我的個人部落格 點star ⭐️

相關文章