把第三方活动页装进 Web 组件,表面上只是给 WebviewController 一个地址。真正麻烦的是“页面已经开始加载”与“用户已经能够操作”之间,有一段并不透明的时间。网络请求可能失败,主框架结束加载时子资源还在下载,甚至内核渲染进程退出后,页面仍保留着先前的视觉状态。如果这几种情况都用一个 loading 布尔值表达,业务按钮和错误面板就容易给出相互矛盾的提示。

本文拿一个叫 WebGate 的第三方 H5 容器做方案演示。页面文件为 PartnerTermsPage.ets,任务编号 WG-017。示例中的 REVIEW、1/2 和时间 16:40:20 是预设的故障演示数据,不是本人在真机上复现并测量出的结果。这里讨论的目标不是让每次加载都成功,而是让失败能够被识别、被限制、被人工恢复。

一、把“加载结束”从“可用”里拆出来

最容易埋雷的写法,是在 onPageEnd 中直接关闭遮罩、把页面状态置为成功。官方 ArkWeb 故障排查文档明确指出,onPageEnd 只在主 frame 触发,不能据此推断所有子 frame 或资源完成,也不保证下一帧已经反映最新 DOM 状态。对于带第三方脚本的活动页,这个边界很实际:一个关键按钮依赖的外部脚本还没准备好,用户却已经看到了“加载成功”。

我更倾向于把应用侧能观察到的阶段收敛成四个值:IDLE 表示尚未请求,LOADING 表示主页面加载流程正在推进,READY 只表示主框架已报告结束,REVIEW 表示需要用户处理的故障。此处故意不使用 SUCCESS,因为没有来自 H5 的业务就绪握手,原生侧不应该给出比证据更强的结论。

这四个值解决的是 UI 表达问题,不是网络协议问题。即便显示 READY,如果 H5 的登录态、组件初始化或业务脚本失败,仍应由前端另行回传更高层的就绪信号。反过来,页面白屏也不应一律归因于网络,可能是访问权限、UA 兼容、本地资源跨域或错误的组件尺寸造成。先分清层次,再谈恢复。

二、先设立有限重试规则

WebGate 允许最多两次人工重试,禁止收到错误后立即进入无限自动循环。重试次数是这次页面会话的计数,不写进永久存储;离开会话重新进入,应该重新建立会话身份。演示页里 WG-017 只是用于日志串联的业务标签,并非系统 API 分配的任务 ID。

下面的纯 ArkTS 规则只解决“是否允许再发一次请求”,不直接调用 Web 内核。这样即使以后替换加载容器,重试政策也不会散落到不同事件回调中。

enum GatePhase {
  IDLE = 'IDLE',
  LOADING = 'LOADING',
  READY = 'READY',
  REVIEW = 'REVIEW'
}

class RetryGate {
  phase: GatePhase = GatePhase.IDLE;
  retryCount: number = 0;
  readonly maxRetry: number = 2;

  markFailed(): void {
    this.phase = GatePhase.REVIEW;
  }

  requestRetry(): boolean {
    if (this.phase !== GatePhase.REVIEW ||
        this.retryCount >= this.maxRetry) {
      return false;
    }
    this.retryCount += 1;
    this.phase = GatePhase.LOADING;
    return true;
  }
}

markFailed() 不改变次数,因为错误本身不等于用户已经选择重试。requestRetry() 才消耗一次机会,这一点直接决定界面上 1/2 的含义:用户已经提交过一次新的加载尝试,而不是系统被动收到了一个错误。真实产品还应按错误类型增加策略,例如 SSL 错误不适合简单重复请求,身份认证失败则应优先刷新凭据。示例没有做这些扩展,也不会宣称自己解决了所有白屏原因。

三、把 Web 回调变成可读的状态变化

本例只使用官方文档中能够核对的 Web、WebviewController、onControllerAttached、onPageBegin、onPageEnd 和 onErrorReceive。控制器没有关联到 Web 组件之前,不主动调用 loadUrl,否则可能出现初始化相关异常。页面地址使用 https://example.com 作为占位,实际集成时必须替换为已确认可访问、可信任的业务地址。

下面展示连接事件的关键片段;它属于 PartnerTermsPage.ets 的构建逻辑,不是整页可直接复制的全部源码。

import { webview } from '@kit.ArkWeb';

@Entry
@Component
struct PartnerTermsPage {
  private controller: webview.WebviewController =
    new webview.WebviewController();
  private controllerReady: boolean = false;
  @State phase: string = 'IDLE';
  @State failureText: string = '';

  build() {
    Column() {
      Web({ src: 'https://example.com', controller: this.controller })
        .javaScriptAccess(true)
        .fileAccess(false)
        .onControllerAttached(() => {
          this.controllerReady = true;
        })
        .onPageBegin(() => {
          this.phase = 'LOADING';
        })
        .onPageEnd(() => {
          if (this.phase !== 'REVIEW') this.phase = 'READY';
        })
        .onErrorReceive((event) => {
          if (event && event.request.isMainFrame()) {
            this.phase = 'REVIEW';
            this.failureText = '主框架加载失败';
          }
        })
        .width('100%')
        .height('65%');
    }
  }
}

