【HarmonyOS】ArkWeb——从入门到入土

1.使用场景

  • 在应用内集成Web页面:应用可以在页面中使用Web组件,嵌入Web页面,从而降低开发成本
  • 浏览器网页浏览场景:浏览器类应用可以使用Web组件来打开第三方网页
  • 小程序:应用内嵌小程序功能的应用可以使用web组件来渲染小程序页面

2.ArkWeb进程

ArkWeb是多进程模型,分为应用进程、web渲染进程、webGpu渲染进程、web孵化进程和Foundation进程

  • 应用进程中的web进程(应用唯一)
    • 应用进程为主进程,包含UI主线程和Web的相关线程。包含网络线程、Video线程、Audio线程和IO线程
  • Foundation进程(系统唯一)
    • 负责接收应用进程要创建并启动一个新进程的请求,管理应用进程和web渲染进程的绑定关系
      这里的创建并启动并非从0开始创建,而是由一定的基础
  • Web孵化进程(系统唯一)
    • 负责接收Foundation进程的请求,执行孵化Web渲染进程与WebGpu进程
    • 孵化完毕后对新的进程进行权限降级处理,并且预加载一些动态库,以提升运行速度
  • Web渲染进程(应用可指定多Web实例之间共享或独立进程)
    • 负责运行Web渲染进程引擎
    • 负责运行ArkWeb执行引擎
    • 提供接口供应用选择多web实例之间是否共享渲染进程
  • WebGpu进程(应用唯一)
    • 指挥GPU进行光栅化等底层硬件相关操作

3.Web组件的生命周期

  • aboutToAppear:创建新自定义组件实例之后,在执行build前执行
  • onControllerAttached:当Controller成功绑定时触发该回调
  • onLoadIntercept:web组件加载url之前触发该回调,判断是否阻止此次访问
  • onInterceptRequest:web组件加载url之间触发该回调,用于拦截url并返回一个自定义的响应数据
  • onPageBegin:网页开始加载时触发该回调
  • onProgressChange:告知开发者当前页面加载进度
  • onPageEnd:网页加载完成触发该回调

4.Web组件的渲染和布局

web组件可以使用layoutMode(WebLayoutMode.FIT_CONTENT)属性实现组件高度跟随页面内容自适应变化

4.1异步渲染模式(默认)

在异步渲染模式下,web组件作为图形surface节点,独立送显。

建议在仅由web组件构成的页面中使用此模式,以提高性能

限制

  • web组件的宽高不能超过7680px,超过会导致白屏
  • 不支持动态切换模式

当前页面由web组件作为主体显示应用页面,web组件仅需占满手机屏幕大小即可,超出的H5页面部分ArkWeb会自动生成滚动条,便于滑动浏览

4.2同步渲染模式

同步渲染模式下,web组件作为图形canvas节点,web渲染跟随系统组件一起送显,可以渲染更长的web组件内容

建议在web组件与其他ArkUI组件共同滑动交互时使用

当前页面有web组件和ArkUI组件共同组成,此时H5界面与Web组件的高度需要一致,web内部不生成滚动条,

作为一个超长组件展示,通过Scroll组件实现应用内部的滚动,确保用户平滑浏览

5.应用中使用前端页面的JS

5.1应用侧调用前端页面函数

应用侧可以通过runJavaScript和runJavaScriptExt方法来调用前端页面的JS相关函数

参数类型有以下差异

  • runJavaScript仅支持string类型
  • runJavaScriptExt支持ArrayBuffer类型和string类型
typescript 复制代码
//应用侧
.onClick(() => {
  // 调用前端页面无参函数。
  this.webviewController.runJavaScript('htmlTest()');
})

/前端页面侧
<script>
//...
    // 无参函数。
    function htmlTest() {
        document.getElementById('text').style.color = 'yellow';
    }
//...
</script>

5.2前端页面调用应用侧函数

开发者使用web组件将应用侧代码注册到前端中,注册完成之后,前端页面

