欢迎光临
我们一直在努力

明明是原生 App,为什么你硬要塞个 H5?——鸿蒙 Web 组件混合开发实战

大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~

本文目录:

    • 前言
    • 一、Web 组件 API:先把“浏览器这块砖”掌握住
      • 1.1 最小可用 Web 组件示例
      • 1.2 WebviewController 能干啥?
    • 二、JS ↔ ArkTS 双向交互:Hybrid 的灵魂
      • 2.1 ArkTS 调 JS:用 runJavaScript 注入
      • 2.2 JS 调 ArkTS:建立“消息通道”
      • 2.3 一份常用交互约定建议
    • 三、资源加载问题:本地 html、图片、缓存那些坑
      • 3.1 本地 html 加载
      • 3.2 远程资源加载与跨域
      • 3.3 缓存与刷新
    • 四、Hybrid 应用开发案例:一套“原生 + H5 活动中心”的组合拳
      • 4.1 页面结构设计
      • 4.2 登录态注入:ArkTS → JS
      • 4.3 JS 调原生分享
      • 4.4 Hybrid 工程结构建议
    • 最后一点小感慨

前言

说句实在话,只写原生的人,迟早要面对 H5;只写 H5 的人,也迟早会被拉去接原生。不管你情不情愿,这年头“Hybrid 应用”已经是常态——活动页是前端同学的一套 Vue/React,主流程又是原生 ArkUI;老板一句“这个网页能不能直接嵌进去?”你就懂了:跑不掉了。

好消息是:鸿蒙给我们准备好了 Web 组件,用来做内嵌网页、Hybrid 混合开发。一套视图里,你既可以用 ArkTS 写原生 UI,又能在一个区域里嵌一整块 H5,甚至还能 ArkTS ↔ JS 互相发消息,做登录、分享、设备能力调用这一整套联动。

但是啊,真要落地的时候,问题就出来了:

  • Web 组件到底有哪些 API?能不能控制前进、后退、注入 JS?
  • JS 到底怎么跟 ArkTS 说话?Native 又怎么把数据塞给网页?
  • 本地 html / 资源加载老报错:404、跨域、白屏、缓存问题一大堆?
  • 做 Hybrid 项目到底怎么设计结构,才能不变成“奇怪的大杂烩”?

今天就来好好聊聊这件事: 鸿蒙 Web 组件:嵌入网页与混合开发的完整实践。

一、Web 组件 API:先把“浏览器这块砖”掌握住

鸿蒙里用 Web 做嵌入网页,核心是 ArkUI 的 <Web> 组件 + WebController 控制对象。

1.1 最小可用 Web 组件示例

先来个最小 demo,有个感觉:

import webview from '@ohos.web.webview';

@Entry
@Component
struct WebDemoPage {
private controller: webview.WebviewController = new webview.WebviewController();

build() {
Column() {
// 顶部简单导航栏
Row() {
Button('Back')
.onClick(() => {
if (this.controller?.canGoBack()) {
this.controller.goBack();
}
})
Button('Reload')
.margin({ left: 8 })
.onClick(() => this.controller.reload())
}.margin({ top: 8, left: 12 })

// Web 区域
Web({ src: 'https://www.example.com', controller: this.controller })
.javaScriptAccess(true) // 允许 JS
.zoomAccess(true) // 允许缩放
.domStorageAccess(true) // 允许 DOM Storage
.width('100%')
.height('100%')
}
.width('100%')
.height('100%')
}
}

几个关键点:

  • Web({ src, controller }):

    • src 可以是远程 URL,也可以是本地 html
    • controller 用来做“浏览器导航控制”
  • .javaScriptAccess(true):不开这个,很多 H5 功能直接废掉

  • .domStorageAccess(true):涉及 localStorage / sessionStorage 的页面要开

  • .zoomAccess(true):是否允许手势缩放(视业务看要不要)

可以把它理解成:ArkUI 里嵌了一个小浏览器窗口,你再用控制器来对它下命令。


1.2 WebviewController 能干啥?

一般会这么写:

private controller: webview.WebviewController = new webview.WebviewController();

常用 API 大概这些(只列最常用的,日常开发够用):

  • loadUrl(url: string):动态加载网页
  • reload():刷新当前页
  • canGoBack() / goBack():后退
  • canGoForward() / goForward():前进
  • stop():停止加载
  • zoomIn() / zoomOut():缩放
  • runJavaScript(script: string, callback?):执行 JS

比如点击按钮动态切换 H5 页面:

