前言
在做 React Native 混合开发时,生产环境有时会遇到关上 RN(即 React Native 简称)利用白屏、RN 页面内操作闪退到 native 页面或者间接导致 APP Crash 的状况。通过剖析 APP 日志,发现起因能够归类为一下两种:
- js 层编译运行时报错。个别是因为某些非凡的数据或情景导致 js 执行报错;
- 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)报错。你能够应用 `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: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 实现的逻辑替换成咱们本人的逻辑!
小提示:源码中认真寻找
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 个办法:
-
handleException
—— 通过console.error()
&reportException()
解决但凡以throw '<error message>'
形式抛出的异样; -
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),能够在控制台看到更加信息的栈追踪信息:
很显著,黄屏提醒是由 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;
}
}
然而谬误边界有以下缺点:
谬误边界 无奈 捕捉以下场景中产生的谬误:
- 事件处理(理解更多)
- 异步代码(例如
setTimeout
或requestAnimationFrame
回调函数) - 服务端渲染(RN 中能够疏忽此条)
- 它本身抛出来的谬误(并非它的子组件)
很侥幸,通过咱们上述源码的剖析,咱们能够在谬误边界中通过 global.ErrorUtils.setGlobalHandler(callback)
注册 RN 错误处理回调函数以及设置 rejection-tracking.js
的onUnhandled
函数来解决未解决的 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
最初,答复几个大家可能有的疑难:
- 为什么不必 try…catch?
答:无奈确定哪个代码块会出现异常,大量应用 try…catch 会存在性能问题,并且它只能捕捉同步代码中的异样,对于异步代码中可能呈现的异样大刀阔斧;另外它也存在“失望的陷阱”这一问题。 - ErrorUtils 能捕捉异步的异样吗?
答:能够。只有是 RN 利用内抛出的异样都会被 ErrorUtils 捕捉。 - ErrorUtils 为什么不能捕捉 Promise 中的异样?
答:因为对于 JSC 来说,此时并没有产生谬误,当然无奈被捕捉。咱们所说的 Promise 异样,其实是 Promise 设计缺点导致一个 rejected Promise 始终未被解决,体现为:异样被吞掉了。因而咱们须要定义onUnhandled
进行解决。 - 能够应用 function component 来编写谬误边界吗?
答:不能够。谬误边界只能是 Class 组件。如果你想把 ErrorUtils 与 Promise 异样解决从谬误边界中剥离进去放到其余函数式组件中也是能够的,然而从组件化设计的角度来看的话,不举荐这样做。
申明
原创分享不易,感觉对你有所帮忙的话,欢送点赞珍藏。
转载需经自己批准,并附上思否原文链接。
谢谢!