注册应用侧代码

  • 在web组件初始化调用,使用javaScriptProxy接口
  • 在web组价初始化完成后调用,使用registerJavaScriptProxy接口,这两种方式都需要和deleteJavaScriptRegister接口配合使用,防止内存泄漏
typescript 复制代码
// Web组件加载本地index.html页面
Web({ src: $rawfile('index.html'), controller: this.webviewController})
        // 将对象注入到web端
 .javaScriptProxy({
         //定义要注入的JavaScript对象
          object: this.testObj,
          name: "testObjName",
          methodList: ["test"],
          controller: this.webviewController,
          // 可选参数
          asyncMethodList: [],//参与注册的应用侧js对象的异步方法
          permission: //json字符串,通过该字符串配置JSBridge的权限管控
        })
//前端页面使用
<script>
    function callArkTS() {
        let str = testObjName.test();
        document.getElementById("demo").innerHTML = str;
        console.info('ArkTS Hello World! :' + str);
    }
</script>

5.3建立应用侧与前端页面数据通道

前端页面和应用侧之间可以使用createWebMessagePorts接口创建消息端口来实现两端的通信

  • 应用侧通过createWebMessagePorts接口创建两个消息端口,再把其中一个端口通过postMessage接口发送到前端页面,便可以子啊前端页面和应用侧之间互相发送消息
typescript 复制代码
//应用侧代码

.onClick(() => {
          try {
            // 1、创建两个消息端口。
            this.ports = this.controller.createWebMessagePorts();
            if (this.ports && this.ports[0] && this.ports[1]) {
              // 2、在应用侧的消息端口(如端口1)上注册回调事件。
              this.ports[1].onMessageEvent((result: webview.WebMessage) => {
                let msg = 'Got msg from HTML:';
                if (typeof (result) === 'string') {
                  console.info(`received string message from html5, string is: ${result}`);
                  msg = msg + result;
                } else if (typeof (result) === 'object') {
                  if (result instanceof ArrayBuffer) {
                    console.info(`received arraybuffer from html5, length is: ${result.byteLength}`);
                    msg = msg + 'length is ' + result.byteLength;
                  } else {
                    console.info('not support');
                  }
                } else {
                  console.info('not support');
                }
                this.receivedFromHtml = msg;
              })
              // 3、将另一个消息端口(如端口0)发送到HTML侧,由HTML侧保存并使用。
              this.controller.postMessage('__init_port__', [this.ports[0]], '*');
            } else {
              console.error(`ports is null, Please initialize first`);
            }
          } catch (error) {
            console.error(`ErrorCode: ${(error as BusinessError).code},  Message: ${(error as BusinessError).message}`);
          }
        })

6.管理Web组件的网页加载

6.1加载页面

6.1.1加载网络页面

  • 默认加载:在web组件的src字段中设置(不能通过状态变量改变地址)
  • 修改地址:使用webController的loadUrl方法重新加载

6.1.2加载本地页面

  • 默认加载:在web组件的src字段中使用$rawfile()绑定
  • 修改地址同样使用loadUrl,不过参数需要传递$rawfile()
    加载html格式的文本数据
    当开发者不需要加兹安整个页面,只需要加载一些页面片段的时候,可使用Controller的loadData接口来实现快速加载

6.1.3加载html格式的文本数据

当开发者不需要加兹安整个页面,只需要加载一些页面片段的时候,可使用Controller的loadData接口来实现快速加载

typescript 复制代码
Button('loadData')
   .onClick(() => {
          try {
            // 点击按钮时,通过loadData,加载HTML格式的文本数据
            this.controller.loadData(
              "<html><body bgcolor=\"white\">Source:<pre>source</pre></body></html>",
              "text/html",
              "UTF-8"
            );
          } catch (error) {
            console.error(`ErrorCode: ${(error as BusinessError).code},  Message: ${(error as BusinessError).message}`);
          }
        })

