写在前面
嵌网页有两套容器。应用里用 ArkUI 的 Web,元服务里用 ASCF 的 web-view。名字都像 WebView,桥不是同一套。
这篇先把两者对齐,后面只展开 Web 怎么放进页面、两个方向怎么通信、按什么顺序用。按「业务里已经有 H5」来写方案,不表示当前学习工程已经接了 Web。
H5 和原生是两个运行时。Vue 的 ref 不会变成 @State,原生对象也不能直接当参数传进网页。过桥的只有 JSON。
一、Web 和元服务 web-view 对比
| 应用里的 `Web` | 元服务 `web-view` | |
|---|---|---|
| 写在哪 | ArkUI 页面的 `build()` 里,和 `Column` 同级 | 元服务 ASCF 页面,写法接近小程序 |
| 心智模型 | 你能控制的 `iframe` | 微信小程序的 ` |
| 打开页面 | `Web({ src, controller })`,`src` 可以是网络地址或 `$rawfile` | ` |
| H5 调原生 | 容器把对象注入 `window`,H5 直接 `window.nativeApi.xxx()` | H5 调 `has.ascfweb.postMessage`,不能自己挂一个原生对象 |
| 原生调 H5 | `controller.runJavaScript(...)`,等于在 iframe 里执行一段脚本 | `has.createWebViewContext().postMessage({ data })`,H5 用 `has.ascfweb.onMessage` 收 |
| 实时性 | `onPageEnd` 之后两边都能立刻调 | H5 发给元服务的 `bindmessage`,常常要等页面返回或销毁才回调,不是 `message` 事件 |
| 怎么判断跑在容器里 | 看 `window` 上有没有注入的对象 | User-Agent 里有 `ASCF/` |
| 适合 | 应用内嵌一块已有 Vue 页,还要拿 token、关原生页、调系统能力 | 元服务里打开一个网页,只走官方 SDK 列出的能力 |
选型就看宿主。这篇后面的通信和用法,都只针对左边这套 Web。元服务不要把 javaScriptProxy 抄过去,应用里也不要装 @atomicservice/ascf-web-sdk 来代替桥。
二、Web 怎么使用
Web 是页面里的一块区域,不是 EntryAbility。Ability 只负责把窗口打开。对照现在的 entry:主框架是 Tab,每个 Tab 一把导航栈。H5 是某一栈里的一页,页里面放一个 Web。退栈、切 Tab 仍走 NavPathStack。网页内部的 vue-router 只在这块区域里生效。
一个 Web 配一个控制器,控制器放在这个页面上,不要在子组件里再 new 一个去调它。
<span>import</span> { webview } <span>from</span> <span>'@kit.ArkWeb'</span>;
<span>@Entry</span>
<span>@Component</span>
struct H5Page {
<span>private</span> <span>controller</span>: webview.<span>WebviewController</span> = <span>new</span> webview.<span>WebviewController</span>();
<span>build</span>(<span></span>) {
<span>Column</span>() {
<span>Web</span>({ <span>src</span>: <span>'https://example.com/h5/'</span>, <span>controller</span>: <span>this</span>.<span>controller</span> })
.<span>javaScriptAccess</span>(<span>true</span>)
.<span>domStorageAccess</span>(<span>true</span>)
.<span>onPageEnd</span>(<span>() =></span> {
<span>this</span>.<span>notifyH5Ready</span>();
})
.<span>onErrorReceive</span>(<span>() =></span> {
<span>// 页没打开,不要在这里调桥</span>
})
.<span>width</span>(<span>'100%'</span>)
.<span>height</span>(<span>'100%'</span>)
}
.<span>width</span>(<span>'100%'</span>)
.<span>height</span>(<span>'100%'</span>)
}
<span>private</span> <span>notifyH5Ready</span>(): <span>void</span> {
<span>this</span>.<span>controller</span>.<span>runJavaScript</span>(<span>'window.onNativeReady && window.onNativeReady()'</span>);
}
}
使用时先打开这几个开关,再谈桥:
| 配置 | 不开会怎样 | 对应的 Web 习惯 |
|---|---|---|
| `javaScriptAccess(true)` | 页能开,脚本不执行,`window.nativeApi` 不会出现 | 浏览器默认就能跑 JS,这里要显式打开 |
| `domStorageAccess(true)` | `localStorage` / `sessionStorage` 不可用 | 前端存登录态、草稿会静默失败 |
| `fileAccess(true)` | 本地 `$rawfile` 页要读本地文件时失败 | 只嵌网络页可以先不开 |
| `mixedMode(MixedMode.All)` | HTTPS 页里的 HTTP 资源被拦 | 混合内容,和浏览器策略一样,要显式放行 |
src 两种来源:
- 线上已有的 Vue 站点:
'https://example.com/h5/' - 打进安装包的静态页:
$rawfile('index.html')
地址要换(例如带上不同业务 id),换的是这次加载的 src,不是改一个 @State 就指望网页 DOM 跟着变。
加载回调按 iframe 的生命周期记:
| 回调 | 什么时候用 |
|---|---|
| `onPageBegin` | 开始请求,可显示原生 loading |
| `onProgressChange` | 进度 |
| `onPageEnd` | 相当于 `iframe.onload`。从这里起才能 `runJavaScript` |
| `onErrorReceive` | 没打开。先查地址、证书、混合内容,不要先改桥 |
推荐的使用顺序:
- 页面创建
WebviewController,build()里放Web,打开javaScriptAccess。 - 需要存储就再开
domStorageAccess。 - 等
onPageEnd,通知 H5「桥可以用了」。 - 之后原生用
runJavaScript调 H5;H5 用注入对象调原生。 - 用户要离开这块网页时,走原生导航栈,不要只调
history.back()。那只退 H5 自己的历史。
三、Web 怎么通信
两个方向,两套 API,不要合成一次 postMessage。依据是 鸿蒙与 H5 桥接 里的基础桥。
| 方向 | 用什么 | 相当于 |
|---|---|---|
| H5 → 原生 | `javaScriptProxy` 注入的对象 | 容器在 `window` 上挂一个 SDK,只暴露白名单方法 |
| 原生 → H5 | `controller.runJavaScript` | `iframe.contentWindow` 上执行一段脚本 |
两边约定同一种消息,参数和返回值都用字符串:
<span>interface</span> <span>BridgeMessage</span> {
<span>type</span>: <span>string</span>;
<span>payload</span>: <span>string</span>;
}
1. H5 调原生:注入 window 上的对象
在 Web 上声明代理。name 是挂到 window 上的名字,methodList 是允许 H5 调用的方法。没写进名单的方法,网页调不到。
<span>Web</span>({ <span>src</span>: <span>'https://example.com/h5/'</span>, <span>controller</span>: <span>this</span>.<span>controller</span> })
.<span>javaScriptAccess</span>(<span>true</span>)
.<span>javaScriptProxy</span>({
<span>name</span>: <span>'nativeApi'</span>,
<span>methodList</span>: [<span>'getToken'</span>, <span>'closePage'</span>, <span>'postMessage'</span>],
<span>controller</span>: <span>this</span>.<span>controller</span>,
<span>object</span>: {
<span>getToken</span>: (): <span><span>string</span> =></span> {
<span>return</span> <span>JSON</span>.<span>stringify</span>({ <span>token</span>: <span>'原生登录态'</span> });
},
<span>closePage</span>: (): <span><span>void</span> =></span> {
<span>// 退出当前原生页</span>
},
<span>postMessage</span>: (<span>raw</span>: <span>string</span>): <span><span>string</span> =></span> {
<span>const</span> msg = <span>JSON</span>.<span>parse</span>(raw) <span>as</span> <span>BridgeMessage</span>;
<span>// 按 msg.type 分发</span>
<span>return</span> <span>JSON</span>.<span>stringify</span>({ <span>ok</span>: <span>true</span> });
}
}
})
H5 里就是普通函数调用。先判断对象在不在,再 parse 返回值:
<span>function</span> <span>readToken</span>(<span></span>) {
<span>if</span> (!<span>window</span>.<span>nativeApi</span> || !<span>window</span>.<span>nativeApi</span>.<span>getToken</span>) {
<span>return</span>;
}
<span>const</span> data = <span>JSON</span>.<span>parse</span>(<span>window</span>.<span>nativeApi</span>.<span>getToken</span>());
sessionStorage.<span>setItem</span>(<span>'token'</span>, data.<span>token</span>);
}
<span>function</span> <span>tellNative</span>(<span>type, payload</span>) {
<span>window</span>.<span>nativeApi</span>.<span>postMessage</span>(<span>JSON</span>.<span>stringify</span>({
<span>type</span>: type,
<span>payload</span>: <span>JSON</span>.<span>stringify</span>(payload)
}));
}
这和在网页里调用一个提前注入的 JS-SDK 相同:方法名固定,入参出参都是字符串。复杂对象在调用前 JSON.stringify,在 ArkTS 里 JSON.parse。不要把 Vue 组件、函数当参数。
登录态走这条方向拿:H5 在自己的 mounted 里调 getToken。不要假设浏览器 Cookie 会自动出现在 Web 里。退出时由 H5 再调一个 clearSession,或者由下一节的原生方向通知 H5 清掉 sessionStorage。两边都清,不能只清一边。
2. 原生调 H5:runJavaScript
H5 先把接收函数挂到 window,和先 addEventListener('message') 再等消息是同一顺序:
<span>window</span>.<span>onNativeReady</span> = <span>function</span> (<span></span>) {
<span>readToken</span>();
};
<span>window</span>.<span>onNativeMessage</span> = <span>function</span> (<span>raw</span>) {
<span>const</span> msg = <span>JSON</span>.<span>parse</span>(raw);
<span>const</span> payload = <span>JSON</span>.<span>parse</span>(msg.<span>payload</span>);
<span>// 按 msg.type 更新页面</span>
};
原生必须等 onPageEnd。在这之前函数还不在。参数用 JSON.stringify 放进脚本,避免引号把脚本截断:
<span>private</span> <span>sendToH5</span>(<span>type</span>: <span>string</span>, <span>payload</span>: <span>string</span>): <span>void</span> {
<span>const</span> raw = <span>JSON</span>.<span>stringify</span>({ <span>type</span>: <span>type</span>, <span>payload</span>: payload });
<span>this</span>.<span>controller</span>.<span>runJavaScript</span>(<span>`window.onNativeMessage(<span>${<span>JSON</span>.stringify(raw)}</span>)`</span>);
}
runJavaScript 的回调拿到的是脚本的返回值字符串。要从网页读数据时,让脚本 return JSON.stringify(...),回调里再 parse。异步函数要在脚本里自己 await 完,最后 return 一个 JSON 字符串,不要把 Promise 对象当结果。
原生把 token 推给 H5、刷新网页里某块数据、通知网页「用户点了原生按钮」,都走这个方向。它灵活,但是在拼脚本,所以脚本内容只来自业务常量或已经校验过的 JSON,不要把 H5 刚传回来的原文再执行一遍。
3. 一次业务里两边怎么配合
以「原生已登录,打开 H5,H5 要 token,并且能通知原生关页」为例,顺序是固定的:
sequenceDiagram
participant N as 原生页面
participant W as Web
participant H as H5
N->>W: 创建控制器并加载 src
W->>H: 解析页面
W-->>N: onPageEnd
N->>H: runJavaScript onNativeReady
H->>N: window.nativeApi.getToken
N-->>H: JSON 字符串
H->>N: window.nativeApi.closePage
N->>N: 退出导航栈
对应到代码上就是上一节的三块:页面持有控制器,javaScriptProxy 提供 getToken / closePage,onPageEnd 里调用 onNativeReady。H5 在 onNativeReady 里再去读 token。不要在 Vue 的 created 里假设 nativeApi 已经存在;注入时机和文档加载有关,就绪通知以 onPageEnd 为准。
methodList 里只留业务方法:取 token、关页、把 { type, payload } 交给原生。不要留「执行任意脚本」。
四、用的时候先查这些
- 白屏:看
src和onErrorReceive。桥还没开始。 nativeApi is undefined:javaScriptAccess没开,或name和网页里写的不一致。- 方法存在但调用报错:方法没写进
methodList。 - 原生调 H5 没反应:早于
onPageEnd,或 H5 还没把函数挂到window。 - 对面拿到
[object Object]:过桥前没有JSON.stringify。 - H5 里
localStorage写不进:没开domStorageAccess。 - 点返回只在网页历史里跳:关页要走原生导航,不是
history.back()。
五、结论
应用嵌 H5 用 Web:页面里放组件,一个控制器,打开 JS。通信是两条线,H5 调 window 上的白名单方法,原生在 onPageEnd 之后用 runJavaScript。数据只走 JSON 字符串。
元服务是另一套 postMessage,和这套不要混用。
把两套容器差异与桥接顺序讲得很清楚,双向通信、JSON 过桥、排查清单都落到实操,适合准备把已有 Vue H5 嵌入鸿蒙应用的开发者对照落地。