Administrator
发布于 2026-09-02 / 3,466 阅读
0
0

鸿蒙 ArkWeb 实战:Web 组件嵌入、JS Bridge 与混合开发

系列文章《鸿蒙开发从入门到专家》第 41 篇 · 中级

前 40 篇我们把鸿蒙原生开发的主干道走了一遍。从第 41 篇开始,进入"基础补强"阶段——把那些容易被忽略、但在真实项目中天天遇到的技术点补齐。

第一个就是 Web 组件。

现实中,纯原生开发是理想,混合开发是日常。活动页、协议页、营销弹窗、第三方 H5 接入,这些页面用 WebView 加载比用 ArkUI 重写快得多。鸿蒙的 ArkWeb Kit 提供了完整的 Web 组件能力,但很多人只用了最基础的 loadUrl,遇到 JS 通信、请求拦截、页面预加载就卡壳。

这篇从 Web 组件的基础用法写到 JS Bridge 的工程化封装,覆盖混合开发中最高频的几个场景。

一、Web 组件的基本用法

1.1 创建 Web 组件

Web 组件的使用分两步:创建 WebviewController 实例,然后在 Web 组件中绑定它。

import { webview } from '@kit.ArkWeb';

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

  build() {
    Column() {
      Web({ src: 'https://www.example.com', controller: this.controller })
        .domStorageAccess(true)
        .width('100%')
        .height('100%')
    }
  }
}

加载网络页面需要在 module.json5 中声明网络权限:

"requestPermissions": [
  {
    "name": "ohos.permission.INTERNET"
  }
]

1.2 三种加载方式

Web 组件支持三种内容加载方式,适用场景各不相同:

方式

API

适用场景

网络页面

src 或 loadUrl(url)

在线网页、H5 活动页

本地文件

src: $rawfile('index.html')

离线包、内置 H5 资源

HTML 片段

loadData(html, mimeType, encoding)

动态生成 HTML、富文本展示

本地文件方式特别适合"离线包"模式——把前端资源打包到 rawfile 目录,通过 $rawfile 加载。好处是不依赖网络,坏处是更新需要发版。

// 加载本地 HTML
Web({ src: $rawfile('web/index.html'), controller: this.controller })

// 加载 HTML 片段(常用于展示后端返回的富文本)
Button('loadData').onClick(() => {
  this.controller.loadData(
    '<html><body><h1>Hello ArkWeb</h1></body></html>',
    'text/html',
    'UTF-8'
  );
})

1.3 生命周期回调

Web 组件提供了一套完整的生命周期回调,理解它们的触发顺序对调试和性能优化很关键:

onControllerAttached → onLoadIntercept → onInterceptRequest 
→ onPageBegin → onProgressChange → onPageVisible → onPageEnd

回调

触发时机

典型用途

onControllerAttached

控制器绑定到组件

初始化配置、注入 JS 对象

onLoadIntercept

加载 URL 前

拦截跳转(如白名单校验)

onInterceptRequest

资源请求前

本地资源替换、请求劫持

onPageBegin

页面开始加载

显示加载进度条

onProgressChange

加载进度变化

更新进度条数值

onPageEnd

页面加载完成

隐藏进度条、执行 JS

onPageVisible

页面内容开始可见

埋点、上报 PV

一个实战建议:把 onControllerAttached 作为注入 JS 代理对象的入口。如果放在 onPageBegin 里,首帧可能已经渲染,代理对象还没就绪,导致 H5 调用时机不对。

二、JS Bridge:原生与 Web 的双向通信

混合开发的核心问题就是通信。ArkWeb 提供了两种通信机制,选错了会很痛苦。

2.1 两种通信方式对比

机制

方向

特点

适用场景

javaScriptProxy

ArkTS → JS

注册对象到 Web 上下文

H5 调原生方法

createWebMessagePorts

双向

消息端口,支持双向通信

实时通信、长连接

2.2 javaScriptProxy:注册原生方法给 H5 调用

这是最常用的方式。原生侧注册一个代理对象,H5 直接用对象名调用方法。

Web({ src: $rawfile('web/index.html'), controller: this.controller })
  .javaScriptProxy({
    object: this,
    name: 'appBridge',
    methodList: ['getToken', 'closeWebPage', 'openNativePage'],
    controller: this.controller,
    asyncMethodList: ['fetchUserData'],  // 异步方法单独声明
  })

H5 侧直接调用:

// H5 代码
document.getElementById('btn').onclick = () => {
  // 同步调用
  const token = appBridge.getToken();
  console.log('token from native:', token);

  // 异步调用(返回 Promise)
  appBridge.fetchUserData().then(data => {
    console.log('user data:', data);
  });
};

原生侧实现:

// 同步方法
getToken(): string {
  return AppStorage.get<string>('userToken') || '';
}

// 异步方法
async fetchUserData(): Promise<string> {
  const response = await fetch('https://api.example.com/user');
  return await response.text();
}

一个容易踩的坑:javaScriptProxy 注册的方法名必须在 methodList 或 asyncMethodList 中声明。没声明的方法,H5 调用会直接报 “function is not defined”。

2.3 消息端口:双向实时通信

消息端口适合需要频繁双向通信的场景,比如 H5 页面持续上报数据给原生,原生再回传处理结果。