同样也可以直接给src字段传递html格式的字符串数据

typescript 复制代码
 htmlStr: string = "data:text/html, <html><body bgcolor=\"white\">Source:<pre>source</pre></body></html>";
  build() {
    Column() {
      // 组件创建时,加载htmlStr
      Web({ src: this.htmlStr, controller: this.controller })
    }
  }

6.1.4使用resource协议加载本地资源

typescript 复制代码
Button('加载Resource资源')
        .onClick(() => {
          try {
            // 通过resource加载resources/rawfile目录下的index1.html文件
            this.controller.loadUrl('resource://rawfile/index1.html');
          } catch (error) {
            console.error(`ErrorCode: ${error.code}, Message: ${error.message}`);
          }
        })

6.2加速Web页面的访问

6.2.1预解析和预连接

此方法通过prepareFoePageLoad接口来预解析或者预连接将要加载的页面

typescript 复制代码
//在web组件的onAppear回调中对要加载的页面进行预连接
.onAppear(() => {
          // 指定第二个参数为true,代表要进行预连接,如果为false该接口只会对网址进行dns预解析
          // 第三个参数为要预连接socket的个数。最多允许6个。
          webview.WebviewController.prepareForPageLoad('https://www.example.com/', true, 2);
        })

该方式仅对url进行DNS解析以及建立tcp,但不会获取主资源子资源

也可以通过intiializeWebEngine接口来提前初始化内核,然后在初始化内核后调用

typescript 复制代码
//在Ability.ets的onCreat中
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
    console.log("EntryAbility onCreate");
    webview.WebviewController.initializeWebEngine();
    // 预连接时,需要将'https://www.example.com'替换成真实要访问的网站地址。
    webview.WebviewController.prepareForPageLoad("https://www.example.com/", true, 2);
    AppStorage.setOrCreate("abilityWant", want);
    console.log("EntryAbility onCreate done");
  }

6.2.2预加载

如果能够预测到web组件将要加载的页面或者即将要跳转的页面,就可以通过prefetchPage接口来预加载页面

预加载会提前下载页面所需的资源,包括主资源子资源,避免阻塞页面渲染。但不会执行前端网页的JS代码。

同时prefetchPage是webViewController的方法,需要一个已经关联好web组件的webViewController实例

typescript 复制代码
//在onPageEnd的时候触发下一个要访问的页面的预加载
.onPageEnd(() => {
          // 预加载https://www.iana.org/help/example-domains。
          this.webviewController.prefetchPage('https://www.iana.org/help/example-domains');
        })

6.2.3预获取Post请求

此方法是针对请求级进行优化。通过prefetchResource接口预获取将要加载页面中的post请求。在页面加载结束时,可以通过clearPrefetchedResource接口清除不需要的预获取资源

typescript 复制代码
//在web组件的onApper回调中,对要加载页面中的post请求进行预获取,在onPageEnd回调中清除缓存
.onAppear(() => {
          // 预获取时,需要将"https://www.example1.com/post?e=f&g=h"替换成真实要访问的网站地址。
          webview.WebviewController.prefetchResource(
            {
              url: "https://www.example1.com/post?e=f&g=h",
              method: "POST",
              formData: "a=x&b=y",
            },
            [{
              headerKey: "c",
              headerValue: "z",
            },],
            "KeyX", 500);
        })
 .onPageEnd(() => {
          // 清除后续不再使用的预获取资源缓存。
          webview.WebviewController.clearPrefetchedResource(["KeyX",]);
        })

6.2.4预编译生成编译缓存

可以通过precompileJavaScript接口在页面加载前生成脚本文件的编译缓存

6.2.5静态资源免拦截缓存

可以通过injectOflineResource在页面加载前提前将图片,html、css文件等静态资源加入到应用的内存缓存中

6.3Web组件在不同的窗口间迁移

web组件能够实现在不同窗口的组件树上进行挂载或移除操作,例如将浏览器的Tab页拖出成独立窗口,或拖入浏览器的另一个窗口

