【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应用侧调用前端页面函数

应用侧可以通过runJavaScriptrunJavaScriptExt方法来调用前端页面的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代码。

同时prefetchPagewebViewController的方法,需要一个已经关联好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));
相关推荐
却尘11 小时前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare11 小时前
浅浅看一下设计模式
前端
Lee川11 小时前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix12 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人12 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl12 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅12 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人12 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼12 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端
布列瑟农的星空12 小时前
前端都能看懂的Rust入门教程(三)——控制流语句
前端·后端·rust