Button('打开活动页')
.onClick(() => {
this.controller.loadUrl('https://m.xxx.com/promo/2025-11-11');
})

或者:

Button('调用 H5 函数')
.onClick(() => {
this.controller.runJavaScript('window.fromNative && window.fromNative("hello")');
})

至此,你就有了一个“听得懂你指挥”的内嵌网页。


二、JS ↔ ArkTS 双向交互:Hybrid 的灵魂

只嵌页面不交互,其实只能算“简单内嵌”; 真正的 Hybrid,一定离不开:

  • ArkTS 调 JS:比如通知网页登录结果、传 token、传用户信息
  • JS 调 ArkTS:比如让原生弹出分享、打开扫码、调原生支付

2.1 ArkTS 调 JS:用 runJavaScript 注入

刚刚提过的 runJavaScript,是最直接、最常用的方式:

ArkTS 侧:

sendUserInfoToWeb(user: { id: string; nickname: string }) {
const json = JSON.stringify(user);
const script = `window.onNativeUserInfo && window.onNativeUserInfo(${json})`;
this.controller.runJavaScript(script);
}

H5 页面中:

// 在 H5 js 里
window.onNativeUserInfo = function (user) {
console.log('收到来自原生的用户信息:', user)
// 比如更新页面 UI
document.getElementById('nickname').innerText = user.nickname
}

这种方式非常直接:ArkTS 拼出一段 JS 字符串,让 Webview 去执行。 适合“简单调用 + 传少量参数”。


2.2 JS 调 ArkTS:建立“消息通道”

