一、为什么要做Taro小程序的全局异常捕获和错误上报

做这个事情的初衷,其实是帮我们提前发现小程序运行时的隐藏问题。很多时候我们上线小程序后,只能从用户的反馈里知道哪里出错了,但用户不一定会主动来告诉你,比如用户用了某个小众的手机型号,点按钮没反应,自己都不知道是程序的问题,还是网的问题,然后就直接关掉小程序了,这对我们来说就是损失。

1.1 常见的小程序异常场景

小程序里的异常真的不少,举几个你肯定遇到过的:比如用户点“提交订单”按钮,页面突然就白了,啥也看不到,大概率是代码里某个变量没定义,导致运行出错;还有调用接口的时候,明明写了正确的地址,结果返回500,用户看了半天提示也不知道咋回事;还有自定义组件的问题,比如一个轮播图组件,用到了某个不存在的API,结果加载不出来,用户看到的就是空白的一片。

1.2 错误上报能帮我们解决什么问题

有了全局捕获和上报,你不用等用户来找你才能知道这些问题。比如你上线后,后台会收到所有错误的信息:哪个页面出的错,用户用的是什么手机,错误的具体内容是什么,甚至能看到代码里出错的行。这样你就能快速定位问题,比如发现某个错误在华为手机上发生率最高,那你就可以优先适配这个机型,还能在用户下次打开小程序的时候,自动修复问题,或者给用户提示出错的原因,不用让用户一头雾水。

二、全局异常捕获的具体实现

要做这个事情,其实Taro已经给我们提供了不少工具,不用从零开始写,只要把这些工具拼起来就行。

2.1 Taro自带的生命周期错误捕获

每个Taro小程序的入口文件(app.js)里,有一个自带的全局监听函数,叫onError,专门用来抓整个小程序的JS错误。不管是页面里的,还是组件里的,只要是JS运行时的错误,都会跑到这个函数里来。这个函数是第一步,也是最核心的,先把所有JS错误都收进来。

2.2 主动捕获组件和接口请求的异常

除了JS错误,还有很多是其他类型的错误,比如接口请求错了,或者某个组件渲染错了,这些得单独处理。比如接口请求,每个地方都写fail回调太麻烦,我们可以封装一个统一的请求方法,不管哪个地方用接口,都走这个封装好的方法,这样就能统一抓接口的错误;组件渲染错的话,React的类组件有个componentDidCatch钩子,能抓组件里的渲染错误,这样整个页面不会崩掉,只会把出错的组件替换成提示文字。

2.3 错误上报的核心逻辑

抓到错误后,我们要把错误信息整理好,然后发给后端的接口。整理的时候不用太复杂,只要把必要的信息带上:错误是什么类型,发生在哪个页面,用的什么设备,错误的具体内容,还有代码里的错误栈(方便找哪行出错了)。上报的时候用Taro自带的请求就行,不用自己写HTTP请求,而且要做容错,如果上报失败了,就把错误存在本地,等用户下次打开小程序的时候再发,不会影响用户的操作。

三、自定义错误上报体系的实践细节

光知道方法还不够,还要注意一些细节,不然写出来的东西要么不好用,要么会影响用户体验。

3.1 上报时机和信息的选择

上报时机很重要,不能用户点个按钮就上报一次,如果短时间内同一个错误发生很多次,会给后端带来压力,也会浪费用户的流量。我们可以加个简单的防抖,比如10分钟内同一个错误只上报一次;还有上报的信息,不要传用户的隐私,比如用户的手机号、昵称这些,只要传错误相关的信息就行,符合微信的隐私规范,也不会有风险。

3.2 完整的示例代码

这里用Taro 3.x + React的技术栈,把刚才说的方法都整合起来,写个完整的例子,每个部分都有注释,直接就能用。 首先是入口文件app.js,全局监听JS错误:

// 技术栈:Taro 3.x + React
import Taro, { Component } from '@tarojs/taro'
import { View } from '@tarojs/components'

export default class App extends Component {
  // 全局JS错误监听函数,所有运行时JS错误都会到这里
  onError(err) {
    // 整理错误信息,只保留必要内容
    const errorInfo = {
      type: 'JS_RUNTIME_ERROR', // 错误类型,方便分类
      message: err.message, // 错误的具体描述
      stack: err.stack, // 错误堆栈,定位代码位置
      currentPage: Taro.getCurrentPages().pop()?.route || '未知页面', // 出错的页面路径
      deviceInfo: Taro.getDeviceInfoSync() // 设备信息,比如品牌、系统版本
    }
    // 调用上报函数,把错误发给后端
    this.uploadError(errorInfo)
  }

  // 错误上报函数,封装好,统一处理
  uploadError(errorData) {
    // 调用后端的错误上报接口,替换成你自己的接口地址
    Taro.request({
      url: 'https://你的后端地址/api/miniapp/error',
      method: 'POST',
      data: errorData,
      timeout: 5000, // 5秒超时,避免等太久
      // 如果上报失败,就把错误存到本地,下次再发
      fail: () => {
        // 这里可以用Taro的缓存API存错误,下次启动再发
        const localErrors = Taro.getStorageSync('local_errors') || []
        localErrors.push(errorData)
        Taro.setStorageSync('local_errors', localErrors)
      }
    })
  }

