【从源码分析】可能是ip流量最实用的React Natioll异常解决方案【建议收藏家】

前言

在做React Native混合开发时,生产环境有时会遇到打开RN(即React Native简称)应用白屏、RN页面内操作闪退到native页面或者直接导致APP Crash的情况。通过分析APP日志,发现原因可以归类为以下两种:

  1. js 层编译运行时报错。一般是由于某些特殊的数据或情景导致js执行报错;
  2. js 转译 native UI 或与 native modules通信时出现异常.

对于第一点,可以很快地通过log追踪到出现问题的js代码并解决,但是对于第二点,往往是框架底层代码执行报错阻塞了UI渲染,报错日志信息无法定位出哪里出了问题,如:

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: com.facebook.react.common.c: Error: JS Functions are not convertible to dynamic

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: This error is located at:

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in RCTView

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in RCTView

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in RCTView

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in u

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in Tile

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in Tile

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in RCTView

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in RCTView

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in TouchableWithoutFeedback

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in RCTView

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in Unknown

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in RCTView

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in RCTView

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in h

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in RCTView

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in RCTScrollView

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in u

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in v

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in f

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in RCTView

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in RCTView

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in h

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in AndroidHorizontalScrollContentView

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in AndroidHorizontalScrollView

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in u

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in v

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in f

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in RCTView

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in n

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in inject-with-store(n)

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in MobXProvider

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in I

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in RCTView

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in RCTView

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: in c, stack:

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: <unknown>@-1

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: value@28:2227

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: <unknown>@19:1668

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: Ci@89:62783

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: qi@89:66674

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: ea@89:69555

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: <unknown>@89:81296

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: unstable_runWithPriority@164:3238

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: ja@89:81253

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: Oa@89:81007

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: Wa@89:80310

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: Aa@89:79323

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: Ki@89:68624

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: Ki@-1

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: yt@89:21420

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: y@115:657

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: callTimers@115:2816

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: value@28:3311

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: <unknown>@28:822

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: value@28:2565

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: value@28:794

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: value@-1

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: at com.facebook.react.modules.core.ExceptionsManagerModule.showOrThrowError(ExceptionsManagerModule.java:54)

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: at com.facebook.react.modules.core.ExceptionsManagerModule.reportFatalException(ExceptionsManagerModule.java:38)

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method)

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: at com.facebook.react.bridge.JavaMethodWrapper.invoke(JavaMethodWrapper.java:372)

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: at com.facebook.react.bridge.JavaModuleWrapper.invoke(JavaModuleWrapper.java:158)

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: at com.facebook.react.bridge.queue.NativeRunnable.run(Native Method)

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: at android.os.Handler.handleCallback(Handler.java:907)

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:105)

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: at com.facebook.react.bridge.queue.MessageQueueThreadHandler.dispatchMessage(MessageQueueThreadHandler.java:29)

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: at android.os.Looper.loop(Looper.java:216)

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: at com.facebook.react.bridge.queue.MessageQueueThreadImpl$4.run(MessageQueueThreadImpl.java:232)

06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: at java.lang.Thread.run(Thread.java:784)

应用出现异常还不是最糟糕的,糟糕的是因为出现异常,带给了用户糟糕的体验,尽管实际出现几率非常低。
我们应该在出现异常时,通过降级UI(如web端常见的404页面、"网络开小差了,请稍后再试"弹窗)提示和安慰用户,并引导用户转向正常页面。
很遗憾,通常情况下我们现在并没有这个主动权,一切异常处理都是由 React Native 框架自己完成的。因此,我们要从React Native中接管异常处理权力来实现我们自己的逻辑(类似 反转控制反转 思想)

下面,将带领大家一步步分析并实现。

分析React Native 的红屏/黄屏提示

不管是何种原因导致RN应用异常,在开发模式环境(在发布版 release/production中都是自动禁用的),默认情况下都会以红屏(red box)或黄屏(yellow box)方式全屏提示:

