news 2026/9/30 2:50:12

ArkWeb 手记 01|把 H5 加载和生命周期管起来

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ArkWeb 手记 01|把 H5 加载和生命周期管起来

HarmonyOS 7 · ArkWeb 混合应用开发手记 01

做混合应用时,很多人第一次接 ArkWeb,代码大概都是这样:

Web({src:'https://example.com',controller:this.controller})

页面能开,任务似乎就完成了。

但只要项目真的开始跑,你很快会碰到一串问题:首屏一直转圈,加载失败后白屏,返回页面又重新请求,H5明明已经打开了,原生侧还在显示 Loading;更麻烦的是,有些逻辑放在aboutToAppear里能跑,换到另一个页面就不稳定。

这时候问题通常不在"H5 能不能加载",而在于:我们根本没有把 Web
页的加载过程和 ArkUI 页面的生命周期分清楚。

这篇就从这里开始。

1. 先别急着封装,先搞清楚两套生命周期

一个 ArkWeb 页面里,其实同时存在两套节奏。

第一套是 ArkUI 页面自己的生命周期。比如组件什么时候出现、什么时候离开。

第二套是 Web 内容的加载生命周期。比如 Web控制器什么时候真正挂上组件、页面什么时候开始加载、什么时候结束、什么时候收到错误。

这两套东西有关联,但不是一回事。最容易踩的坑,就是把它们硬绑在一起。

比如:

aboutToAppear():void{this.loading=true}

这段代码没错,但它只能说明 ArkUI 组件准备出现了,并不能说明某个 H5 URL此刻真的开始加载。

如果用户在当前 Web 组件里从 A 页面跳到 B 页面,aboutToAppear
不一定重新执行,但 Web 页面已经开始了新一轮加载。所以 Loading 状态如果只靠aboutToAppear控制,迟早会乱。

更稳的思路是:ArkUI 页面生命周期负责页面级资源和状态;Web
组件自己的加载回调负责 URL加载、进度、成功、失败和重试。职责拆开以后,很多"偶现问题"会直接消失。

2. WebviewController 不是装饰品

先从最小页面开始。

import{webview}from'@kit.ArkWeb'@Entry@Componentstruct WebPage{privatecontroller:webview.WebviewController=newwebview.WebviewController()build(){Column(){Web({src:'https://example.com',controller:this.controller}).width('100%').height('100%')}}}

这里真正值得注意的不是src,而是controller。

WebviewController可以理解成原生侧握住 Web页面的"方向盘"。后面刷新、加载新地址,以及其他针对 Web页面的控制动作,都会围绕它展开。

所以我不建议在业务代码里到处临时创建 controller。一个 Web容器对应一个清晰的 controller,后续排查问题会轻松很多。

例如做一个最简单的刷新:

Button('重新加载').onClick(()=>{this.controller.refresh()})

或者切换地址:

Button('打开帮助页').onClick(()=>{this.controller.loadUrl('https://example.com/help')})

这比通过修改一堆外部状态"逼着 Web 重建"更直接。

但这里还有一个关键点:controller 创建出来,不等于它已经可以随便操作Web。

3.onControllerAttached:先确认方向盘真的装上了

ArkWeb 提供了onControllerAttached。它很适合处理那些"必须等 controller和 Web 组件建立关联以后再做"的事情。

Web({src:this.url,controller:this.controller}).onControllerAttached(()=>{console.info('[ArkWeb] controller attached')})

我在项目里通常会把这个节点单独记日志。

原因很简单:如果以后碰到"controller明明有值,调用却不符合预期"的问题,你至少能先判断控制器有没有完成挂载,而不是对着URL 和网络抓瞎。

可以再加一个状态:

@StatecontrollerReady:boolean=false.onControllerAttached(()=>{this.controllerReady=true})

然后对依赖 controller 的主动操作做保护:

privatereloadPage():void{if(!this.controllerReady){console.warn('[ArkWeb] controller is not ready')return}this.controller.refresh()}