// 原生侧
private ports: webview.WebMessagePort[] = [];

setupMessagePort() {
  this.ports = this.controller.createWebMessagePorts();
  // ports[0] 给 Web 侧使用(通过 postMessage 传递)
  // ports[1] 留在原生侧收发消息
  this.controller.postMessage('init', [this.ports[0]], '*');

  // 监听 Web 侧发来的消息
  this.ports[1].onMessageEvent((msg: webview.WebMessage) => {
    if (msg.getData()) {
      const data = msg.getData();
      console.info('Received from web:', data);
    }
  });
}

// 发送消息给 Web 侧
sendToWeb(message: string) {
  this.ports[1].postMessageEvent(message);
}

H5 侧:

let port = null;
window.addEventListener('message', (e) => {
  port = e.ports[0];
  port.onmessage = (m) => {
    console.log('Received from native:', m.data);
  };
});

// 发送消息给原生
document.getElementById('send').onclick = () => {
  port.postMessage('Hello from H5');
};

2.4 runJavaScript:原生主动调 H5 函数

如果原生侧需要主动触发 H5 的逻辑(比如原生推送到达后通知页面刷新),用 runJavaScript:

this.controller.runJavaScript('refreshData()').then(result => {
  console.info('JS execution result:', result);
});

注意 runJavaScript 要在 onPageEnd 之后调用,页面没加载完就执行 JS 会报错。

三、请求拦截:本地资源替换

混合开发中一个高频需求:H5 引用了远程 JS/CSS 资源,但你想用本地缓存的版本替换它,加速加载或做离线支持。

onInterceptRequest 就是干这个的。

Web({ src: 'https://m.example.com', controller: this.controller })
  .onInterceptRequest((event) => {
    const url = event.request.getRequestUrl();

    // 拦截特定路径,返回本地资源
    if (url.includes('/assets/app.js')) {
      const response = new WebResourceResponse();
      response.setResponseHeader([
        { headerName: 'Content-Type', headerValue: 'application/javascript' }
      ]);
      response.setResponseData($rawfile('web/app.js'));
      response.setResponseEncoding('utf-8');
      response.setResponseMimeType('application/javascript');
      return response;
    }

    // 不拦截,正常请求
    return null;
  })

这个能力在以下场景特别有用:

  • 离线包加速:把核心 JS/CSS 打包到本地,远程页面引用时直接命中本地缓存。

  • 域名替换:测试环境把生产域名替换成测试域名。

  • 资源注入:给页面注入自定义的 JS,比如埋点 SDK。

但要注意:拦截所有请求会影响性能。只拦截确实需要替换的资源,其他放行。

四、预加载与性能优化

4.1 预热 Web 引擎

Web 组件首次创建时,Chromium 内核需要初始化,这个过程可能花 200-500ms。ArkWeb 提供了 initializeWebEngine 静态方法,可以在 UIAbility.onCreate 阶段预热引擎:

import { webview } from '@kit.ArkWeb';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant): void {
    // 预热 Web 引擎,后续创建 Web 组件时直接复用
    webview.WebviewController.initializeWebEngine();
  }
}

这招对首屏有 Web 组件的页面效果明显。预热后 Web 组件创建时间能减少到 50ms 以内。

4.2 多 Web 组件复用

如果应用里有多个页面都用到 Web 组件,不要每个页面都新建 WebviewController。可以在父组件或 Ability 级别创建一个共享的 controller,页面切换时 loadUrl 即可。

@Entry
@Component
struct WebContainer {
  // 共享 controller
  controller: webview.WebviewController = new webview.WebviewController();

  @State currentUrl: string = '';

  build() {
    Stack() {
      Web({ src: this.currentUrl, controller: this.controller })
    }
  }

  // 页面切换时调用
  loadPage(url: string) {
    this.currentUrl = url;
    this.controller.loadUrl(url);
  }
}

五、安全注意事项

混合开发最容易出安全问题的环节是 JS Bridge。几条原则:

1. 方法白名单。 javaScriptProxy 的 methodList 只声明确实需要暴露的方法。别图省事把整个 this 注册上去。

2. URL 白名单。 onLoadIntercept 里校验加载的 URL,拒绝非白名单域名:

.onLoadIntercept((event) => {
  const url = event.data.getUrl();
  const whitelist = ['example.com', 'cdn.example.com'];
  const isAllowed = whitelist.some(domain => url.includes(domain));
  return !isAllowed;  // true 表示拦截
})

3. 敏感数据脱敏。 H5 调原生方法拿到的 token、用户信息,不要直接 console.log 打到 Web 控制台,防止被第三方页面读取。

小结

Web 组件的混合开发可以总结为三件事:加载页面、双向通信、拦截请求。

  • 加载用 loadUrl / loadData / $rawfile,预热引擎减少首屏延迟。

  • 通信用 javaScriptProxy(同步/异步方法注册)和消息端口(双向实时)。

  • 拦截用 onInterceptRequest 做本地资源替换,用 onLoadIntercept 做安全校验。

下一篇我们聊 Stage 模型与 FA 模型的区别。理解了应用模型,才能理解为什么 Web 组件的生命周期要和 UIAbility 绑定。


评论