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

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类型
//应用侧
.onClick(() => {
  // 调用前端页面无参函数。
  this.webviewController.runJavaScript('htmlTest()');
})

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

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

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

注册应用侧代码

  • 在web组件初始化调用,使用javaScriptProxy接口
  • 在web组价初始化完成后调用,使用registerJavaScriptProxy接口,这两种方式都需要和deleteJavaScriptRegister接口配合使用,防止内存泄漏
// 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接口发送到前端页面,便可以子啊前端页面和应用侧之间互相发送消息
//应用侧代码

.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接口来实现快速加载

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格式的字符串数据

 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协议加载本地资源

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接口来预解析或者预连接将要加载的页面

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

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

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

//在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实例

//在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接口清除不需要的预获取资源

//在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
// 创建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中调用初始化方法
createNWeb(defaultUrl, windowStage.getMainWindowSync().getUIContext());
  1. 在@Entry装饰的入口页面中调用获取函数获得Map中目标url对应的MyNodeController
private nodeController : MyNodeController =
    new MyNodeController(getBuilderNode(defaultUrl), getWebviewController(defaultUrl));



可以从开源项目地址和官网获取Frida从入门到深入学习的相关资料。开源项目地址是获取Frida相关资源的重要来源,官网则提供了丰富的使用文档,有助于深入学习Frida的使用方法,不过具体的开源项目地址和官网地址需补充完善 [^1]。 此外,还有一些代码示例可供学习。如使用Frida进行爆破的Python代码示例: ```python import frida import sys def on_message(message, data): print("[%s] => %s" % (message, data)) session = frida.attach("100w.exe") # 附加frida到目标进程 script = session.create_script(''' console.log("hook success!"); // 这里也可以单独起一个js文件来进行调用。 ''') script.on('message', on_message) script.load() sys.stdin.read() session.detach() ``` 此代码展示了如何使用Frida附加到目标进程并执行简单的hook操作 [^2]。 在Frida脚本中主动调用Java函数的示例代码如下: ```javascript function callSub(a, b) { var Arith = Java.use('com.example.junior.Arith'); var javaString = Java.use('Java.lang.String'); var result = Arith.sub(javaString.$new(a), javaString.$new(b)); console.log(a, "-", b, "=", result); } rpc.exports = { sub: callSub }; ``` 展示了在Frida脚本中如何主动调用Java的静态函数和实例函数 [^3]。 还有使用Frida在Mumu模拟器中查看进程和加载脚本的命令示例,如`frida-ps -U`可输出Mumu模拟器中的所有正在运行的进程,`frida -U -l F:\hello_world.js android.process.media`可在模拟器中加载指定脚本 [^4]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值