红屏:
red box.png
黄屏:
yellow box2.png

在官方描述中:

### 红屏错误

应用内的报错会以全屏红色显示在应用中(调试模式下),我们称为红屏(red box)报错。你可以使用`console.error()`来手动触发红屏错误。

### 黄屏警告

应用内的警告会以全屏黄色显示在应用中(调试模式下),我们称为黄屏(yellow box)报错。点击警告可以查看详情或是忽略掉。和红屏报警类似,你可以使用`console.warn()`来手动触发黄屏警告。

这2个全屏提示就是 React Native 对RN应用异常的处理。
那么思路来了,我们只需要找到 RN 弹出红屏、黄屏的地方,并将之替换为我们自己的业务逻辑即可。
示意图如下:
接管RN异常处理逻辑.png

OK,接下来我们需要从源码中去找到这个切入口,不要害怕源码,跟着我的思路,let's go!

从源码上找出切入口

1.找出红屏切入点

在上述红屏图片中,我们通过 console.error('I am red box') 触发了红屏提示。在提示中打印出了错误栈追踪信息:

console.error: "I am red box"

error

<unknown>

C:\workspace\test_timer_picker\node_modules\react-native\Libraries\Renderer\oss\ReactFabric-prod.js:6808:9

_callTimer

C:\workspace\test_timer_picker\node_modules\react-native\Libraries\Renderer\oss\ReactNativeRenderer-dev.js:8778:10

callTimers

C:\workspace\test_timer_picker\node_modules\react-native\Libraries\Renderer\oss\ReactNativeRenderer-dev.js:9080:8

__callFunction

<unknown>

__guard

C:\workspace\test_timer_picker\node_modules\react-native\Libraries\ART\ReactNativeART.js:169:9

callFunctionReturnFlushedQueue

callFunctionReturnFlushedQueue

[native code]

其中,指出了错误出现的文件位置:

\node_modules\react-native\Libraries\Renderer\oss\ReactFabric-prod.js

\node_modules\react-native\Libraries\Renderer\oss\ReactNativeRenderer-dev.js

\node_modules\react-native\Libraries\ART\ReactNativeART.js

依次在这几个文件中查询 console.error,可以在 ReactNativeRenderer-dev.js 文件中的showErrorDialog方法中找到这么一段注释:

  ExceptionsManager.handleException(errorToHandle, false);

// Return false here to prevent ReactFiberErrorLogger default behavior of

// logging error details to console.error. Calls to console.error are

// automatically routed to the native redbox controller, which we've already

// done above by calling ExceptionsManager.

意思是“调用 console.error 会自动导航到 native 红屏 controller” ,再查看showErrorDialog方法的注释:

/**

* Intercept lifecycle errors and ensure they are shown with the correct stack

* trace within the native redbox component.

*/

function showErrorDialog(capturedError) {/****/}

意思是“截获生命周期错误,并确保在native redbox 组件中显示正确的堆栈跟踪”
Perfect,我们根据错误栈信息一下找到了红屏的原因!
再仔细看这一句注释:

  //Calls to console.error are

// automatically routed to the native redbox controller, which we've already

// done above by calling ExceptionsManager.

“调用 console.error 会自动导航到 native 红屏 controller的原因,是我们已经在上面调用了 ExceptionsManager”

那么此时,我们可以想到,产生红屏 === 因为 ExceptionsManager 做了什么 我们要做的是去将ExceptionsManager实现的逻辑替换成我们自己的逻辑!

ok,继续看 ExceptionsManager.js,它的路径为:node_modules\react-native\Libraries\Core\ExceptionsManager.js,内容如下:

/**

* Copyright (c) Facebook, Inc. and its affiliates.

*

* This source code is licensed under the MIT license found in the

* LICENSE file in the root directory of this source tree.

*

* @format

* @flow

*/

'use strict';

import type {ExtendedError} from 'parseErrorStack';

/**

* Handles the developer-visible aspect of errors and exceptions

*/

let exceptionID = 0;

