前言

在做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 dynamic06-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 RCTView06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in u06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in Tile06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in Tile06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in TouchableWithoutFeedback06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in Unknown06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in h06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTScrollView06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in u06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in v06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in f06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in h06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in AndroidHorizontalScrollContentView06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in AndroidHorizontalScrollView06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in u06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in v06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in f06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in n06-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 MobXProvider06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in I06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView06-17 17:59:49.482 10253 24147 24401 E AndroidRuntime:     in RCTView06-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>@-106-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: value@28:222706-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: <unknown>@19:166806-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: Ci@89:6278306-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: qi@89:6667406-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: ea@89:6955506-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: <unknown>@89:8129606-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: unstable_runWithPriority@164:323806-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: ja@89:8125306-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: Oa@89:8100706-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: Wa@89:8031006-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: Aa@89:7932306-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: Ki@89:6862406-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: Ki@-106-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: yt@89:2142006-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: y@115:65706-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: callTimers@115:281606-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: value@28:331106-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: <unknown>@28:82206-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: value@28:256506-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: value@28:79406-17 17:59:49.482 10253 24147 24401 E AndroidRuntime: value@-106-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)报错。你能够应用`console.error()`来手动触发红屏谬误。### 黄屏正告利用内的正告会以全屏黄色显示在利用中(调试模式下),咱们称为黄屏(yellow box)报错。点击正告能够查看详情或是疏忽掉。和红屏报警相似,你能够应用`console.warn()`来手动触发黄屏正告。

这2个全屏提醒就是 React Native 对RN利用异样的解决。
那么思路来了,咱们只须要找到 RN 弹出红屏、黄屏的中央,并将之替换为咱们本人的业务逻辑即可
示意图如下:

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:10callTimers    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:9callFunctionReturnFlushedQueue    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实现的逻辑替换成咱们本人的逻辑!

小提示: 源码中认真寻找showErrorDialog()被调用的地位,你会找到logCapturedError()以及更下层的logError(),剖析logError(),你会发现,原来 React 中的谬误边界能捕捉到组件渲染时谬误也与之无关

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 handlerif (!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),能够在控制台看到更加信息的栈追踪信息:

很显著,黄屏提醒是由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 级别日志如下:

有过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异样解决了!!!

OK,通过剖析源码,咱们曾经理清思路并晓得应该如何做了,接下来入手实现吧。

完满的解决方案

计划:谬误边界 + ErrorUtils + promise rejection tracking

在前言中有提到:

咱们应该在出现异常时,通过降级UI(如web端常见的404页面、"网络开小差了,请稍后再试"弹窗)提醒刺激用户,并疏导用户转向失常页面。

例如上面的提醒(demo):

有 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季度,也就是往年实现,架构演变如下:

图片来源于 React Native maintainer——Lorenzo S.

心愿到时 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 异样解决从谬误边界中剥离进去放到其余函数式组件中也是能够的,然而从组件化设计的角度来看的话,不举荐这样做。

申明

原创分享不易,感觉对你有所帮忙的话,欢送点赞珍藏。
转载需经自己批准,并附上思否原文链接。
谢谢!