在作React Native混合開發時,生產環境有時會遇到打開RN(即React Native簡稱)應用白屏、RN頁面內操做閃退到native頁面或者直接致使APP Crash的狀況。經過分析APP日誌,發現緣由能夠歸類爲如下兩種:html
對於第一點,能夠很快地經過log追蹤到出現問題的js代碼並解決,可是對於第二點,每每是框架底層代碼執行報錯阻塞了UI渲染,報錯日誌信息沒法定位出哪裏出了問題,如:java
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中接管異常處理權力來實現咱們本身的邏輯(相似 反轉控制反轉 思想)node
下面,將帶領你們一步步分析並實現。react
不論是何種緣由致使RN應用異常,在開發模式環境(在發佈版 release/production中都是自動禁用的),默認狀況下都會以紅屏(red box)或黃屏(yellow box)方式全屏提示:android
請注意此文中,報錯和警告,都視爲異常
紅屏:
黃屏:
es6
在官方描述中:web
### 紅屏錯誤 應用內的報錯會以全屏紅色顯示在應用中(調試模式下),咱們稱爲紅屏(red box)報錯。你可使用`console.error()`來手動觸發紅屏錯誤。 ### 黃屏警告 應用內的警告會以全屏黃色顯示在應用中(調試模式下),咱們稱爲黃屏(yellow box)報錯。點擊警告能夠查看詳情或是忽略掉。和紅屏報警相似,你可使用`console.warn()`來手動觸發黃屏警告。
這2個全屏提示就是 React Native 對RN應用異常的處理。
那麼思路來了,咱們只須要找到 RN 彈出紅屏、黃屏的地方,並將之替換爲咱們本身的業務邏輯便可。
示意圖以下:
react-native
OK,接下來咱們須要從源碼中去找到這個切入口,不要懼怕源碼,跟着個人思路,let's go!promise
在上述紅屏圖片中,咱們經過 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
進行處理。原創分享不易,以爲對你有所幫助的話,歡迎點贊收藏。轉載需經本人贊成,並附上思否原文連接。謝謝!