Web 组件与 ArkTS 双向通信
概述
应用里嵌一个 H5 活动页是很常见的需求:活动频繁改版,用 H5 可以不走发版流程;但页面又需要原生能力,比如读取当前登录态、跳转到原生页面、弹出原生提示。于是就出现了双向通信的问题——原生要能把数据传给 H5,H5 也要能调用原生。
最后把通信收敛成两条明确的通道:原生到 H5 用runJavaScript,H5 到原生用javaScriptProxy注入的对象。
说明
先明确两个方向各自的入口。
ArkTS 调用 H5,用的是 WebviewController.runJavaScript(script)。它接收一段字符串形式的脚本,在页面的 JS 上下文里执行,返回一个 Promise<string>。注意返回值只能是字符串——如果 H5 侧要回传对象,需要在脚本里 JSON.stringify,原生侧再 JSON.parse。另外这段脚本必须在页面加载完成后执行,放在 onPageEnd 里最稳妥,否则可能作用在还没就绪的文档上。
H5 调用 ArkTS,用的是Web组件的 javaScriptProxy 属性。它把一个 ArkTS 对象挂到 H5 的 window 上,name 是挂载名,methodList 是允许 H5 调用的方法白名单。H5 里直接 window.nativeBridge.getToken() 就能拿到返回值。
这里有一个很关键的时机差异:javaScriptProxy 是组件属性,在组件创建时就完成注入,首次加载页面即可使用;而 WebviewController.registerJavaScriptProxy() 是运行时方法,注册后需要调用 refresh() 重新加载页面才会生效。首次接入时如果混用,就会出现"首屏拿不到对象、刷新一下又正常"的诡异现象。所以首屏注入优先用 javaScriptProxy。
还有几个实践中的约束值得提前知道:
-
只暴露白名单方法。 methodList 里没列的方法,H5 是调不到的。不要图省事把整个对象的所有方法都暴露出去,尤其是能读写文件、能任意路由的方法。
-
跨语言只传字符串。 复杂参数先序列化,原生侧拿到后要做校验,不能直接信任 H5 传来的path、url。比如跳转页面的方法,应该校验目标是否在允许的路由表里,而不是原样 pushUrl。
-
注入方法的返回值是同步给回 H5 的。 需要异步结果时,通常拆成"H5 发起调用 → 原生处理完再用 runJavaScript 回调 H5"两步,而不是指望注入方法直接返回一个 Promise。
-
token 这类敏感数据按需取,不要一次性塞进全局变量。 H5 每次需要时调用 getToken(),原生侧返回当前最新值,既避免了过期问题,也减少了泄露面。
使用实践
下面这个页面把两条通道都接上了:H5 可以取 token、跳转原生页面;原生在页面加载完成后主动把当前主题推给 H5。
import { webview } from '@kit.ArkWeb'
import { router } from '@kit.ArkUI'
// 暴露给 H5 的原生能力,只放白名单里的方法
class NativeBridge {
getToken(): string {
return AppStorage.get<string>('token') ?? ''
}
openPage(path: string): void {
// 不直接信任 H5 传来的路径,先校验再跳转
const allowList: string[] = ['pages/Coupon', 'pages/Help']
if (allowList.indexOf(path) >= 0) {
router.pushUrl({ url: path })
}
}
}
@Entry
@Component
struct ActivityPage {
private controller: webview.WebviewController = new webview.WebviewController()
private bridge: NativeBridge = new NativeBridge()
build() {
Column() {
Web({ src: 'https://example.com/activity', controller: this.controller })
.javaScriptAccess(true)
.javaScriptProxy({
object: this.bridge,
name: 'nativeBridge',
methodList: ['getToken', 'openPage'],
controller: this.controller
})
.onPageEnd(() => {
// 原生 -> H5:页面就绪后再推送主题,避免脚本作用在未完成的文档上
const theme: string = AppStorage.get<string>('theme') ?? 'light'
this.controller.runJavaScript(
`window.onNativeTheme && window.onNativeTheme('${theme}')`
)
})
.width('100%')
.layoutWeight(1)
}
.width('100%')
.height('100%')
}
}
对应到 H5 侧,调用就是普通的方法调用,比如 window.nativeBridge.getToken() 取当前登录态,window.nativeBridge.openPage('pages/Coupon')跳到原生页面,不需要额外的 SDK 或桥接脚本。
代码里有三处是刻意为之。methodList 只列了两个方法,未列出的方法即使对象上有,H5 也调不到;openPage 里先做白名单校验,避免 H5 通过篡改参数跳到未授权页面;onPageEnd 保证了 runJavaScript 执行时文档已经就绪。
把这套通道固定下来之后,后续活动页再改版,原生侧基本不用动——新增能力只是在 NativeBridge 里加一个方法,并把它登记进 methodList。
更多推荐
所有评论(0)