在做React Native混合开发时,生产环境有时会遇到打开RN(即React Native简称)应用白屏、RN页面内操作闪退到native页面或者直接导致APP Crash的情况。通过分析APP日志,发现原因可以归类为以下两种:
对于第一点,可以很快地通过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中接管异常处理权力来实现我们自己的逻辑(类似 反转控制反转 思想)
下面,将带领大家一步步分析并实现。
不管是何种原因导致RN应用异常,在开发模式环境(在发布版 release/production中都是自动禁用的),默认情况下都会以红屏(red box)或黄屏(yellow box)方式全屏提示:
请注意此文中,报错和警告,都视为异常
红屏:
黄屏:
在官方描述中:
### 红屏错误 应用内的报错会以全屏红色显示在应用中(调试模式下),我们称为红屏(red box)报错。你可以使用`console.error()`来手动触发红屏错误。 ### 黄屏警告 应用内的警告会以全屏黄色显示在应用中(调试模式下),我们称为黄屏(yellow box)报错。点击警告可以查看详情或是忽略掉。和红屏报警类似,你可以使用`console.warn()`来手动触发黄屏警告。
这2个全屏提示就是 React Native 对RN应用异常的处理。
那么思路来了,我们只需要找到 RN 弹出红屏、黄屏的地方,并将之替换为我们自己的业务逻辑即可。
示意图如下:
OK,接下来我们需要从源码中去找到这个切入口,不要害怕源码,跟着我的思路,let's go!
在上述红屏图片中,我们通过 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)的原因。
与红屏报错原因不同,熟悉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
回调函数)很幸运,通过我们上述源码的分析,我们可以在错误边界中通过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 能带给我们更好的开发与使用体验!
最后,回答几个大家可能有的疑问:
onUnhandled
进行处理。原创分享不易,觉得对你有所帮助的话,欢迎点赞收藏。
转载需经本人同意,并附上思否原文链接。
谢谢!