当前位置: 首页 > news >正文

HarmonyOS7 全局异常怎么兜底才靠谱?错误处理和降级架构这样搭

文章目录

    • 前言
    • 鸿蒙应用的错误分类
    • 全局错误捕获
    • 错误降级策略:三层兜底
    • 错误码 → 用户可读文案
    • ErrorBoundary 组件:错误隔离 + 降级 UI
    • 错误上报
    • 一些心得

前言

你有没有遇到过这种情况:用户在某个页面点了个按钮,接口超时了,页面直接白屏,然后应用闪退。查日志发现是一个未捕获的 Promise rejection。

这类问题在鸿蒙应用里也很常见。错误处理如果只是到处写 try-catch,不仅代码臃肿,还容易漏掉。这篇文章我来搭一套全局错误处理架构,从捕获、分类、降级到上报,一条龙解决。

鸿蒙应用的错误分类

先理清楚错误有哪些类型,不同类型处理方式不一样:

  • 同步错误:代码逻辑出错,比如空指针、数组越界。这类最致命,通常会导致页面崩溃。

  • 异步错误:Promise rejection 没被 catch,或者 async 函数里抛了异常。
  • 网络错误:超时、断网、服务端 5xx。这类最常见,也是最能优化体验的地方。
  • 系统错误:权限被拒、存储空间不足、传感器不可用。这类需要引导用户去设置。

全局错误捕获

鸿蒙提供了globalThis上的错误监听能力,我在应用启动时就注册好:

// entry/src/main/ets/entryability/EntryAbility.tsimport{ErrorHandler}from'../common/ErrorHandler';exportdefaultclassEntryAbilityextendsUIAbility{onCreate(want:Want,launchParam:AbilityConstant.LaunchParam):void{// 注册全局错误捕获ErrorHandler.setup();// 初始化日志、网络层等Logger.info('App','应用启动');}}

ErrorHandler是核心类,负责注册各类错误监听并分发处理:

import{ErrorReporter}from'./ErrorReporter';import{ErrorMessageMapper}from'./ErrorMessageMapper';exportinterfaceAppError{type:'sync'|'async'|'network'|'system';code:number;message:string;stack?:string;timestamp:number;pageName?:string;}exportclassErrorHandler{privatestatichandlers:Array<(error:AppError)=>void>=[];staticsetup():void{// 捕获未处理的同步错误globalThis.onerror=(msg:string,source:string,lineno:number,colno:number,error:Error)=>{constappError:AppError={type:'sync',code:-1,message:msg,stack:error?.stack,timestamp:Date.now()};ErrorHandler.dispatch(appError);};// 捕获未处理的 Promise rejectionglobalThis.onunhandledrejection=(event:PromiseRejectionEvent)=>{constappError:AppError={type:'async',code:-2,message:String(event.reason),stack:event.reason?.stack,timestamp:Date.now()};ErrorHandler.dispatch(appError);// 阻止默认行为,防止应用崩溃event.preventDefault?.();};Logger.info('ErrorHandler','全局错误捕获已注册');}// 注册错误处理回调staticaddHandler(handler:(error:AppError)=>void):void{this.handlers.push(handler);}// 分发错误到所有处理器privatestaticdispatch(error:AppError):void{// 先上报ErrorReporter.report(error);// 再通知所有注册的处理器for(consthandlerofthis.handlers){try{handler(error);}catch(e){console.error('错误处理器自身出错',e);}}}// 手动上报错误(用于业务层主动上报)staticreport(code:number,message:string,type:AppError['type']='sync'):void{constappError:AppError={type,code,message,timestamp:Date.now()};this.dispatch(appError);}}

错误降级策略:三层兜底

核心思路是接口失败 → 缓存兜底 → 兜底 UI,尽量不让用户看到白屏。

exporttypeDataState<T>=|{status:'loading'}|{status:'success';data:T}|{status:'cache';data:T;stale:boolean}|{status:'error';error:AppError};exportclassDataFetcher<T>{privatecacheKey:string;constructor(cacheKey:string){this.cacheKey=cacheKey;}asyncfetch(requestFn:()=>Promise<T>,options?:{useCache?:boolean;cacheTTL?:number}):Promise<DataState<T>>{try{constdata=awaitrequestFn();// 请求成功,同时更新缓存if(options?.useCache!==false){awaitthis.saveCache(data);}return{status:'success',data};}catch(error){// 请求失败,尝试缓存兜底if(options?.useCache!==false){constcached=awaitthis.loadCache();if(cached){Logger.warn('DataFetcher',`${this.cacheKey}接口失败,使用缓存兜底`);return{status:'cache',data:cached.data,stale:true};}}// 缓存也没有,返回错误状态return{status:'error',error:errorasAppError};}}privateasyncsaveCache(data:T):Promise<void>{constcontext=getContext()ascommon.UIAbilityContext;constprefs=awaitpreferences.getPreferences(context,'data_cache');awaitprefs.put(this.cacheKey,JSON.stringify({data,timestamp:Date.now()}));awaitprefs.flush();}privateasyncloadCache():Promise<{data:T;stale:boolean}|null>{try{constcontext=getContext()ascommon.UIAbilityContext;constprefs=awaitpreferences.getPreferences(context,'data_cache');constraw=prefs.getSync(this.cacheKey,'')asstring;if(!raw)returnnull;constparsed=JSON.parse(raw);return{data:parsed.dataasT,stale:true};}catch{returnnull;}}}

错误码 → 用户可读文案

直接给用户看 “Error code: 40001” 没有任何意义。搞一个映射表,把错误码翻译成人话:

exportclassErrorMessageMapper{privatestaticmessages:Map<number,string>=newMap([[10001,'网络连接失败,请检查网络设置'],[10002,'请求超时,请稍后重试'],[10003,'服务器开小差了,请稍后再试'],[20001,'登录已过期,请重新登录'],[20002,'账号在其他设备登录'],[20003,'账号已被禁用'],[30001,'内容不存在或已被删除'],[30002,'没有权限执行此操作'],[40001,'存储空间不足,请清理后重试'],[40002,'相机权限未开启,请在设置中允许'],]);staticgetMessage(code:number):string{returnthis.messages.get(code)??'出了点问题,请稍后重试';}// 注册新的错误码映射staticregister(code:number,message:string):void{this.messages.set(code,message);}}

ErrorBoundary 组件:错误隔离 + 降级 UI

借鉴 React 的 ErrorBoundary 思路,我实现了一个 ArkUI 版本的错误边界组件。核心是用@State控制显示状态,出错了就展示兜底 UI,不影响其他模块。

@Componentexportstruct ErrorBoundary{@StatehasError:boolean=false;@StateerrorMessage:string='';@StateerrorDetail:string='';@PropfallbackText:string='加载失败';onRetry?:()=>void;@BuilderParamcontent:()=>void;aboutToAppear():void{ErrorHandler.addHandler((error:AppError)=>{// 可以根据 pageName 判断是不是当前区域的错误this.hasError=true;this.errorMessage=ErrorMessageMapper.getMessage(error.code);this.errorDetail=error.message;});}build(){if(this.hasError){Column(){Image($r('app.media.ic_error')).width(80).height(80).margin({bottom:16})Text(this.errorMessage).fontSize(16).fontColor('#333333').margin({bottom:8})Text('点击重试').fontSize(14).fontColor('#007DFF').onClick(()=>{this.hasError=false;this.onRetry?.();})}.width('100%').height('100%').justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)}else{Column(){this.content()}}}}

使用方式很简单,把可能出错的区域包起来:

@Entry@Componentstruct ProductPage{@Stateproducts:Product[]=[];build(){Column(){// 头部不参与错误隔离HeaderBar({title:'商品列表'})// 列表区域用 ErrorBoundary 包裹ErrorBoundary({onRetry:()=>this.loadProducts(),content:()=>{List(){ForEach(this.products,(item:Product)=>{ListItem(){ProductCard({product:item})}})}}})}}asyncloadProducts(){constfetcher=newDataFetcher<Product[]>('products');constresult=awaitfetcher.fetch(()=>httpService.get<Product[]>('/products'),{useCache:true});switch(result.status){case'success':case'cache':this.products=result.data;if(result.status==='cache'){// 提示用户数据可能不是最新的promptAction.showToast({message:'当前显示离线数据'});}break;case'error':ErrorHandler.report(result.error.code,result.error.message,'network');break;}}}

错误上报

最后别忘了把错误发到服务端,方便排查问题。上报的时候带上设备信息、页面路径、用户 ID 这些上下文:

exportclassErrorReporter{privatestaticreportUrl:string='https://log.example.com/report';privatestaticqueue:AppError[]=[];privatestaticbatchSize:number=10;staticreport(error:AppError):void{this.queue.push(error);if(this.queue.length>=this.batchSize){this.flush();}}staticasyncflush():Promise<void>{if(this.queue.length===0)return;constbatch=this.queue.splice(0);constdeviceInfo=device.getCurrent();constreportData={errors:batch,device:deviceInfo.productModel,osVersion:deviceInfo.osFullName,appVersion:'1.0.0',timestamp:Date.now()};try{// 上报请求本身不能再触发错误上报,否则会死循环awaithttp.createHttp().request(this.reportUrl,{method:http.RequestMethod.POST,extraData:reportData,connectTimeout:5000,});}catch{// 上报失败就丢掉,不能影响主流程}}}

一些心得

搞完这套错误处理架构之后,我的项目稳定性提升了很多。几个关键点:

降级比报错重要。用户不关心你的接口为什么挂了,他们只想知道还能不能用。缓存兜底 + 提示"离线数据",体验比白屏好太多了。

错误码映射表要维护好。每加一个新接口就检查一下错误码是否有对应文案,别让用户看到 “undefined” 或 “Error: null”。

错误上报别阻塞主线程。用队列批量上报,失败了就静默丢弃。上报请求本身绝对不能触发二次上报,不然一个网络抖动就能把日志打爆。

http://www.gsyq.cn/news/1608430.html

相关文章:

  • 从零到一:将OpenHarmony轻量内核移植到STM32F407的实践指南
  • Flux、Mono、Reactor 核心操作符与高阶应用场景深度解析
  • 参考文献格式乱如麻?博导推荐这几个AI论文工具
  • Python实战:基于skimage的灰度共生矩阵(GLCM)纹理特征分析与应用
  • 陶瓷卫浴整厂输送线怎么规划合理?4 个核心设计要点与避坑指南
  • Flink on K8s:云原生架构部署分析
  • 2026 AI营销机构选型指南:本土服务商塔米德数智科技的价值与路径
  • SLO2016光耦与TM4C129ENCPDT微控制器的工业通信方案
  • CAPL脚本中整型数组与Hex字符串互转的实战技巧与性能优化
  • 【S32K3实战指南】巧用FlexCAN FIFO Filters实现多ID精准接收
  • 项目文档骨架生成器
  • 云南历史类455-515分各分数段怎么填?云南工商学院从征集到稳妥都值得关注
  • 终极音乐解放:3分钟掌握ncmdumpGUI,永久解锁网易云音乐加密文件
  • 从拒稿到录用:我的IEEE TII投稿实战复盘与避坑指南
  • 《重启日记》第十四周|主业忙碌,更新放缓:流量起伏无碍长期沉淀
  • 【银河麒麟V10】vsFTPd服务实战:从零部署到安全加固全攻略
  • d2s-editor:重新定义暗黑破坏神2存档编辑体验的开源工具
  • AI正在变成特权,你还配用吗 - 微元算力(weytoken)
  • 免费开源项目文档:基于HSV颜色空间和形态学特征的火灾与烟雾智能检测系统
  • Python实战:打造阴阳师御魂副本智能挂机脚本,兼顾效率与防检测
  • Python 多源行情数据冲突排查:symbol、timestamp、字段口径和原始返回校验
  • 龙口让人放心防水公司特点
  • openEuler HPC Runner性能优化秘籍:提升HPC应用运行效率的10个技巧
  • 暗黑破坏神2存档编辑器终极指南:零基础学会角色自定义
  • 在Carla 0.9.14 Windows环境下构建自定义多轴车辆:从Blender建模到UE4蓝图部署
  • STM32CubeMX实战:PWM波形生成与动态调光应用
  • 电商OAuth2.0授权码泄露漏洞自动化渗透测试与防御实战
  • 电子保函办理条件与流程详解:新手也能快速上手
  • Codex桌面自动化:PPT生成与文件整理的零代码工作流
  • 个人项目 UI 没配图?用 Pexels API + Claude Code 一键搞定