系列:《Web 到 HarmonyOS》· 插播(不占正篇序号)
文档性质:方案整理,不是本仓库的集成记录
适合人群:会 Vue / iframe /
postMessage,准备把已有 H5 嵌进鸿蒙
写在前面
嵌网页有两套容器。应用里用 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-view> |
| 打开页面 | Web({ src, controller }),src 可以是网络地址或 $rawfile |
<web-view src="{{url}}">,网络地址要先在 AGC 配业务域名 |
| 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 一个去调它。
ts
import { webview } from '@kit.ArkWeb';
@Entry
@Component
struct H5Page {
private controller: webview.WebviewController = new webview.WebviewController();
build() {
Column() {
Web({ src: 'https://example.com/h5/', controller: this.controller })
.javaScriptAccess(true)
.domStorageAccess(true)
.onPageEnd(() => {
this.notifyH5Ready();
})
.onErrorReceive(() => {
// 页没打开,不要在这里调桥
})
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
}
private notifyH5Ready(): void {
this.controller.runJavaScript('window.onNativeReady && window.onNativeReady()');
}
}
使用时先打开这几个开关,再谈桥:
| 配置 | 不开会怎样 | 对应的 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 上执行一段脚本 |
两边约定同一种消息,参数和返回值都用字符串:
ts
interface BridgeMessage {
type: string;
payload: string;
}
1. H5 调原生:注入 window 上的对象
在 Web 上声明代理。name 是挂到 window 上的名字,methodList 是允许 H5 调用的方法。没写进名单的方法,网页调不到。
ts
Web({ src: 'https://example.com/h5/', controller: this.controller })
.javaScriptAccess(true)
.javaScriptProxy({
name: 'nativeApi',
methodList: ['getToken', 'closePage', 'postMessage'],
controller: this.controller,
object: {
getToken: (): string => {
return JSON.stringify({ token: '原生登录态' });
},
closePage: (): void => {
// 退出当前原生页
},
postMessage: (raw: string): string => {
const msg = JSON.parse(raw) as BridgeMessage;
// 按 msg.type 分发
return JSON.stringify({ ok: true });
}
}
})
H5 里就是普通函数调用。先判断对象在不在,再 parse 返回值:
js
function readToken() {
if (!window.nativeApi || !window.nativeApi.getToken) {
return;
}
const data = JSON.parse(window.nativeApi.getToken());
sessionStorage.setItem('token', data.token);
}
function tellNative(type, payload) {
window.nativeApi.postMessage(JSON.stringify({
type: type,
payload: JSON.stringify(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') 再等消息是同一顺序:
js
window.onNativeReady = function () {
readToken();
};
window.onNativeMessage = function (raw) {
const msg = JSON.parse(raw);
const payload = JSON.parse(msg.payload);
// 按 msg.type 更新页面
};
原生必须等 onPageEnd。在这之前函数还不在。参数用 JSON.stringify 放进脚本,避免引号把脚本截断:
ts
private sendToH5(type: string, payload: string): void {
const raw = JSON.stringify({ type: type, payload: payload });
this.controller.runJavaScript(`window.onNativeMessage(${JSON.stringify(raw)})`);
}
runJavaScript 的回调拿到的是脚本的返回值字符串。要从网页读数据时,让脚本 return JSON.stringify(...),回调里再 parse。异步函数要在脚本里自己 await 完,最后 return 一个 JSON 字符串,不要把 Promise 对象当结果。
原生把 token 推给 H5、刷新网页里某块数据、通知网页「用户点了原生按钮」,都走这个方向。它灵活,但是在拼脚本,所以脚本内容只来自业务常量或已经校验过的 JSON,不要把 H5 刚传回来的原文再执行一遍。
3. 一次业务里两边怎么配合
以「原生已登录,打开 H5,H5 要 token,并且能通知原生关页」为例,顺序是固定的:
对应到代码上就是上一节的三块:页面持有控制器,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,和这套不要混用。