基本原理是,通过BuilderNode创建web的离线节点,并结合自定义占位节点控制web节点的挂载与移除。当从一个组件树上移除并挂载到另一个组件树上时,就完成了web组件在窗口间的迁移

  1. 在common.ets中声明一个储存MyNodeController的Map,并且提供初始化函数,需要在初始化函数中完成BuilderNode的build方法,并加入Map
typescript 复制代码
// 创建Map保存所需要的BuilderNode
let builderNodeMap : Map<string, BuilderNode<[Data]> | undefined> = new Map();
// 创建Map保存所需要的webview.WebviewController
let webControllerMap : Map<string, webview.WebviewController | undefined> = new Map();

// 初始化需要UIContext对象,UIContext对象可通过窗口或自定义组件的getUIContext方法获取
export const createNWeb = (url: string, uiContext: UIContext) => {
  // 创建WebviewController
  let webController = new webview.WebviewController() ;
  // 创建BuilderNode
  let builderNode : BuilderNode<[Data]> = new BuilderNode(uiContext);
  // 创建动态Web组件
  builderNode.build(wrap, new Data(url, webController));

  // 保存BuilderNode
  builderNodeMap.set(url, builderNode);
  // 保存WebviewController
  webControllerMap.set(url, webController);
}
// 自定义获取BuilderNode的接口
export const getBuilderNode = (url : string) : BuilderNode<[Data]> | undefined => {
  return builderNodeMap.get(url);
}
// 自定义获取WebviewController的接口
export const getWebviewController = (url : string) : webview.WebviewController | undefined => {
  return webControllerMap.get(url);
}

//NodeController类
// 用于控制和反馈对应的NodeContainer上的节点的行为,需要与NodeContainer一起使用
export class MyNodeController extends NodeController {
  private builderNode: BuilderNode<[Data]> | null | undefined = null;
  private webController : webview.WebviewController | null | undefined = null;
  private rootNode : FrameNode | null = null;
  constructor(builderNode : BuilderNode<[Data]> | undefined, webController : webview.WebviewController | undefined) {
    super();
    this.builderNode = builderNode;
    this.webController = webController;
  }
  //....
}
  1. 在EntryAbility中调用初始化方法
typescript 复制代码
createNWeb(defaultUrl, windowStage.getMainWindowSync().getUIContext());
  1. 在@Entry装饰的入口页面中调用获取函数获得Map中目标url对应的MyNodeController
typescript 复制代码
private nodeController : MyNodeController =
    new MyNodeController(getBuilderNode(defaultUrl), getWebviewController(defaultUrl));
相关推荐
n87658 小时前
Playwright Java WEB UI 自动化实现框架与逻辑
java·前端·ui
keyipatience8 小时前
TypeScript学习(1)
前端·javascript·学习·typescript·html·css3
xifangge20258 小时前
基于 JavaScript 实现 CNC 数控切削参数与螺纹公差纯前端核算引擎
开发语言·前端·javascript
wangruofeng9 小时前
2026 Web 技术选型指南,AI 时代如何选择适合项目的技术栈
前端·aigc·ai编程
honkun69 小时前
vue 表格组件 vxe-table 修改滚动条样式,修改滚动条样式,统一宽度、颜色与自动隐藏
前端·javascript·vue.js
爱勇宝9 小时前
研究数字孪生的这半年,做了个 Demo,也有了一些新想法
前端·程序员
不可能片场9 小时前
工程实践 一个缓存把选片从十分钟压到一分钟
前端·electron
前端小万9 小时前
探索副业五个月,最终以放弃告终
前端·后端·面试
FRing0310 小时前
JavaScript入门到精通二十四(正则表达式、元字符、修饰符)
开发语言·前端·javascript·正则表达式
用户294694054481610 小时前
DSH Desktop - Electron on HarmonyOS Edition
harmonyos·deepseek