function reportException(e: ExtendedError, isFatal: boolean) {

const {ExceptionsManager} = require('NativeModules');

if (ExceptionsManager) {

const parseErrorStack = require('parseErrorStack');

const stack = parseErrorStack(e);

const currentExceptionID = ++exceptionID;

const message =

e.jsEngine == null ? e.message : `${e.message}, js engine: ${e.jsEngine}`;

if (isFatal) {

ExceptionsManager.reportFatalException(

message,

stack,

currentExceptionID,

);

} else {

ExceptionsManager.reportSoftException(message, stack, currentExceptionID);

}

if (__DEV__) {

const symbolicateStackTrace = require('symbolicateStackTrace');

symbolicateStackTrace(stack)

.then(prettyStack => {

if (prettyStack) {

ExceptionsManager.updateExceptionMessage(

e.message,

prettyStack,

currentExceptionID,

);

} else {

throw new Error('The stack is null');

}

})

.catch(error =>

console.warn('Unable to symbolicate stack trace: ' + error.message),

);

}

}

}

declare var console: typeof console & {

_errorOriginal: Function,

reportErrorsAsExceptions: boolean,

};

/**

* Logs exceptions to the (native) console and displays them

*/

function handleException(e: Error, isFatal: boolean) {

// Workaround for reporting errors caused by `throw 'some string'`

// Unfortunately there is no way to figure out the stacktrace in this

// case, so if you ended up here trying to trace an error, look for

// `throw '<error message>'` somewhere in your codebase.

if (!e.message) {

e = new Error(e);

}

if (console._errorOriginal) {

console._errorOriginal(e.message);

} else {

console.error(e.message);

}

reportException(e, isFatal);

}

function reactConsoleErrorHandler() {

console._errorOriginal.apply(console, arguments);

if (!console.reportErrorsAsExceptions) {

return;

}

if (arguments[0] && arguments[0].stack) {

reportException(arguments[0], /* isFatal */ false);

} else {

const stringifySafe = require('stringifySafe');

const str = Array.prototype.map.call(arguments, stringifySafe).join(', ');

if (str.slice(0, 10) === '"Warning: ') {

// React warnings use console.error so that a stack trace is shown, but

// we don't (currently) want these to show a redbox

// (Note: Logic duplicated in polyfills/console.js.)

return;

}

const error: ExtendedError = new Error('console.error: ' + str);

error.framesToPop = 1;

reportException(error, /* isFatal */ false);

}

}

/**

* Shows a redbox with stacktrace for all console.error messages. Disable by

* setting `console.reportErrorsAsExceptions = false;` in your app.

*/

function installConsoleErrorReporter() {

// Enable reportErrorsAsExceptions

if (console._errorOriginal) {

return; // already installed

}

// Flow doesn't like it when you set arbitrary values on a global object

console._errorOriginal = console.error.bind(console);

console.error = reactConsoleErrorHandler;

if (console.reportErrorsAsExceptions === undefined) {

// Individual apps can disable this

// Flow doesn't like it when you set arbitrary values on a global object

console.reportErrorsAsExceptions = true;

}

}

module.exports = {handleException, installConsoleErrorReporter};

我们通过语义良好的方法名以及清晰的注释可以了解到:
其暴露了2个方法:

  1. handleException —— 通过console.error() & reportException()处理凡是以throw '<error message>'方式抛出的异常;

  2. installConsoleErrorReporter —— 重载 console.error,只要是使用 console.error打印信息都会以“红屏”的方式显示错误堆栈信息。支持设置console.reportErrorsAsExceptions = false; 将此行为关闭。

分析到这一步,可以明显地感觉到,一切指向 console.error 方法!!

我们继续在 react native 源码中进行查询,找到installConsoleErrorReporter()方法在
node_modules\react-native\Libraries\Core\setUpErrorHandling.js 中被调用:

/**

* Copyright (c) Facebook, Inc. and its affiliates.

*

* This source code is licensed under the MIT license found in the

* LICENSE file in the root directory of this source tree.

*

* @flow strict-local

* @format

*/

