一、为什么要做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抓组件错误;上报时加防抖,避免重复上报;信息精简,只保留必要内容;上报失败时存本地重试,不影响用户体验。这个体系不用很复杂,只要把这几个部分做好,就能帮你快速定位和解决小程序的问题,提升用户的使用体验,减少用户的流失。
Comments