  // 页面加载时,检查有没有本地存的错误,有的话上报
  componentDidMount() {
    const localErrors = Taro.getStorageSync('local_errors') || []
    if (localErrors.length > 0) {
      localErrors.forEach(err => this.uploadError(err))
      // 上报完清空本地的错误
      Taro.removeStorageSync('local_errors')
    }
  }

  config = {
    pages: ['pages/index/index'],
    window: {
      navigationBarTitleText: '示例小程序'
    }
  }

  render() {
    return <View>{this.props.children}</View>
  }
}

然后是封装的统一请求方法,用来捕获接口错误:

// 封装的全局请求方法,所有接口调用都用这个
export const request = (options) => {
  return Taro.request({
    ...options,
    // 接口返回成功(状态码200),但业务逻辑出错的情况
    success: (res) => {
      if (res.statusCode !== 200) {
        // 接口HTTP错误,上报
        const errorInfo = {
          type: 'API_HTTP_ERROR',
          message: `接口HTTP状态码:${res.statusCode}`,
          url: options.url,
          page: Taro.getCurrentPages().pop()?.route
        }
        getApp().uploadError(errorInfo)
        Taro.showToast({ title: '请求失败,请重试', icon: 'none' })
      } else if (res.data.code !== 0) {
        // 业务错误,比如接口返回code是1,说明业务出错
        const errorInfo = {
          type: 'API_BUSINESS_ERROR',
          message: res.data.msg || '业务处理出错',
          url: options.url,
          page: Taro.getCurrentPages().pop()?.route,
          businessCode: res.data.code
        }
        getApp().uploadError(errorInfo)
        Taro.showToast({ title: res.data.msg, icon: 'none' })
      }
      return res.data
    },
    // 网络请求失败,比如超时、断网
    fail: (err) => {
      const errorInfo = {
        type: 'API_NETWORK_ERROR',
        message: err.errMsg || '网络请求失败',
        url: options.url,
        page: Taro.getCurrentPages().pop()?.route
      }
      getApp().uploadError(errorInfo)
      Taro.showToast({ title: '网络不给力,请检查网络', icon: 'none' })
    }
  })
}

还有组件里的错误捕获,比如一个自定义的商品组件:

import { Component } from '@tarojs/taro'
import { View, Image, Text } from '@tarojs/components'

export default class GoodsComponent extends Component {
  state = {
    hasError: false, // 标记组件是否出错
    errorMsg: '' // 出错提示
  }

  // 捕获组件渲染时的错误,比如用到了不存在的商品数据
  componentDidCatch(error) {
    this.setState({
      hasError: true,
      errorMsg: '商品组件加载出错'
    })
    // 上报组件渲染错误
    const errorInfo = {
      type: 'COMPONENT_RENDER_ERROR',
      message: error.message,
      component: 'GoodsComponent',
      page: Taro.getCurrentPages().pop()?.route
    }
    getApp().uploadError(errorInfo)
  }

  render() {
    // 出错时显示替代内容,不会影响整个页面
    if (this.state.hasError) {
      return <View className="goods-item">{this.state.errorMsg}</View>
    }
    // 正常渲染的商品内容,比如用到了this.props里的商品数据
    const goods = this.props.goods
    return (
      <View className="goods-item">
        <Image src={goods.image} mode="aspectFit" />
        <Text>{goods.name}</Text>
      </View>
    )
  }
}

四、应用场景与技术考量

做好这个上报体系,能覆盖很多日常的开发和运营场景,但也要知道它的优缺点和注意事项。

4.1 适用的应用场景

最常用的就是小程序的核心流程,比如电商的订单提交、支付环节,这些地方出问题会直接影响转化,上报后能快速修复;还有工具类小程序,比如笔记、待办,用户保存数据时出错,上报后能知道是哪里的问题;还有内容类小程序,比如文章页面,渲染时出错,用户看不到内容,上报后能及时修复。

4.2 技术优缺点分析

优点很明显:不用等用户反馈,实时掌握小程序的运行状态;能统计错误的发生率,比如某个错误占了总错误的多少,优先级高;可以区分错误类型,比如致命错误和非致命错误,分别处理。缺点的话:会增加一点代码量,还有上报请求的性能损耗,比如如果上报的请求和页面加载同时发生,可能会慢一点;如果上报逻辑写得不好,比如存太多本地错误,会占用户的缓存空间;还有如果上报的接口出问题了,错误就收不到了,所以最好做降级处理,比如用本地缓存重试。

4.3 开发时的注意事项

开发的时候要注意,不要在上报函数里做太复杂的操作,比如不要调用其他耗时的接口,不然会影响上报的速度;要测试不同的错误场景,比如自己写个不存在的变量,看能不能触发onError;要区分错误级别,比如致命错误(页面白屏)要优先处理,非致命错误(小组件出错)可以缓一缓;还要注意不同平台的差异,比如微信小程序和支付宝小程序的上报接口可能不一样,Taro要兼容这些差异。

五、总结与最佳策略

总结下来,搭建Taro小程序的全局异常捕获和错误上报体系,核心就是“多抓错误、少占资源、安全上报”。最佳策略是:用Taro的onError抓全局JS错误,用统一请求封装抓接口错误,用componentDidCatch抓组件错误;上报时加防抖,避免重复上报;信息精简,只保留必要内容;上报失败时存本地重试,不影响用户体验。这个体系不用很复杂,只要把这几个部分做好,就能帮你快速定位和解决小程序的问题,提升用户的使用体验,减少用户的流失。