'use strict';

/**

* Sets up the console and exception handling (redbox) for React Native.

* You can use this module directly, or just require InitializeCore.

*/

const ExceptionsManager = require('ExceptionsManager');

ExceptionsManager.installConsoleErrorReporter();

// Set up error handler

if (!global.__fbDisableExceptionsManager) {

const handleError = (e, isFatal) => {

try {

ExceptionsManager.handleException(e, isFatal);

} catch (ee) {

console.log('Failed to print error: ', ee.message);

throw e;

}

};

const ErrorUtils = require('ErrorUtils');

ErrorUtils.setGlobalHandler(handleError);

}

其注释十分清晰地指出:“为 React Native 设置 console 以及 异常处理(红屏)”

其核心设置代码是:

  const ErrorUtils = require('ErrorUtils');

ErrorUtils.setGlobalHandler(handleError); // 这就是我们要找的切入点

这就是我们要找的最终切入点,所有异常全部由ErrorUtils.setGlobalHandler的回调函数处理,只要将其设置为我们自己定义的回调函数就能从RN手中接过异常处理权了!!!
如:

    global.ErrorUtils.setGlobalHandler(e=> {

/*处理异常*/

console.log('%c 处理异常 .....', 'font-size:12px;color:#869')

console.log(e.message)

// do something to handle exception

//...

})

Nice~,接下来我们继续寻找黄屏(yellow box)的原因。


2.找出黄屏切入点

与红屏报错原因不同,熟悉js开发的同学应该知道,唯一能输出警告信息的就是调用console.warn()。在上述的黄屏提示中,并没有打印出栈追踪信息,但是我们可以开启debug模式(开发者菜单 -> Debug JS Remotely),可以在控制台看到更加详细的栈追踪信息:
yellow box stack.png

很明显,黄屏提示是由YellowBox.js输出的。
继续查看 RN 源码,找到其位置:node_modules\react-native\Libraries\YellowBox\YellowBox.js,内容如下:

/**

* Copyright (c) Facebook, Inc. and its affiliates.

*

* This source code is licensed under the MIT license found in the

* LICENSE file in the root directory of this source tree.

*

* @flow

* @format

*/

'use strict';

const React = require('React');

import type {Category} from 'YellowBoxCategory';

import type {Registry, Subscription} from 'YellowBoxRegistry';

type Props = $ReadOnly<{||}>;

type State = {|

registry: ?Registry,

|};

let YellowBox;

/**

* YellowBox displays warnings at the bottom of the screen.

*

* Warnings help guard against subtle yet significant issues that can impact the

* quality of the app. This "in your face" style of warning allows developers to

* notice and correct these issues as quickly as possible.

*

* YellowBox is only enabled in `__DEV__`. Set the following flag to disable it:

*

* console.disableYellowBox = true;

*

* Ignore specific warnings by calling:

*

* YellowBox.ignoreWarnings(['Warning: ...']);

*

* Strings supplied to `YellowBox.ignoreWarnings` only need to be a substring of

* the ignored warning messages.

*/