从网页回调到 ArkTS,通常会有两种思路:

  • URL 拦截

    • JS 修改 location,例如 myapp://doSomething?xxx=yyy
    • ArkTS 侧通过 Web 的 URL 拦截回调解析
  • 消息机制(推荐)

    • 通过 webview 的消息机制派发数据
  • 下面以一种常见的“约定 URL 协议 + 拦截”方式举例(思路清晰易懂):

    H5 页面这样调用:

    function callNativeShare(data) {
    const encoded = encodeURIComponent(JSON.stringify(data))
    window.location.href = `myapp://share?data=${encoded}`
    }

    ArkTS 侧监听 URL 变化(具体 API 名称依版本有所差异,这里用伪代码表达下思路):

    Web({ src: this.url, controller: this.controller })
    .onUrlLoad((event) => {
    const url = event.url;
    if (url.startsWith('myapp://')) {
    this.handleJsBridge(url);
    // 阻止 Webview 继续加载这个“假页面”
    event.preventDefault && event.preventDefault();
    }
    })

    handleJsBridge 解析协议:

    handleJsBridge(url: string) {
    // myapp://share?data=xxx
    const [schemaPart, queryPart] = url.split('?');
    if (schemaPart === 'myapp://share') {
    const searchParams = new URLSearchParams(queryPart);
    const data = searchParams.get('data');
    if (data) {
    const payload = JSON.parse(decodeURIComponent(data));
    this.doShare(payload);
    }
    }
    }

    doShare 就是原生侧的分享逻辑:跳系统分享、调 SDK 都可以。

    这种写法优点:

    • 前端同学很好理解,只要改 URL 就行
    • ArkTS 这边只要解析 URL 即可
    • 不容易乱七八糟

    当然,如果后续用到官方提供的 MessagePort、WebMessage 之类更正式的通道也可以,思路是一样的:双方约定协议 + 明确函数名 + 数据格式 JSON 化。


    2.3 一份常用交互约定建议

    为了不搞成一团糊,建议团队约定个简单的“JSBridge 协议”,比如:

    • JS → Native:myapp://action?data=xxx

      • action:动作名称,比如 login, share, openScanner
      • data:JSON 字符串,需 encodeURIComponent
    • Native → JS:统一通过 window.onNativeMessage && window.onNativeMessage({ type, payload })

    这样你的通信就会统一清晰得多:

    ArkTS:

    sendToWeb(type: string, payload: any) {
    const json = JSON.stringify({ type, payload });
    const script = `window.onNativeMessage && window.onNativeMessage(${json})`;
    this.controller.runJavaScript(script);
    }

    H5:

    window.onNativeMessage = function (msg) {
    switch (msg.type) {
    case 'loginSuccess':
    // …
    break
    case 'refreshToken':
    // …
    break
    }
    }


    三、资源加载问题:本地 html、图片、缓存那些坑

    实际项目里,很少所有内容都在远端,有很多 Hybrid 场景会用到:

    • 本地打包 html 页面(比如某个内置帮助页、离线页面)
    • H5 里引用的静态资源(图片、css、js)
    • 缓存 / 刷新 / 404 / 白屏 等问题

    3.1 本地 html 加载

    假设你在工程 resources/rawfile 下放了一个 help/index.html:

    项目结构大概这样:

    entry
    ├── src
    └── resources
    └── rawfile
    └── help
    └── index.html

    ArkTS 里可以这样加载(不同版本写法略有区别,示意):

    import webview from '@ohos.web.webview';
    import resourceManager from '@ohos.resourceManager';

    @Entry
    @Component
    struct LocalHtmlPage {
    private controller: webview.WebviewController = new webview.WebviewController();

    aboutToAppear() {
    // 有些版本支持直接写特殊协议,如 'file://…' 或 'rawfile://help/index.html'
    this.controller.loadUrl('rawfile://help/index.html');
    }

    build() {
    Web({ src: '', controller: this.controller })
    .javaScriptAccess(true)
    .width('100%')
    .height('100%')
    }
    }

    如果你遇到“本地 html 里的 js/css 404”,别慌:

    • 检查资源路径是否相对正确(./js/main.js vs /js/main.js)
    • 确保 html 所在路径和资源目录结构对应
    • 有些场景下需要使用 loadData 来加载 html 字符串与 baseUrl

    3.2 远程资源加载与跨域

    常见问题:

  • H5 引用了第三方接口,报跨域
  • 一些图片 / 脚本走 http 而页面是 https,会有“混合内容”警告
  • Cookie / Storage 导致登录态混乱
  • 一般 Hybrid 项目里,这些应该让前端统一配置:

    • 尽量全站 https
    • 域名统一(或 CORS 设置完整)
    • 不在 H5 里随便写死业务域名,而是通过配置或注入方式

    ArkTS 这边需要注意的更多是:

    • 不乱把 Web 当浏览器那样打开任何地址
    • 对加载失败提供 fallback 页面或错误提示

    3.3 缓存与刷新

    很多 Hybrid 活动页会遇到一个经典问题:

    “我们后台刚改完 H5,为什么用户还看到旧版本?”

    因为缓存。

    几种常见手段:

    • H5 侧给静态资源加版本号:main.js?v=20251114
    • 活动页面 URL 自身也带上版本:/promo/2025?v=2
    • ArkTS 这边在必要时调用 controller.reload() 强刷

    更激进一点:

    • 为调试提供一个隐藏长按入口:长按某个区域 3 秒 → 出现“清缓存 / 刷新 H5 配置”的选项

    四、Hybrid 应用开发案例:一套“原生 + H5 活动中心”的组合拳

    说了这么多,咱们用一个稍微贴近实战的案例,把整个流程串一下:

    场景: 你在做一个电商类 App,首页、商品详情、购物车等都是 ArkUI 原生写的。 运营同学三天两头丢一个“营销活动 H5 地址”给你,要你接入:

    • 需要在 App 里打开
    • 要能拿到用户登录态
    • H5 里点击“立即分享”要调原生分享
    • 有时要调起原生支付

    这是非常典型的 Hybrid 活动中心 场景。


    4.1 页面结构设计

    可以有一个原生页面:

    • 标题栏:原生
    • 网页区域:Web 组件
    • 底部可以视情况扩展一些原生按钮(比如“关闭”、“回到首页”)

    @Entry
    @Component
    struct HybridActivityPage {
    private controller: webview.WebviewController = new webview.WebviewController();
    private url: string = '';

    aboutToAppear() {
    // 假设路由带进来一个活动地址
    const params = router.getParams() as Record<string, string>;
    this.url = params?.url ?? '';
    }

    build() {
    Column() {
    // 原生头部
    Row() {
    Button('< 返回')
    .onClick(() => {
    if (this.controller.canGoBack()) {
    this.controller.goBack();
    } else {
    router.back();
    }
    })
    Text('活动页面')
    .fontSize(18)
    .margin({ left: 12 })
    }
    .height(48)
    .padding({ left: 12, right: 12 })
    .backgroundColor(0xFFFFFF)

    // Web 区域
    Web({ src: this.url, controller: this.controller })
    .onPageEnd((res) => {
    console.info('页面加载完成', JSON.stringify(res));
    })
    .width('100%')
    .height('100%')
    }
    .width('100%')
    .height('100%')
    }
    }


    4.2 登录态注入:ArkTS → JS

    用户登录在原生侧完成,H5 又想知道当前用户是谁,这个场景非常普遍。

    进入活动页后,可以这样做:

    ArkTS:

    aboutToAppear() {
    // 省略获取 url 逻辑
    // 页面加载完后再注入用户信息
    this.controller.on('pageEnd', () => {
    const user = { id: 'u123', token: 'xxxx', nick: 'Mark' };
    const json = JSON.stringify(user);
    const script = `window.onNativeLogin && window.onNativeLogin(${json});`;
    this.controller.runJavaScript(script);
    });
    }

    H5:

    window.onNativeLogin = function (user) {
    // 存本地,后续接口请求带上
    localStorage.setItem('token', user.token)
    renderUserName(user.nick)
    }

    这样就完成了最基础的 Hybrid 登录态打通。


    4.3 JS 调原生分享

    H5 里很多“分享活动给好友”按钮,其实都希望调用 App 原生的分享面板,而不是简单用 H5 的那套。

    H5 侧统一这样调用:

    function triggerShare() {
    const payload = {
    title: '双十一神券大放送',
    desc: '全场满 199 减 100',
    url: window.location.href,
    icon: '2026-01-29ren54pzc315.png'
    }
    const encoded = encodeURIComponent(JSON.stringify(payload))
    window.location.href = `myapp://share?data=${encoded}`
    }

    ArkTS 侧拦截:

    Web({ src: this.url, controller: this.controller })
    .onUrlLoad((event) => {
    const url = event.url;
    if (url.startsWith('myapp://')) {
    this.handleBridge(url);
    event.preventDefault && event.preventDefault();
    }
    })

    handleBridge(url: string) {
    const [schemaPart, queryPart] = url.split('?');
    const params = new URLSearchParams(queryPart);
    const data = params.get('data');
    const payload = data ? JSON.parse(decodeURIComponent(data)) : null;

    if (schemaPart === 'myapp://share' && payload) {
    this.doShare(payload);
    }
    }

    doShare(payload: { title: string; desc: string; url: string; icon?: string }) {
    // 这里调系统分享 / 三方 SDK / 自家分享面板都可
    console.info('执行原生分享:', JSON.stringify(payload));
    }

    这样,H5 → Native 的动作就非常顺了。


    4.4 Hybrid 工程结构建议

    为了不让项目乱成一锅粥,结构上建议这样:

    entry/src/main/ets
    ├── ability
    │ └── MainAbility.ets
    ├── pages
    │ ├── HomePage.ets
    │ ├── ProductDetailPage.ets
    │ └── HybridActivityPage.ets // 专门承载 H5
    ├── hybrid
    │ ├── bridge
    │ │ └── JsBridgeHandler.ets // 统一处理 URL 协议、消息分发
    │ └── config
    │ └── HybridConfig.ets // 白名单域名、特殊路由策略等
    ├── services
    │ └── api
    └── …
    resources
    └── rawfile
    └── hybrid
    └── offline.html // 断网时展示的本地 H5

    几条原则:

    • ArkTS 与 JS 交互逻辑集中管理 → hybrid/bridge
    • 域名白名单 / 特殊跳转策略单独配置 → 防止随便加载未知地址
    • 离线兜底页(offline html)单独放 rawfile
    • Web 组件最好封装成一个复用组件,例如 HybridWebView,不要在每个页面重复写乱七八糟的逻辑

    最后一点小感慨

    混合开发这东西,说简单也简单,说复杂也复杂。简单是因为技术上无非就那几个点: 嵌 Web、控导航、搞交互、管资源。 复杂是因为你得协调: 前端、客户端、服务端、运营的各种诉求,既要“随时能改 H5”,又要“体验接近原生”。

    鸿蒙给了我们一个不错的工具:Web 组件 + WebviewController + 通信能力,剩下的就看你怎么把这些拼成一个有秩序、好维护的 Hybrid 体系。

    如果你能把今天这几块思路吃透:

    • Web 组件 API:怎么嵌、怎么控、能做哪些事
    • JS ↔ ArkTS 通信:怎么设计协议、怎么落地实现
    • 本地 / 远程资源加载要注意什么、怎么防坑
    • Hybrid 案例:登录态、分享、活动中心怎么串起来

    那你在鸿蒙这边搞 Hybrid,基本就算是“入门 + 实战”一条龙了。

    如果觉得有帮助,别忘了点个赞+关注支持一下~ 喜欢记得关注,别让好内容被埋没~

    赞(0)
    未经允许不得转载:171主机测评 » 明明是原生 App,为什么你硬要塞个 H5?——鸿蒙 Web 组件混合开发实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址