React-Native错误监控方案

React-Native 是一个基于 React 框架开发的跨平台开发框架,在此框架上业务代码基本上采用 js 进行编写,但是 js 与其他原生开发框架的开发语言如 java、oc 都不同,它的错误不好捕捉,如果线上出现问题也比较难发现,所以需要有一个 js 错误监控方案来帮助我们监控错误以及定位问题,提高产品质量。

1. JS 空指针错误

众所周知,采用原生框架开发的 app,一般 Android 是采用 Java/Kotlin,iOS 是采用 Objective-c/Swift,当代码中出现错误可能会直接 crash,也就是俗称的闪退。通常造成闪退的原因除了内存溢出这种之外,几乎绝大多数都是空指针异常造成的。但是在 rn 框架中,如果 js 代码出现空指针异常,是不会造成闪退的,在 debug 模式下,会出现我们常见的红屏现象,我们举个例子:

在某个组件的 render() 方法中,特意加 2 行错误的代码,如下所示:

let a = null
let b = a.b;

这是一个很明显的空指针异常,运行后会出现如下错误:


平时我们在开发时,都是 debug 模式,框架为了更方便我们调试,不会闪退掉,而是通过这种方式告知开发者错误在哪里,以变我们更好地调试。在原生框架里,我们有很多现成的 carsh 错误监控方案,但是这种 js 里的报错,并不会传到底层引起闪退,所以需要我们专门针对这种错误进行监控。

2. RN 白屏现象

前面介绍到,我们在 debug 模式下开发时,如果出现 js 空指针异常,只会出现红屏错误现象 。但是当我们 app 正式发布时,需要打的是 release 包,这个时候如果运行还有空指针异常,那么框架并不会像前面那样,弹出一个红屏错误页面,而是直接会出现白屏,白屏是因为组件树渲染失败,啥都没有了。当然不同的 rn 版本对这种错误处理方式可能不一样,有的错误可能会出现一个弹窗信息,弹窗里展示错误堆栈信息。

总之,出现了白屏现象的话,对用户是非常不友好的,既没有闪退也没有任何提示,所以我们要避免这种情况发生。

3. JS 错误监控

A JavaScript error in a part of the UI shouldn’t break the whole app. To solve this problem for React users, React 16 introduces a new concept of an “error boundary”.
Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed. Error boundaries catch errors during rendering, in lifecycle methods, and in constructors of the whole tree below them.

这段话的意思就是说,从 React 16 开始,React 框架提供了一种叫做错误边界(Error Boundary)的技术,可以用于捕捉组件的异常,具体文章可以参考:https://reactjs.org/blog/2017/07/26/error-handling-in-react-16.html。

在 rn 中,一般这样定义一个错误边界组件:

import React, { Component } from 'react'
import { View, Text } from 'react-native'

export default class ErrorBoundary extends Component {

    constructor(props) {
        super(props)
        this.state = {
            hasError: false,
            errorMsg: null,
            errorStack: null
        }
    }
    
    //注意这是一个静态方法
    static getDerivedStateFromError(error) {
        return {
            hasError: true,
            errorMsg: error ? error.message : null,
            errorStack: error ? error.stack : null
        }
    }

    componentDidCatch(error, errorInfo) {
        //与getDerivedStateFromError() 雷同
        //可以在这里将错误堆栈信息收集以后,上报到后端处理
    }

    render() {
        return this.state.hasError ? (
            
                
                    {this.state.errorMsg}
                    {this.state.errorStack}
                
            
        ) : this.props.children
    }
}

使用时,需要将错误边界组件包在具体的业务组件外面,如下所示:


    

ErrorBoundary 组件主要有 2 个方法 getDerivedStateFromErrorcomponentDidCatch,任何组件都可以定义,一个组件只要定义了 componentDidCatch(error, info)方法,那么该组件就成为一个错误边界了。当子组件有错误发生,例如上面代码所示中 HomePage 组件中出现 js 空指针异常时,该异常能够在它最近的父 ErrorBoundary 组件中捕获到。什么意思呢,看下面这个例子:


    
        
    
    

HomePage组件中的错误,在 ErrorBoundary2 中能捕获到,并不会传递到 ErrorBoundary1 中,只有 MinePage 中的错误,才能被 ErrorBoundary1 捕获。

通常我们可以在根组件外面包一层 ErrorBoundary,然后给一些友好的提示。但是我们并不想任意一个小错误就导致全局页面渲染失败,所以可以考虑在一些子组件外包一层 ErrorBoundary,这样可以限定错误的影响范围,将对用户的影响降到最小。

4. 异常分析方案

4.1 JS 错误堆栈信息

我们得到 js 的错误堆栈信息之后,需要对其进行分析,才能找到具体出错的代码行才能解决根本问题,但不幸的是 release 模式下打包时,会对 js 进行压缩混淆。一个错误堆栈范例如下所示:

This error is located at:
    in s
    in inject-s
    in s
    in RCTView
    in u
    in n
    in E
    in RCTView
    in RCTView
    in n
    in L
    in RCTView
    in h
    in v
    in k
    in T
    in P
    in RCTView
    in Portal.Host
    in Unknown
    in p
    in _
    in n
    in n
    in n
    in inject-n
    in RCTView
    in s
    in RCTView
    in RCTView
    in Unknown
    in RCTView
    in [email protected]:1229:11360