别嫌这个判断啰嗦。混合页面最烦的就是"十次有九次正常"。把时序条件写清楚,比靠运气稳定得多。

4.onPageBegin和onPageEnd才适合管 Loading

接下来处理最常见的加载状态。

@Stateloading:boolean=false@StatecurrentUrl:string=''Web({src:this.url,controller:this.controller}).onPageBegin((event)=>{this.loading=truethis.currentUrl=event?.url??''console.info(`[ArkWeb] begin:${this.currentUrl}`)}).onPageEnd((event)=>{this.loading=falseconsole.info(`[ArkWeb] end:${event?.url??''}`)})

思路很直接:onPageBegin到了,说明一次页面加载开始了,onPageEnd到了,把 Loading 收掉。

然后 UI 层只负责展示状态:

Stack(){Web({src:this.url,controller:this.controller})if(this.loading){Column(){LoadingProgress().width(32).height(32)Text('页面加载中…').fontSize(14).margin({top:12})}.width('100%').height('100%').justifyContent(FlexAlign.Center)}}

这样做最大的好处,是 Loading 跟着 Web的实际加载走,而不是跟着"我猜它应该开始加载了"走。

不过要提醒一句:onPageEnd适合当作页面加载阶段的重要结束信号,但业务上所谓的"页面完全可用"可能还有自己的条件。

比如 H5 首屏出来以后,还要异步拉接口、渲染列表。那就不能简单地把onPageEnd当成"业务页面全部准备完毕"。原生加载状态和 H5
业务状态,最好继续分开。

5. 别只处理成功,白屏往往是错误态没接住

很多 Demo 到onPageEnd就结束了,线上项目不能这么写。

网络断开、域名异常、超时、资源错误,都可能让用户看到一个很尴尬的页面。最差的处理方式就是:Loading一直转,或者直接留一块白色区域。

我们加一个错误状态。

@Stateloading:boolean=false@StateloadError:boolean=falseprivatestartLoading():void{this.loading=truethis.loadError=false}privatefinishLoading():void{this.loading=false}

再接到 Web 上:

Web({src:this.url,controller:this.controller}).onPageBegin(()=>{this.startLoading()}).onPageEnd(()=>{this.finishLoading()}).onErrorReceive((event)=>{this.loading=falsethis.loadError=trueconstmessage=event.error.getErrorInfo()console.error(`[ArkWeb] load error:${message}`)})

UI 上别搞太复杂,一个错误提示加重试按钮就够用:

if(this.loadError){Column({space:12}){Text('页面暂时打不开').fontSize(18)Text('检查网络后再试一次').fontSize(14)Button('重新加载').onClick(()=>{this.loadError=falsethis.controller.refresh()})}.width('100%').height('100%').justifyContent(FlexAlign.Center)}

注意顺序。重试时先把错误层收掉,再调用refresh()。新的加载开始后,onPageBegin会再次接管Loading。这样状态流是闭环的,不需要在按钮里复制一堆加载逻辑。

6. 再加一个超时兜底,别让用户无限等

真实网络环境没那么讲武德。

有时请求没有马上给你一个漂亮的失败结果,但用户已经等了十几秒。技术上还在"加载",体验上其实已经"挂了"。

可以加一个简单的超时兜底。

privateloadTimer:number=-1privatestartLoadTimer():void{this.clearLoadTimer()this.loadTimer=setTimeout(()=>{this.loading=falsethis.loadError=trueconsole.error('[ArkWeb] load timeout')},15000)}privateclearLoadTimer():void{if(this.loadTimer!==-1){clearTimeout(this.loadTimer)this.loadTimer=-1}}

然后把它放进加载节点:

.onPageBegin(()=>{this.loading=truethis.loadError=falsethis.startLoadTimer()}).onPageEnd(()=>{this.clearLoadTimer()this.loading=false}).onErrorReceive(()=>{this.clearLoadTimer()this.loading=falsethis.loadError=true})

15秒不是标准答案。你的页面如果是公司内网、弱网业务或者首屏资源特别重,这个值要按真实数据调整。这里的重点不是"必须15 秒",而是一定要有退出机制。

