《Web 到 HarmonyOS》-- 嵌入 Web 与桥接基础

文章来源声明: 原文作者:Nayana; 来源站点:掘金; 原文链接:https://juejin.cn/post/7686746122727407650; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

把两套容器差异与桥接顺序讲得很清楚,双向通信、JSON 过桥、排查清单都落到实操,适合准备把已有 Vue H5 嵌入鸿蒙应用的开发者对照落地。

> 系列:《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({ src, controller })`,`src` 可以是网络地址或 `$rawfile```,网络地址要先在 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 一个去调它。

<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`没打开。先查地址、证书、混合内容,不要先改桥

推荐的使用顺序:

  1. 页面创建 WebviewControllerbuild() 里放 Web,打开 javaScriptAccess
  2. 需要存储就再开 domStorageAccess
  3. onPageEnd,通知 H5「桥可以用了」。
  4. 之后原生用 runJavaScript 调 H5;H5 用注入对象调原生。
  5. 用户要离开这块网页时,走原生导航栈,不要只调 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 / closePageonPageEnd 里调用 onNativeReady。H5 在 onNativeReady 里再去读 token。不要在 Vue 的 created 里假设 nativeApi 已经存在;注入时机和文档加载有关,就绪通知以 onPageEnd 为准。

methodList 里只留业务方法:取 token、关页、把 { type, payload } 交给原生。不要留「执行任意脚本」。


四、用的时候先查这些

  1. 白屏:看 srconErrorReceive。桥还没开始。
  2. nativeApi is undefinedjavaScriptAccess 没开,或 name 和网页里写的不一致。
  3. 方法存在但调用报错:方法没写进 methodList
  4. 原生调 H5 没反应:早于 onPageEnd,或 H5 还没把函数挂到 window
  5. 对面拿到 [object Object]:过桥前没有 JSON.stringify
  6. H5 里 localStorage 写不进:没开 domStorageAccess
  7. 点返回只在网页历史里跳:关页要走原生导航,不是 history.back()

五、结论

应用嵌 H5 用 Web:页面里放组件,一个控制器,打开 JS。通信是两条线,H5 调 window 上的白名单方法,原生在 onPageEnd 之后用 runJavaScript。数据只走 JSON 字符串。

元服务是另一套 postMessage,和这套不要混用。