鸿蒙从零到一 HarmonyOS Web 组件与 JSBridge 通信实战:从页面加载到安全协议
在 HarmonyOS 应用中,Web 组件适合承载已有 H5 页面、富文本内容、活动页和需要快速迭代的业务界面。但网页和 ArkTS 页面属于两套运行环境:网页使用 JavaScript,原生侧使用 ArkTS;如果只是把 URL 放进 Web 组件,双方并不会自动共享状态。
真正可维护的方案,是把 Web 组件当成一个有边界的通信端点,明确加载策略、消息格式、调用方向、生命周期和安全策略。本文以"原生页面打开一个网页订单详情,网页请求原生能力并接收结果"为例,逐步搭建一套轻量 JSBridge。
一、先理解 Web 组件的职责边界
Web 组件负责在 ArkUI 页面中展示网页,并提供网页加载、导航、脚本执行和事件监听等能力。它不是把网页代码直接编译成 ArkTS,也不是一个可以随意访问应用内部对象的容器。
常见职责可以这样分工:
- 网页负责展示内容、表单交互和与网页后端的协议。
- ArkTS 负责应用身份、系统能力、页面路由和本地数据访问。
- Bridge 负责把双方真正需要交换的数据转换成明确消息。
- 业务层负责校验消息来源、参数和调用结果。
如果网页只是静态帮助文档,可以不引入 Bridge;如果网页需要调用相册、定位、支付或原生页面跳转,就应先设计协议,再编写具体接口。把所有能力都暴露给网页,后续很难收紧权限,也难以定位问题。
二、创建 Web 组件与加载页面
Web 组件通常需要一个 WebviewController。控制器负责加载页面、执行脚本以及访问导航相关能力。示例中的 API 名称和参数请以当前 DevEco Studio 对应 SDK 的类型定义为准,不同版本可能会有细节差异。
ts
import web_webview from '@ohos.web.webview'
@Entry
@Component
struct OrderWebPage {
private controller: web_webview.WebviewController =
new web_webview.WebviewController()
build() {
Column() {
Web({
src: 'https://m.example.com/order/detail',
controller: this.controller
})
.width('100%')
.height('100%')
.javaScriptAccess(true)
.onPageBegin(() => {
console.info('order page begin')
})
.onPageEnd(() => {
console.info('order page ready')
})
.onErrorReceive((event) => {
console.error(`web load error: ${JSON.stringify(event)}`)
})
}
.width('100%')
.height('100%')
}
}
加载外部地址前,应在模块配置中声明网络访问权限,并确认域名、证书和重定向策略符合应用的网络安全要求。调试阶段可以使用本地页面,但发布环境应使用 HTTPS,并对允许访问的域名做白名单管理。
页面加载事件适合更新加载状态、记录耗时和展示错误页。不要把一次加载完成误认为 Bridge 已经可以调用:网页脚本可能还在初始化,通信通道应在网页主动发送 ready 消息后才认为可用。
三、设计稳定的消息协议
Bridge 最容易失控的地方不是发送消息,而是消息没有统一格式。建议让每条消息都包含版本、动作名、请求标识和参数,并区分请求、响应和事件。
ts
interface BridgeRequest {
type: 'request'
version: 1
requestId: string
action: string
payload: Record<string, string | number | boolean | null>
}
interface BridgeResponse {
type: 'response'
version: 1
requestId: string
ok: boolean
data?: unknown
error?: {
code: string
message: string
}
}
requestId 用于把异步结果匹配回原始请求,不能使用时间戳加动作名这种容易碰撞的组合。version 让网页和应用可以平滑升级;ok、error.code 和 error.message 让调用方能稳定处理失败,而不是解析自然语言。
动作名建议采用有限集合,例如 getAppInfo、openNativePage、selectImage。不要允许网页把任意字符串当成方法名直接反射调用。参数也要按动作单独校验,不能因为外层是 JSON 就认为内容可信。
四、网页调用原生能力
一种常见做法是由网页调用原生注入的 JavaScript 方法,原生侧收到 JSON 后解析并分发。网页侧可以封装成 Promise,让业务代码不必关心 requestId。
js
const pending = new Map()
function callNative(action, payload = {}) {
const requestId = `${Date.now()}_${Math.random().toString(16).slice(2)}`
return new Promise((resolve, reject) => {
pending.set(requestId, { resolve, reject })
window.arkBridge.postMessage(JSON.stringify({
type: 'request',
version: 1,
requestId,
action,
payload
}))
})
}
function receiveNativeMessage(raw) {
const message = typeof raw === 'string' ? JSON.parse(raw) : raw
if (message.type !== 'response') return
const task = pending.get(message.requestId)
if (!task) return
pending.delete(message.requestId)
message.ok ? task.resolve(message.data) :
task.reject(new Error(message.error?.message || 'native call failed'))
}
这里的 window.arkBridge.postMessage 代表双方约定的通信入口,具体注入方式依赖当前 Web 组件 SDK。无论采用脚本注入、网页消息回调还是自定义 URL 协议,都应该把底层差异收敛在 Bridge 适配层,业务代码只依赖 callNative。
原生侧的处理流程可以抽象为:读取原始消息、解析 JSON、校验公共字段、校验动作参数、执行能力、返回同一个 requestId。任何一步失败都返回结构化错误,不能让异常直接穿透到 Web 组件回调。
ts
private async handleBridgeMessage(raw: string): Promise<void> {
let request: BridgeRequest
try {
request = JSON.parse(raw) as BridgeRequest
this.validateRequest(request)
} catch (error) {
console.error(`invalid bridge request: ${JSON.stringify(error)}`)
return
}
try {
const data = await this.dispatchAction(request.action, request.payload)
this.sendResponse({
type: 'response', version: 1, requestId: request.requestId,
ok: true, data
})
} catch (error) {
this.sendResponse({
type: 'response', version: 1, requestId: request.requestId,
ok: false,
error: { code: 'NATIVE_CALL_FAILED', message: this.toSafeMessage(error) }
})
}
}
五、原生侧调用网页方法
反方向的调用也很常见,例如原生完成登录后通知网页刷新用户信息。原生侧可以通过控制器执行 JavaScript,但不要直接拼接用户输入到脚本字符串中。
ts
private notifyWebLogin(token: string): void {
const safeToken = JSON.stringify(token)
const script = `window.appEvents && window.appEvents.onLogin(${safeToken})`
this.controller.runJavaScript(script, (result) => {
console.info(`login event delivered: ${JSON.stringify(result)}`)
})
}
使用 JSON.stringify 做字符串字面量编码,可以避免引号、换行或脚本片段破坏 JavaScript 语法。更复杂的数据应先序列化为 JSON,再在网页侧解析。不要采用字符串拼接的方式拼出对象,也不要把服务端返回的 HTML 当作脚本执行。
原生调用必须等待网页 ready。可以维护一个待发送队列,在网页发送 ready 事件后刷新;页面重新加载时清空旧队列和旧请求,避免把上一页面的响应交给新页面。
六、处理生命周期与并发
Web 页面会经历加载、跳转、刷新和销毁,Bridge 不能只考虑"打开后点击按钮"的理想路径。建议把以下状态作为组件内部状态机管理:
idle:控制器已创建,但页面还未准备好。loading:页面正在加载,暂不处理业务调用。ready:网页已发送协议版本匹配的 ready 消息。failed:加载或协议协商失败,拒绝新的调用。destroyed:组件销毁,清理监听器、队列和超时任务。
每个请求都应设置超时和取消策略。页面跳转或销毁时,未完成 Promise 必须统一 reject,并清理 Map,否则长时间运行的应用会积累闭包和请求对象。对于高频事件,例如滚动、输入和进度通知,使用事件消息而不是为每个变化创建一个请求。
ts
private readonly pending = new Map<string, (result: BridgeResponse) => void>()
private rejectAllPending(code: string): void {
this.pending.forEach((resolve, requestId) => {
resolve({
type: 'response', version: 1, requestId, ok: false,
error: { code, message: 'web page is no longer available' }
})
})
this.pending.clear()
}
在 onPageBegin、错误回调和组件销毁阶段调用清理逻辑,并用日志记录请求数、超时数和失败码。线上问题通常不是"完全不能通信",而是偶发超时、页面重载后响应错配或旧监听器重复触发。
七、安全边界不能省略
Web 组件会执行网页 JavaScript,因此安全策略必须和功能设计同时完成。重点包括:
- 只加载明确允许的 HTTPS 域名,限制不必要的跳转。
- Bridge 只暴露业务必需的动作,不暴露任意系统 API、文件路径或账号令牌。
- 对来源、协议版本、动作名、参数类型和参数长度逐项校验。
- 敏感操作在原生侧再次确认用户身份和业务状态,不能只相信网页传来的字段。
- 令牌不通过 URL、日志或错误消息传递;返回网页的数据遵循最小化原则。
- 外部网页和内嵌网页分开处理,第三方内容不要获得同等原生能力。
如果 Bridge 使用自定义 URL 拦截协议,必须严格校验 scheme、host、path 和参数,避免网页通过伪造 URL 触发敏感动作。对于脚本注入方式,应固定函数名和参数编码,拒绝直接执行来自网页的任意脚本。
安全校验不是单次上线检查。域名变更、网页前端升级、Bridge 增加新动作和 SDK 升级都应重新验证,尤其要覆盖错误页面、重定向页面和离线缓存页面。
八、错误处理与可观测性
网页端应区分网络错误、协议错误、业务错误和原生能力错误。原生端也应使用稳定错误码,例如:
BRIDGE_NOT_READY:网页尚未完成握手。INVALID_REQUEST:消息格式或参数不符合协议。ACTION_NOT_ALLOWED:当前网页来源不能调用该动作。NATIVE_PERMISSION_DENIED:系统权限或用户授权不足。NATIVE_TIMEOUT:原生能力在约定时间内没有完成。
日志中记录 requestId、action、耗时、结果码和页面地址摘要即可,不要记录完整 token、身份证号、手机号或用户输入。对异常数据做脱敏后再上报。开发环境可以保留详细堆栈,生产环境返回给网页的 message 应该是可理解但不泄露内部实现的安全文本。
九、封装成可测试的 Bridge 服务
不要把消息解析、业务分发和 Web 组件控制器全部写在页面 build() 中。可以拆成三个层次:
BridgeCodec:负责 JSON 编解码和公共字段校验。BridgeRouter:负责动作白名单、参数校验和业务调用。WebBridgeAdapter:负责和 Web 组件 API 对接、发送响应以及生命周期清理。
这样可以在不启动 Web 组件的情况下测试无效 JSON、未知动作、缺失 requestId、超长参数和重复响应。真正依赖控制器的部分只需要验证脚本调用、页面重载和销毁时机。
测试清单至少应包括:页面正常加载并握手、网页调用成功、原生调用失败、连续请求乱序返回、页面刷新、网络断开、超时、重复 ready、非法来源和组件销毁。若应用支持多窗口或横竖屏切换,还要确认控制器和 Bridge 状态不会被旧页面复用。
总结
HarmonyOS Web 组件解决的是网页承载问题,JSBridge 解决的是跨运行环境协作问题。工程上最重要的不是找到一个能"传字符串"的技巧,而是建立一条有版本、有 requestId、有超时、有错误码且有安全边界的通信协议。
从页面加载开始,先等待网页完成握手,再通过白名单分发能力;从原生调用网页开始,做好参数编码、生命周期清理和失败回调。把这些规则集中在适配层后,H5 和 ArkTS 就能各自保持清晰职责,业务页面也不会被大量平台细节绑架。