if (__DEV__) {

const Platform = require('Platform');

const RCTLog = require('RCTLog');

const YellowBoxList = require('YellowBoxList');

const YellowBoxRegistry = require('YellowBoxRegistry');

const {error, warn} = console;

// eslint-disable-next-line no-shadow

YellowBox = class YellowBox extends React.Component<Props, State> {

static ignoreWarnings(patterns: $ReadOnlyArray<string>): void {

YellowBoxRegistry.addIgnorePatterns(patterns);

}

static install(): void {

(console: any).error = function(...args) {

error.call(console, ...args);

// Show YellowBox for the `warning` module.

if (typeof args[0] === 'string' && args[0].startsWith('Warning: ')) {

registerWarning(...args);

}

};

(console: any).warn = function(...args) {

warn.call(console, ...args);

registerWarning(...args);

};

if ((console: any).disableYellowBox === true) {

YellowBoxRegistry.setDisabled(true);

}

(Object.defineProperty: any)(console, 'disableYellowBox', {

configurable: true,

get: () => YellowBoxRegistry.isDisabled(),

set: value => YellowBoxRegistry.setDisabled(value),

});

if (Platform.isTesting) {

(console: any).disableYellowBox = true;

}

RCTLog.setWarningHandler((...args) => {

registerWarning(...args);

});

}

static uninstall(): void {

(console: any).error = error;

(console: any).warn = error;

delete (console: any).disableYellowBox;

}

_subscription: ?Subscription;

state = {

registry: null,

};

render(): React.Node {

// TODO: Ignore warnings that fire when rendering `YellowBox` itself.

return this.state.registry == null ? null : (

<YellowBoxList

onDismiss={this._handleDismiss}

onDismissAll={this._handleDismissAll}

registry={this.state.registry}

/>

);

}

componentDidMount(): void {

this._subscription = YellowBoxRegistry.observe(registry => {

this.setState({registry});

});

}

componentWillUnmount(): void {

if (this._subscription != null) {

this._subscription.unsubscribe();

}

}

_handleDismiss = (category: Category): void => {

YellowBoxRegistry.delete(category);

};

_handleDismissAll(): void {

YellowBoxRegistry.clear();

}

};

const registerWarning = (...args): void => {

YellowBoxRegistry.add({args, framesToPop: 2});

};

} else {

YellowBox = class extends React.Component<Props> {

static ignoreWarnings(patterns: $ReadOnlyArray<string>): void {

// Do nothing.

}

static install(): void {

// Do nothing.

}

static uninstall(): void {

// Do nothing.

}

render(): React.Node {

return null;

}

};

}

module.exports = YellowBox;

它是一个 class 组件,大概逻辑是:“劫持宿主环境的console.warn,并将警告信息用原生 YellowBoxList渲染出来;同时也劫持console.error,将React环境中以error级别输出的警告信息还原成warning级别的日志(避免影响理解,这一点无需理会)”

这就是黄屏的切入点了,仅仅是将警告日志以另一种方式输出而已,好像与我们要做的事情无关,但是真的无关吗?

时刻记住,应用的每一个 error 和 warn 级别的日志都不应该忽视,尤其是warn级别的日志!

让我们看下以下代码:

  // 模拟异步操作 可能是请求、可能是与native modules 方法通信

mockAsyncHandle = ()=>{

return new Promise((resolve,reject)=>{

// 执行异常

throw new Error([1,2,3].toString())

})

}

async componentDidMount(){

const resp = await this.mockAsyncHandle() // 执行异常

// 后续代码不会再执行

console.log(resp)

// 使用 resp 去做业务处理,可能是更新state 也可能是某些操作的前提条件

// ...

}

这段代码会触发一个 yellow box 黄屏提示, warning 级别日志如下:
unhandled Promise.png

有过Promise丰富使用经验的同学可能已经发现了,在这里,throw new Error([1,2,3].toString()) 抛出的异常被吞掉了,代码中依赖resp的逻辑全部会失败,非常严重的异常!你可能想到链式调用Promise.prototye.catch()去处理拒绝状态的Promise,但是假如catch处理函数中继续抛出异常呢?这种现象在《你所不知道的JavaScript》书中被称为“绝望的陷阱”,与 try...catch 一样,始终会吞掉最后的异常。

在 web 端,浏览器会自动追踪内存使用情况,通过垃圾回收机制处理这个 rejected Promise,并且提供unhandledrejection事件进行监听。

那么,在RN中,此类Promise异常怎么处理呢?

查看源码node_modules\react-native\Libraries\Promise.js 可知,RN扩展了ES6 Promise :

/**

* Copyright (c) Facebook, Inc. and its affiliates.

*

* This source code is licensed under the MIT license found in the

* LICENSE file in the root directory of this source tree.

*

* @format

* @flow

*/