这里对 isMainFrame() 的过滤很关键。若把一张非核心图片请求失败直接提升为全页失败,弹窗会过度打断用户;但如果主框架请求失败还继续显示“正在加载”,就可能留下永久等待。READY 仍然不是“业务功能全部可用”。代码中的 height('65%') 是为了让错误提示区域有固定空间,百分比并非所有设备的最佳布局,折叠屏或横屏需要在父容器中重新分配高度。

.javaScriptAccess(true) 不代表应该随意暴露原生能力;需要 JSBridge 时,还要单独审查对象可调用范围、来源与参数。.fileAccess(false) 则是面向普通在线页面的保守默认,涉及文件选择或本地离线包的产品需要按官方访问规则另行评估。

四、恢复动作不应由错误事件直接发起

如果在 onErrorReceive 里立刻 loadUrl,一次资源失败很容易造成接连不断的请求,调试日志也难以看清“新请求由谁触发”。WebGate 的恢复入口单独放在页面按钮上:只有 REVIEW、控制器已绑定、次数未耗尽,三个条件同时满足,才允许手动重载。

下面是页面内的关键方法。retryCount 应当与上一节的 RetryGate 实例保持同一个真值源;为方便说明 UI 更新,这里展示直接挂在组件上的等价写法,实际项目二选一,不要让两套计数并存。

private readonly maxRetry: number = 2;
@State retryCount: number = 0;

retryByUser(): void {
  if (!this.controllerReady || this.phase !== 'REVIEW' ||
      this.retryCount >= this.maxRetry) {
    return;
  }
  this.retryCount += 1;
  this.phase = 'LOADING';
  this.failureText = '';
  try {
    this.controller.loadUrl('https://example.com');
  } catch (error) {
    this.phase = 'REVIEW';
    this.failureText = '控制器调用失败';
    console.error(`WebGate retry error: ${String(error)}`);
  }
}

按钮点击之后,LOADING 应立即遮住重复点击入口。这里没有借助 setTimeout 假装页面成功,也不根据网络等待时间臆测真实故障。生产实现还应为正在进行的会话打上递增序号,避免上一轮异步回调覆盖新一轮状态;若 Web 渲染进程异常退出,应结合 onRenderExited 的官方处理建议保存必要信息,再由明确的交互发起重新加载。

再强调一个细节:控制器调用成功,只代表请求被提交,不代表地址有效。图里的 WG-017 / REVIEW / 1/2 展示的是设计好的故障注入画面,不等于 example.com 真有同样错误。调试时应保留 URL 的脱敏版本、是否主 frame、会话编号、阶段与重试次数,而不是直接把完整查询参数和令牌写入日志。

五、从页面与日志一起看错误链

这次演示固定一组用于文图校对的数据:WebGate、WG-017、REVIEW、主框架加载失败(演示)、重试 1/2、自动重试关闭。时间采用示例时刻 16:40:20。在配图里它们应指向同一个事件,不把加载进度当成已测出的成功率。

若准备在 DevEco Studio 中真正验证,我会先用可控的本地测试服务模拟 HTTP 失败与超时,检查 onPageBegin → onErrorReceive → REVIEW 的顺序;再用可正常访问的页面检验 onPageEnd 后能否显示对应状态。接着禁用网络、模拟子资源错误,确认只有主框架失败才提升到全页错误。最后连续点按重试按钮,观察调用次数是否最多两次,并检查离开页面后不会留有业务层定时器或未释放的回调引用。这些都是建议执行的验收步骤,本文没有把它们写成已经跑出的数据。

对于三方 H5,还应单独核对网络权限、domStorageAccess、外部 cookie 需求、UA 和本地资源跨域限制。官方白屏排障文档明确给出了这些检查项;不是打开所有权限就能把页面修好。尤其不应为了排错一股脑启用 fileAccess 和调试能力,否则问题可能从“偶发白屏”变成“访问边界过宽”。

六、把可观测性留在应用内

完整的工程结论不是“失败自动刷新两次”,而是让页面承认自己究竟知道什么。onPageEnd 证明主框架加载流程到达某个阶段;onErrorReceive 提供错误事件;应用业务是否就绪,则需要业务本身给出证据。把这三件事混为一谈,界面即使暂时不白屏,也可能在交互时出问题。

WebGate 的这版方案适用于可控的三方活动页、帮助中心或协议页容器。它不是浏览器通用容错层,也不处理证书绕过、登录态恢复及任意脚本注入。上线前应做真机和不同网络场景验证,确认 SDK 版本对应的回调细节,并建立脱敏日志的回收周期。留下这些边界,才是一个可以继续迭代的工程方案。

官方资料: ArkWeb 页面白屏排查(2026-06-26)、ArkWeb runJavaScript 与 onPageEnd 说明(2026-06-26)。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