否则 LoadingProgress 转得再丝滑,也只是一个精致的死循环。

7. 页面离开以后,定时器和临时状态要收

前面说了,ArkUI 生命周期和 Web 加载生命周期不要混在一起。但这不代表ArkUI 生命周期没用了。

组件离开时,正适合处理页面级资源。

aboutToDisappear():void{this.clearLoadTimer()}

如果页面里还有事件监听、业务侧回调、临时资源,也应该在对应的生命周期里解除。

这里有个很实用的判断方法:它是"跟着一次 URL 加载"的,还是"跟着这个ArkUI 页面实例"的?

跟 URL 加载走的,放到 Web 加载回调附近。跟页面实例走的,放到 ArkUI生命周期附近。一旦按这个标准拆,代码会清楚很多。

8. 我更喜欢把加载状态收成一个小状态机

写到这里,你会发现几个布尔值很容易互相打架。

比如loading = true和loadError = true理论上可以同时出现,但 UI到底显示 Loading还是错误页?项目再复杂一点,还会有空页面、登录过期、离线页。布尔值越堆越多,组合就越离谱。

所以实际项目里,我更喜欢直接定义状态:

enumWebLoadState{Idle,Loading,Success,Error}@StatewebState:WebLoadState=WebLoadState.Idle

生命周期里只切状态:

.onPageBegin(()=>{this.webState=WebLoadState.Loading}).onPageEnd(()=>{this.webState=WebLoadState.Success}).onErrorReceive(()=>{this.webState=WebLoadState.Error})

UI 也跟着状态走:

if(this.webState===WebLoadState.Loading){LoadingProgress()}if(this.webState===WebLoadState.Error){Button('重新加载').onClick(()=>this.controller.refresh())}

这就是一个很小的状态机。听起来像个"大词",其实一点都不玄学:一个时刻只允许页面处于一种明确状态。

9. 给日志加统一前缀,排错效率差很多

混合应用还有一个典型痛点:日志太杂。ArkTS 一份,H5一份,网络一份。真出问题的时候,控制台滚得像弹幕。

最简单的改进,是先统一前缀:

privatelog(stage:string,message:string=''):void{console.info(`[ArkWeb][${stage}]${message}`)}

使用时:

.onControllerAttached(()=>this.log('ATTACHED')).onPageBegin((event)=>this.log('BEGIN',event?.url??'')).onPageEnd((event)=>this.log('END',event?.url??'')).onErrorReceive((event)=>{this.log('ERROR',event.error.getErrorInfo())})

一次正常加载,你希望看到的顺序大致是:

[ArkWeb][ATTACHED] [ArkWeb][BEGIN] https://example.com [ArkWeb][END] https://example.com

如果只有BEGIN没有后续,你就知道应该继续看网络、超时或页面加载过程。如果连ATTACHED都没看到,那排查方向就完全不同。

这也是为什么我前面一直强调:不要只看"页面有没有显示",要把关键节点留下来。

10. 最后整理成一个能继续扩展的版本

把前面的逻辑合起来,核心结构可以保持得很干净:

import{webview}from'@kit.ArkWeb'enumWebLoadState{Idle,Loading,Success,Error}@Entry@Componentstruct ArkWebPage{privatecontroller:webview.WebviewController=newwebview.WebviewController()@StatewebState:WebLoadState=WebLoadState.Idle@StatecontrollerReady:boolean=falseprivateurl:string='https://example.com'privateloadTimer:number=-1privateclearTimer():void{if(this.loadTimer!==-1){clearTimeout(this.loadTimer)this.loadTimer=-1}}privatestartTimer():void{this.clearTimer()this.loadTimer=setTimeout(()=>{this.webState=WebLoadState.Error},15000)}aboutToDisappear():void{this.clearTimer()}build(){Stack(){Web({src:this.url,controller:this.controller}).width('100%').height('100%').onControllerAttached(()=>{this.controllerReady=trueconsole.info('[ArkWeb][ATTACHED]')}).onPageBegin((event)=>{this.webState=WebLoadState.Loadingthis.startTimer()console.info(`[ArkWeb][BEGIN]${event?.url??''}`)}).onPageEnd((event)=>{this.clearTimer()this.webState=WebLoadState.Successconsole.info(`[ArkWeb][END]${event?.url??''}`)}).onErrorReceive((event)=>{this.clearTimer()this.webState=WebLoadState.Errorconsole.error(`[ArkWeb][ERROR]${event.error.getErrorInfo()}`)})if(this.webState===WebLoadState.Loading){LoadingProgress().width(36).height(36)}if(this.webState===WebLoadState.Error){Column({space:12}){Text('页面加载失败').fontSize(18)Button('重新加载').onClick(()=>{if(this.controllerReady){this.controller.refresh()}})}.width('100%').height('100%').justifyContent(FlexAlign.Center)}}.width('100%').height('100%')}}