'use strict';

const Promise = require('promise/setimmediate/es6-extensions');

require('promise/setimmediate/done');

Promise.prototype.finally = function(onSettled) {

return this.then(onSettled, onSettled);

};

if (__DEV__) {

/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an

* error found when Flow v0.54 was deployed. To see the error delete this

* comment and run Flow. */

require('promise/setimmediate/rejection-tracking').enable({

allRejections: true,

onUnhandled: (id, error = {}) => {

let message: string;

let stack: ?string;

const stringValue = Object.prototype.toString.call(error);

if (stringValue === '[object Error]') {

message = Error.prototype.toString.call(error);

stack = error.stack;

} else {

/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses

* an error found when Flow v0.54 was deployed. To see the error delete

* this comment and run Flow. */

message = require('pretty-format')(error);

}

const warning =

`Possible Unhandled Promise Rejection (id: ${id}):\n` +

`${message}\n` +

(stack == null ? '' : stack);

console.warn(warning);

},

onHandled: id => {

const warning =

`Promise Rejection Handled (id: ${id})\n` +

'This means you can ignore any previous messages of the form ' +

`"Possible Unhandled Promise Rejection (id: ${id}):"`;

console.warn(warning);

},

});

}

module.exports = Promise;

RN 默认在开发环境下,通过promise/setimmediate/rejection-tracking去追踪 rejected 状态的Promise,并提供了onUnhandled回调函数处理未进行处理的 rejected Promise,其执行时机可以在rejection-tracking.js中源码中找到:

//...

timeout: setTimeout(

onUnhandled.bind(null, promise._51),

// For reference errors and type errors, this almost always

// means the programmer made a mistake, so log them after just

// 100ms

// otherwise, wait 2 seconds to see if they get handled

matchWhitelist(err, DEFAULT_WHITELIST)

? 100

: 2000

),

//...

与错误处理类似,我们只需将 onUnhandled回调函数替换成我们自定义的Promise 异常处理逻辑就能从RN手中接管Promise异常处理了!!!

完美的解决方案

在前言中有提到:

例如下面的提示(demo):
subUI.png

有 React 开发经验的同学应该知道,React 16+ 提供了一个方案:错误边界(Error Boundaries),完美地契合了我们逻辑上的要求。
官方demo如下:

class ErrorBoundary extends React.Component {

constructor(props) {

super(props);

this.state = { hasError: false };

}

static getDerivedStateFromError(error) {

// 更新 state 使下一次渲染能够显示降级后的 UI

return { hasError: true };

}

componentDidCatch(error, errorInfo) {

// 你同样可以将错误日志上报给服务器

logErrorToMyService(error, errorInfo);

}

render() {

if (this.state.hasError) {

// 你可以自定义降级后的 UI 并渲染

return <h1>Something went wrong.</h1>;

}

return this.props.children;

}

}

但是错误边界有以下缺陷:

错误边界无法捕获以下场景中产生的错误:

  • 事件处理(了解更多)
  • 异步代码(例如 setTimeoutrequestAnimationFrame 回调函数)
  • 服务端渲染(RN中可以忽略此条)
  • 它自身抛出来的错误(并非它的子组件)

很幸运,通过我们上述源码的分析,我们可以在错误边界中通过global.ErrorUtils.setGlobalHandler(callback)注册RN错误处理回调函数以及设置rejection-tracking.jsonUnhandled函数来处理未处理的 rejected Promise.

来看看修改后的最终代码,升级版错误边界:

import React from 'react'

import PropTypes from 'prop-types'

