Web 页面里有个上传进度条,原生壳也想在工具栏显示同一个进度。最初看起来只要传一个数字就够:H5 更新到 72%,发一条消息给 ArkTS,原生页面跟着显示 72%。但只要在上传过程中切换 Web 页面、重复初始化消息通道或把页面关掉,进度消息就可能来自上一轮会话。UI 看到的是一个正确格式的 72,却无法知道它是不是当前上传的 72。我想讨论的不是上传服务,也不是文件选择权限,而是这个容易被轻视的连接边界:WebMessagePort 建立以后,如何确认双方已经准备好,怎样拒绝旧会话消息,以及页面离开时谁负责关闭端口。Demo 叫 UploadBridge,任务 ID 固定为 UP_20261016_15,双方完成握手后状态 ACTIVE,示例进度 72%,收到的最大序号 seq=4。界面专门标注“演示消息,未实际上传文件”,避免把进度模拟误认为已经连接真实服务。图 01:应用与 H5 双向消息通道的抽象封面;两端交换的是教学数据,并不代表网络上传已经发生。## 一、通信端口不是一条可以永远信任的线华为官方《建立应用侧与前端页面数据通道》介绍了 WebviewController.createWebMessagePorts()、端口上的 onMessageEvent()、发送给页面的 postMessage(),以及结束时使用 close()。这是一组真正存在的 ArkWeb 接口;官方也特别说明,消息可以是字符串和 ArrayBuffer,若要传对象,需要先 JSON.stringify。我选择使用 JSON 字符串而不是裸数字,正是为了把会话 ID、消息序号、类型与进度放在同一个包裹里。https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/web-app-page-data-channel需要留意:WebMessagePort 是传递数据的通道,不会自动证明 H5 内容可信。不同来源的网页应落实白名单和来源验证,示例使用本地静态页面。本文也不讨论进程崩溃后的真正断点续传;这里说的“恢复”只指会话状态与 UI 恢复,不等于文件续传。第一段代码只实现消息验证,尽量不跟 UI 生命周期耦合。解决的问题是:旧任务、乱序回调和非法进度不能覆盖当前任务的业务快照。arktsinterface ProgressPacket { type: 'READY' | 'PROGRESS' | 'DONE' taskId: string seq: number percent: number}class ProgressGate { private taskId: string = 'UP_20261016_15' private lastSeq: number = -1 percent: number = 0 ready: boolean = false accept(p: ProgressPacket): boolean { if (p.taskId !== this.taskId || !Number.isInteger(p.seq)) return false if (p.seq <= this.lastSeq || !Number.isFinite(p.percent)) return false if (p.percent < 0 || p.percent > 100) return false if (p.type === 'READY') this.ready = true if (p.type !== 'READY' && !this.ready) return false this.lastSeq = p.seq if (p.type === 'PROGRESS') this.percent = p.percent return true }}为什么不允许序号倒退?因为网络请求和渲染队列不保证所有异步结果都按照发起顺序回到业务层。即使单个端口在局部时序上有顺序约束,应用仍可能重新建端口、从旧对象收到消息。lastSeq 的作用就是把当前可接收的进度单调化。READY 必须先于 PROGRESS 被接受,才能把“端口已创建”和“H5 已准备好”区分开。后续真正的上传协议还应加入来源验证、任务签名、服务器状态对账与失败重试,而不是把 UI 数字当最终事实。## 二、只创建通道一次,并为其准备明确的回收入口最危险的实现是每次 Web 加载结束都无条件再执行一次 createWebMessagePorts(),但旧端口仍持有消息回调。时间一长,你会看到一条 Web 消息触发多次原生事件,然后误以为是 H5 重复上传。我的处理方式是:每次建立新会话前先回收旧端口,注册接收事件时绑定当前会话代次;离开页面时关闭端口并使旧代次失效。下面代码展示 ArkTS 与 ArkWeb 的连接骨架。它解决通道初始化、消息派发和资源释放三个动作。和官方示例保持一致,用 createWebMessagePorts() 取得两个端口,把一端交给本地 HTML 页面,另一端在应用侧接收数据。为控制篇幅,省略 Web 页面结构的常规布局。arktsimport { webview } from '@kit.ArkWeb'@Entry@Componentstruct UploadBridgePage { controller: webview.WebviewController = new webview.WebviewController() private ports: webview.WebMessagePort[] = [] private generation: number = 0 private gate: ProgressGate = new ProgressGate() private timeoutId: number = -1 @State percent: number = 0 @State status: string = 'HANDSHAKE' private openBridge(): void { this.closeBridge() const lease = ++this.generation this.gate = new ProgressGate() this.status = 'HANDSHAKE' this.timeoutId = setTimeout(() => { if (lease === this.generation && !this.gate.ready) { this.status = 'TIMEOUT' this.closeBridge() } }, 5000) // 业务约定的5秒握手期限,不是系统默认值 this.ports = this.controller.createWebMessagePorts() if (this.ports.length !== 2) return this.ports[1].onMessageEvent((message: webview.WebMessage) => { if (lease !== this.generation || typeof message !== 'string') return try { const packet = JSON.parse(message) as ProgressPacket if (!this.gate.accept(packet)) return if (packet.type === 'READY' && this.timeoutId !== -1) { clearTimeout(this.timeoutId) this.timeoutId = -1 } this.status = this.gate.ready ? 'ACTIVE' : 'HANDSHAKE' this.percent = this.gate.percent } catch (err) { console.error(`invalid progress message: ${err}`) } }) this.controller.postMessage('__init_port__', [this.ports[0]], '*') } private closeBridge(): void { this.generation++ if (this.timeoutId !== -1) { clearTimeout(this.timeoutId) this.timeoutId = -1 } this.ports.forEach((port: webview.WebMessagePort) => { try { port.close() } catch (_) { /* 记录环境异常 */ } }) this.ports = [] } aboutToDisappear(): void { this.closeBridge() } build() { Column() { Text(`任务 UP_20261016_15 · ${this.status}`) Text(`进度 ${this.percent}%`) Web({ src: $rawfile('index.html'), controller: this.controller }) .onPageEnd(() => this.openBridge()) } }}注意这个示例采用 * 传递端口,仅用于受控的本地 index.html 演示。实际加载远端网页时不应无条件放宽来源限制,要结合业务白名单与页面来源设置目标 origin。closeBridge() 增加代次并清理定时器,让旧回调即使晚到也不能改变当前页面。生产工程应记录关闭异常,并在页面重建后重新创建 ProgressGate 或重置会话信息;否则一个新端口却沿用旧会话的最后序号,会把新的合法握手也挡掉。图 02:独立绘制的 DevEco Studio 风格演示图。右侧显示 UP_20261016_15 的 ACTIVE / 72%,底部列出 READY、PROGRESS 72 与 stale seq ignored 示例日志,不代表实际运行结果。## 三、H5 侧只负责发消息,不负责决定原生任务是否完成Web 页面收到 __init_port__ 以后,要保存 event.ports[0] 并明确发出握手消息。模拟进度时也应该带上同一任务 ID 和递增序号。这里的 READY 表示页面已经持有端口,不表示远程上传服务已经准备好;PROGRESS 仅作为演示消息,不证明远端文件已经写入。这样约定以后,原生应用就能拒绝没有握手的进度包。第三段代码是 index.html 中的关键 JavaScript。它只模拟一次准备就绪和一次 72% 进度消息,避免为了画图假装真的上传了文件。javascriptconst TASK_ID = 'UP_20261016_15';let port = null;window.addEventListener('message', (event) => { if (event.data !== '__init_port__' || !event.ports[0]) return; if (port) port.close(); port = event.ports[0]; port.postMessage(JSON.stringify({ type: 'READY', taskId: TASK_ID, seq: 1, percent: 0 })); port.postMessage(JSON.stringify({ type: 'PROGRESS', taskId: TASK_ID, seq: 4, percent: 72 }));});window.addEventListener('pagehide', () => { if (port) { port.close(); port = null; }});这里故意让进度包序号从 1 跳到 4,是为了表达中间消息可能已经被过滤或合并,不能依赖收到消息的条数当作进度本身。在示例里,如果一个序号为 3 的旧包在序号 4 后到达,就应该被拒绝;而不是把页面显示从 72% 改回 40%。但要特别区分:“过期进度包被拒绝”不等于“上传任务已经取消”,取消远端任务需要单独的请求和服务端确认。## 四、结束页面时要能说清楚关掉了什么Demo 的最终展示状态固定为 ACTIVE、进度 72%、桥接握手 READY、任务 UP_20261016_15。手机图底部有“断开通道”操作入口,但图中并没有把它按下,所以配图仍然显示 ACTIVE。触发断开后,页面应转为 CLOSED 并把端口对象置空。不能仅把进度条清零就宣称释放了 WebMessagePort;回调仍可能持有旧引用。*图 03:纯屏运行效果示意图,显示 72% 的教学消息和当前握手态。图中明确标注“模拟进度,未实际上传文件”。*我会把调试重点放在四种时序上:先收进度再收 READY、两个 H5 页面先后初始化、Web 页面已经离开但晚到消息仍存在、连续开关页面造成端口对象累积。日志至少包含 taskId、generation、seq、当前状态与关闭次数,而不是只输出“message received”。出现消息重复时,通过这几个字段才有机会判断到底是 H5 重发、Webview 重建,还是原生重复注册事件。日志中展示的一切数值都是预置演示数据,不是性能测试统计。## 五、真正上线还差哪几道关这篇文章能直接迁移的经验是:建立端口后有握手确认、每条进度消息有业务身份和序号、页面离开时有明确的端口回收。它不能替代真实上传协议中的断点续传、鉴权、防重放、服务端进度确认,更不能把 72% 当成网络层已上传比例。对远程页面,还要按官方安全建议严控加载地址和消息来源,并在目标 SDK 上实测端口重建与页面生命周期行为。官方文档已经给出 createWebMessagePorts、onMessageEvent、postMessage、close 的能力边界,本文只在此范围内做一个业务协议模型。代码未经 DevEco 编译和设备运行,图片也全部是工程演示,不代表已经完成真实 H5 上传。把通信通道“什么时候有效、什么时候结束”说清楚,比让一个绿色进度条看起来顺滑更重要。另外,页面恢复时必须区分“恢复展示”和“重新开始传输”:前者读取已有任务快照,后者可能创建全新任务 ID。将二者混为同一个动作,通常就会制造重复发送和进度回退。对于有真正后端服务的上传业务,还应让服务端保存权威进度,再由 Web 与原生订阅同一任务,而不是在两个界面各维护一份互相猜测的百分比。只有当超时、取消、页面销毁与服务端确认之间的关系都写进协议,进度条才算具备工程意义。

Logo

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

更多推荐