这个版本还不算"企业级封装",但已经把最关键的边界理顺了:controller有自己的就绪节点;Web 加载有自己的开始、结束和失败节点;ArkUI页面离开时负责清理页面级资源;UI 不再到处改loading,而是根据一个明确的加载状态渲染。

11. 几个特别容易写错的地方

第一,不要用aboutToAppear代替 Web 的加载开始。
它们描述的不是同一件事。页面实例出现,不等于每一次 URL
导航都从这里开始。

第二,不要看到 controller 已经new了,就默认 Web 已经可操作。
对时序敏感的操作,先关注 controller 与 Web 的挂载节点。

第三,不要只写成功路径。错误页、重试、超时兜底最好在第一版 Web容器里就放进去。等线上白屏以后再补,通常会补得很狼狈。

第四,不要让多个布尔值随便组合。
页面状态一多,尽早改成枚举状态。代码看起来多了两行,后面会少很多 if。

第五,不要把onPageEnd直接理解成"业务全部加载完成"。它描述的是Web 页面加载过程中的节点。你的 H5 如果还有接口请求和业务渲染,业务 Ready应该有自己的定义。

小结

这一篇没有讲 JSBridge,也没有讲缓存、预加载和性能优化。

因为这些东西都建立在一个前提上:你得先知道 Web 组件现在处于什么状态。

把WebviewController、onControllerAttached、onPageBegin、onPageEnd、错误处理和页面清理这几个点串起来,ArkWeb
就不再是一个"能打开网页就行"的黑盒,而是一个可以观察、可以控制、也可以排错的业务容器。

你可以给自己留一个小练习:把示例里的错误页再加一个"复制当前
URL"或者"返回上一页"的动作,同时把每次加载耗时打印出来。

下一篇再往前走:H5 和 ArkTS 到底怎么稳定通信,JSBridge
应该在哪个时机注册。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 2:49:44

AI来了,数据治理还重要吗

Tags 数据治理 AI 大模型 数据质量 主数据 元数据 数据中台 NL2SQL 数据资产 数据驱动 我的判断是,AI越强,数据治理越值钱。 目录 1 开头 2 先看AI现在能干什么 3 AI解决不了的三类数据问题 4 AI和治理,其实是互相成就 5 我的判断&#xff0…

作者头像 李华
网站建设 2026/9/30 2:48:54

电子产品为什么要防静电:PCBA贴片加工厂解析静电危害与防护常识

摘要:静电是积累在物体表面的电荷,人在干燥环境里走几步就可能带上上千伏,摸到电路板时会瞬间放电。放电能量虽小,却足以击穿芯片内部的细小结构:有的当场失效,有的只受伤、用一阵才坏,这类隐性…

作者头像 李华
网站建设 2026/9/30 2:47:01

硅光芯片为什么开始自己“测光”?从Tapless监测到动态功率控制

硅光芯片为什么开始自己“测光”?从Tapless监测到动态功率控制 过去讨论硅光芯片时,工程重点通常集中在调制器、光探测器、耦合器以及光波导损耗等器件本身。但随着光子集成度继续提高,一个越来越现实的问题开始出现:芯片内部的光…

作者头像 李华