class ErrorBoundary extends React.Component {

constructor(props) {

super(props)

this.state = { hasError: false }

global.ErrorUtils.setGlobalHandler(e=> {

/*你的异常处理逻辑*/

console.log('%c 处理异常 .....', 'font-size:12px;color:#869')

console.log(e.message)

this.setState({

hasError: true

})

})

require('promise/setimmediate/rejection-tracking').enable({

allRejections: true,

onUnhandled: (id, error = {}) => {

let message

let stack

const stringValue = Object.prototype.toString.call(error);

if (stringValue === '[object Error]') {

message = Error.prototype.toString.call(error);

stack = error.stack;

} else {

/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses

* an error found when Flow v0.54 was deployed. To see the error delete

* this comment and run Flow. */

message = require('pretty-format')(error);

}

const warning =

`Possible Unhandled Promise Rejection (id: ${id}):\n` +

`${message}\n` +

(stack == null ? '' : stack);

console.warn(warning);

// 更新 state 使下一次渲染能够显示降级后的 UI

this.setState({

hasError: true

})

},

onHandled: id => {

const warning =

`Promise Rejection Handled (id: ${id})\n` +

'This means you can ignore any previous messages of the form ' +

`"Possible Unhandled Promise Rejection (id: ${id}):"`;

console.warn(warning);

},

});

}

static propTypes={

//自定义降级后的 UI

errorPage:PropTypes.element,

//可以根据自己的实际业务需求再增加其他属性,比如配置开发模式下是否要关闭红屏/黄屏显示

}

static getDerivedStateFromError(error) {

// 更新 state 使下一次渲染能够显示降级后的 UI

return { hasError: true }

}

componentDidCatch(error, errorInfo) {

// 你同样可以将错误日志上报给服务器

console.log(error, errorInfo)

}

render() {

if (this.state.hasError) {

// 你可以自定义降级后的 UI 并渲染

return this.props.errorPage? this.props.errorPage:<h1>Something went wrong.</h1>

}

return this.props.children

}

}

export default ErrorBoundary

使用方式与错误边界使用方式相同,在组件树最顶层,即包裹根组件使用:

//ErrorPage 是你自定义的降级显示UI

<ErrorBoundary errorPage={<ErrorPage/>}>

<App/>

</ErrorBoundary>

ErrorPage 是你自定义的降级显示UI

完美,自此,RN应用中所用的异常全部由我们自己掌控处理了!快去项目中试试吧

附注

本文中的 React Native 源码分析,皆来自于 0.59.9 版本,但我也查阅分析了最新的 0.62.2 版本源码,除了部分文件内容有新增以外,本文涉及的 API 均未发生破坏性更改,请放心食用。

另外,有消息称 React Native 架构重构将于2020年第4季度,也就是今年完成,架构演变如下:
rn 架构重构.png

希望到时 React Native 能带给我们更好的开发与使用体验!

FAQ

最后,回答几个大家可能有的疑问:

  1. 为什么不用 try...catch?
    答: 无法确定哪个代码块会出现异常,大量使用try...catch 会存在性能问题,并且它只能捕获同步代码中的异常,对于异步代码中可能出现的异常束手无策;另外它也存在 “绝望的陷阱” 这一问题。
  2. ErrorUtils 能捕获异步的异常吗?
    答:可以。只要是RN应用内抛出的异常都会被 ErrorUtils 捕获。
  3. ErrorUtils 为什么不能捕获Promise中的异常?
    答:因为对于JSC来说,此时并没有发生错误,当然无法被捕获。我们所说的 Promise 异常,其实是Promise 设计缺陷导致一个 rejected Promise 一直未被处理,表现为:异常被吞掉了。因此我们需要定义onUnhandled进行处理。
  4. 可以使用function component 来编写错误边界吗?
    答:不可以。错误边界只能是 Class 组件。如果你想把 ErrorUtils 与 Promise 异常处理从错误边界中剥离出来放到其他函数式组件中也是可以的,但是从组件化设计的角度来看的话,不推荐这样做。

声明

原创分享不易,觉得对你有所帮助的话,欢迎点赞收藏。
转载需经本人同意,并附上思否原文链接。
谢谢!

以上是 【从源码分析】可能是ip流量最实用的React Natioll异常解决方案【建议收藏家】 的全部内容, 来源链接: utcz.com/a/46253.html

回到顶部