value@[native code]
[email protected]:680:54481
index.android.bundle:717:8552
[email protected]:680:46688
[email protected]:680:49303
[email protected]:717:8472
[email protected]:91:48721
[email protected]:91:72881
[email protected]:91:73371
[email protected]:91:80972
[email protected]:91:80310
[email protected]:91:79323
[email protected]:91:78013
[email protected]:91:29425
[email protected]:61:2074
[email protected]:389:1220
[email protected]:368:7969
index.android.bundle:368:3384
[email protected]:49:1280
[email protected]:28:3311
index.android.bundle:28:822
[email protected]:28:2565
[email protected]:28:794
value@[native code]

APP进行打包时,会将所有 js 文件压缩混淆后,合成在一个 js bundle 文件里,这里的错误信息反映的是出错代码行在这个 js bundle 文件里的位置。上面这个例子中,出错代码行数为:[email protected]:1229:11360,也就是说在 index.android.bundle 文件中的 1229 行 11360 列出错了。

熟悉前端 React、Vue 框架的都清楚,js 打包压缩混淆的时候,可以生成一个 mapping 文件,Android打包 apk 开启混淆之后也会生成一个 mapping 文件,这个 mapping 文件其实就是混淆代码与源文件的一个映射文件,也就是说通过 mapping 文件以及混淆后代码的行列号,我们可以反推出该代码在源文件的行列号,所以我们在打 release 包时也必须找到对应的 mapping 文件。

4.2 如何输出混淆压缩后的 mapping 文件

React-Native 将 js 文件打包成 js bundle 文件的命令为:

react-native bundle --platform 平台 --entry-file 启动文件 --bundle-output 打包js输出文件 --assets-dest 资源输出目录 --dev 是否调试 --sourcemap-output mapping文件输出路径

例如在 Android 中,我们可以这样单独输出 js bundle 文件:

react-native bundle --platform android --entry-file index.android.js --bundle-output ./jsbundle/index.android.bundle --assets-dest ./jsbundle --sourcemap-output ./jsbundle/android-mapping.txt

这样在 jsbundle 目录里可以输出打包好的 js bundle 文件以及资源文件,还有我没需要的 mapping 文件。但遗憾的是我们在 rn 工程当中,默认的打包脚本里是没有 --sourcemap-output 这个参数的,也就是说生成 release 包的时候,默认是不会输出 mapping 文件的,那么这就需要我们修改原来的打包脚本。

对于 Android ,在 node_module/react-native/react.gradle 中修改脚本,增加 --sourcemap-output 参数:

commandLine(*nodeExecutableAndArgs, cliPath, bundleCommand, "--platform", "android", "--dev", "${devEnabled}",
                    "--reset-cache", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir,
                    "--sourcemap-output", file("$buildDir/../../android_outputs/android-release.bundle.map"), *extraArgs)

针对 iOS,在 node_modules/scripts/react-native-xcode.sh 中,增加 --sourcemap-output 参数:

"$NODE_BINARY" $NODE_ARGS "$CLI_PATH" $BUNDLE_COMMAND \
  $CONFIG_ARG \
  --entry-file "$ENTRY_FILE" \
  --platform ios \
  --dev $DEV \
  --reset-cache \
  --bundle-output "$BUNDLE_FILE" \
  --assets-dest "$DEST" \
  --sourcemap-output "ios-release.bundle.map" \
  $EXTRA_PACKAGER_ARGS

最终,在我们打包时,能够分别将 iOS、Android 打包时生产的混淆压缩 mapping 文件输出到指定的目录,后面我们会基于这个文件来分析错误堆栈信息。

4.3 解析 mapping 文件

解析 mapping 文件,可以采用一个叫做 souce-map 的库,它是运行在 node 环境下的,大概代码如下:

var sourceMap = require('source-map');
var fs = require('fs');

var line = 1229         //混淆代码里的行号
var column = 11360  //混淆代码里的列号
var mappingFile = __dirname + '/release.bundle.map'  //mapping文件

fs.readFile(mappingFile, 'utf8', function (err, data) {
    var smc = new sourceMap.SourceMapConsumer(data);
    smc.then(map => {
        console.log(map.originalPositionFor({
            line: line,
            column: column
        }))
    }).catch(err => {
        console.log(err)
    })
});

设置好 mapping 文件、出错行列号,在 node 环境中执行,即可解析出该错误在我们项目中的所在的 js 类文件,以及在源文件中的具体行列号。

如下图所示,可以输出 source、line、column、name 能信息:


5. 小结

本文主要介绍了 rn 当中 js 的错误监控方案,主要总结一下:

  1. React-Native 不同的编译模式,使用的 JS 来源是不一样的,debug 模式来自 package server,release 模式来自包里打好的 js bundle 文件;
  2. debug 模式下 js 报错会触发红屏,而 release 模式下则会出现白屏现象或者闪退,看情况而定;
  3. debug 模式下红屏错误信息可以显示出错行列号,是因为编译的 js 文件中包含了对应的源文件和代码行号,在 release 模式下默认是没有的;
  4. release 模式下的错误信息是被混淆后的,不能很清楚地定位出错代码以及源文件;
  5. 编译 js 时增加 --sourcemap-output 参数,可以输出混淆的 mapping 文件;
  6. 解析 mapping 文件,可以使用 source-map 这个 node 库来解析,能够精准定位到出错的源文件以及代码行号;

为了更方便地使用,我这里封装成一个 shell 脚本,具体脚本源代码请参考:使用 source-map 解析 mapping 文件。

你可能感兴趣的:(React-Native错误监控方案)