[鸿蒙开发] 11 - 使用ArkWeb组件加载H5页面

前言:最近做的App都是混合开发的,其中大部分的业务实现都是H5页面。刚开始挺担心ArkWeb是否能够满足业务需求,所以最近的重点都是在学习和使用ArkWeb组件,这里简单总结一下(基于Developer Preview1版本)。

1. ArkWeb

相当于iOS/Android中的WebView控件,给个url就可以展示H5页面。

但是在项目中,我们需要考虑以下需求:

  • H5页面加载(处理页面加载的状态:成功、失败、加载中)
  • H5页面栈(window.history)管理
  • UserAgent管理
  • Cookie管理
  • Native和H5交互
  • 页面加载优化
  • Web页面调试

我也是带着这些问题去学习使用ArkWeb的。

2. 使用ArkWeb加载url

页面加载是Web组件最基本的功能了,支持三种场景:

  • 加载网络页面(需要配置ohos.permission.INTERNET网络访问权限);
  • 加载本地页面;
  • 加载HTML格式的富文本数据;

我们以加载网络页面为例:

typescript 复制代码
// 导入webview
import { webview } from '@kit.ArkWeb';

@Entry
@Component
struct Index {

  private webviewController = new webview.WebviewController()

  build() {
    Stack() {
      Web({ src: "https://www.baidu.com", controller: this.webviewController })
        .onPageBegin((event) => {
          // 网页开始加载时触发该回调
        })
        .onPageEnd((event) => {
          // 网页加载完成时触发该回调
        })
        .onErrorReceive((event) => {
          // 网页加载遇到错误时触发该回调. 无网下会触发该回调
        })
        .onHttpErrorReceive((event) => {
          // 网页加载资源遇到的HTTP错误(响应码>=400)时触发该回调
        })
        .onProgressChange((event) => {
          // 网页加载进度变化时触发该回调
        })
    }
  }
}

Web组件暴露了很多回调方法,我们可以在跟加载进度和状态的回调方法中设置webview在加载中、加载失败、加载成功的样式。

3. H5页面栈(window.history)管理

在上面的示例中,可以看到在创建Web组件的时候,传入了一个controller。我们没办法直接操作Web组件,只能通过controller去操作Web组件的行为,包括页面栈管理、页面刷新、执行JS等。

我们先来看下controller提供的管理页面栈的方法,如下所示:

arduino 复制代码
// 判断当前页面是否可前进,是否有前进历史记录
accessForward(): boolean

// 判断页面是否可后退,即当前页面是否有返回历史记录
accessBackward(): boolean

// 按照历史栈,前进一个页面
forward(): void

// 按照历史栈,后退一个页面
backward(): void

// 删除所有前进后退记录
clearHistory(): void

4. UserAgent管理

默认的UserAgent定义如下: Mozilla/5.0 ({deviceType}; {OSName} {OSVersion}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 ArkWeb/{ArkWeb VersionCode} {Mobile}

举例:

scss 复制代码
Mozilla/5.0 (Phone; OpenHarmony 4.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 ArkWeb/4.1.6.1 Mobile

4.1 在webInited回调设置UA

目前在Preview1版本测试webInited对应的回调方法不会执行,等升级后再进行验证。🙋🙋🙋

如果我们想基于默认的UserAgent去定制UserAgent:

typescript 复制代码
// xxx.ets
import web_webview from '@ohos.web.webview';
import business_error from '@ohos.base';

@Entry
@Component
struct WebComponent {
  controller: web_webview.WebviewController = new web_webview.WebviewController();
  @State ua: string = ""

  aboutToAppear():void {
    web_webview.once('webInited', () => {
      try {
        // 应用侧用法示例,定制UserAgent。
        this.ua = this.controller.getUserAgent() + 'xxx';
      } catch(error) {
        let e:business_error.BusinessError = error as business_error.BusinessError;
        console.error(`ErrorCode: ${e.code},  Message: ${e.message}`);
      }
    })
  }

  build() {
    Column() {
      Web({ src: 'www.example.com', controller: this.controller })
        .userAgent(this.ua)
    }
  }
}

上面的代码是在webInited回调中,通过this.controller.getUserAgent()获取默认的UA。once是webview提供的方法,用于订阅一次指定类型Web事件的回调,目前也仅支持webInited(Web初始化完成)事件。

4.2 在onControllerAttached回调中设置UA

controller中可以通过setCustomUserAgent方法来设置自定义用户代理,会覆盖系统的用户代理,推荐放在onControllerAttached回调中。

若需要setCustomUserAgent,在setCustomUserAgent方法后执行this.controller.loadUrl(this.webUrl),webUrl为要加载的web页面,在原始的web组件的src可以设置一个空字符串。

typescript 复制代码
import web_webview from '@ohos.web.webview'

@Entry
@Component
struct WebComponent {
  controller: web_webview.WebviewController = new web_webview.WebviewController();

  build() {
    Column() {
      // 这个src可以先设置为空的字符串
      Web({ src: '', controller: this.controller })
        .onControllerAttached(()=>{
          let userAgent = this.controller.getUserAgent() + "xxx";
          this.controller.setCustomUserAgent(userAgent);
          // 设置完UA之后,再次加载url
          this.controller.loadUrl("www.baidu.com")
        })
    }
  }
}

5. Cookie管理

Web组件提供了WebCookieManager类,用来管理Web组件的Cookie信息,Cookie信息保存在应用沙箱路径下/proc/{pid}/root/data/storage/el2/base/cache/web/Cookiesd的文件中。

下面以configCookieSync接口举例,为 www.example.com 设置单个Cookie的值:

typescript 复制代码
// xxx.ets
import web_webview from '@ohos.web.webview';
import business_error from '@ohos.base';

@Entry
@Component
struct WebComponent {
  controller: web_webview.WebviewController = new web_webview.WebviewController();

  build() {
    Column() {
      Button('configCookieSync')
        .onClick(() => {
          try {
            web_webview.WebCookieManager.configCookieSync('https://www.example.com', 'value=test');
          } catch (error) {
            let e: business_error.BusinessError = error as business_error.BusinessError;
            console.error(`ErrorCode: ${e.code},  Message: ${e.message}`);
          }
        })
      Web({ src: 'www.example.com', controller: this.controller })
    }
  }
}

WebCookieManager还提供了其他操作Cookie的方法,截图如下:

6. Native和H5交互

6.1 Native调用H5页面中的函数

Native可以通过runJavaScript()方法执行JavaScript,调用前端页面的JavaScript相关函数。

H5页面:

xml 复制代码
<!-- index.html -->
<!DOCTYPE html>
<html>
<body>
<script>
    var param = "JavaScript Hello World!";
    function htmlTest(param) {
        console.log(param);
    }
</script>
</body>
</html>

Native页面:

typescript 复制代码
// xxx.ets
import web_webview from '@ohos.web.webview';

@Entry
@Component
struct WebComponent {
  webviewController: web_webview.WebviewController = new web_webview.WebviewController();

  aboutToAppear() {
    // 配置Web开启调试模式
    web_webview.WebviewController.setWebDebuggingAccess(true);
  }

  build() {
    Column() {
      Button('runJavaScript')
        .onClick(() => {
          // 通过调用webviewController的runJavaScript方法执行js函数
          this.webviewController.runJavaScript('htmlTest(param)');
        })
      Web({ src: $rawfile('index.html'), controller: this.webviewController})
    }
  }
}

6.2 H5页面调用Native函数

Native需要先将应用侧代码注册到H5页面中,之后H5页面就可以使用注册的对象名称调用应用侧的函数,实现在H5页面中调用Native的方法。

有两种注册方式:

  • Web组件初始化调用,使用javaScriptProxy接口;
  • Web组件初始化完成后调用,使用registerJavaScriptProxy接口;

只是注册时机不同,其它都一样。这里以Web组件初始化调用为例,在Web组件初始化时进行注册:

typescript 复制代码
// xxx.ets
import web_webview from '@ohos.web.webview';

// 用于注入的对象
class testClass {

  constructor() {
  }

  // H5页面要调用的方法
  test(): string {
    return 'ArkTS Hello World!';
  }
}

@Entry
@Component
struct WebComponent {
  webviewController: web_webview.WebviewController = new web_webview.WebviewController();
  // 声明需要注册的对象
  @State testObj: testClass = new testClass();

  build() {
    Column() {
      // web组件加载本地index.html页面
      Web({ src: $rawfile('index.html'), controller: this.webviewController})
        // 将对象注入到web端
        .javaScriptProxy({
          object: this.testObj,
          name: "testObjName",  // H5会通过这个name调用方法
          methodList: ["test"], // 暴露有H5的方法
          controller: this.webviewController
        })
    }
  }
}

H5页面触发应用侧代码:

xml 复制代码
<!-- index.html -->
<!DOCTYPE html>
<html>
<body>
<button type="button" onclick="callArkTS()">Click Me!</button>
<p id="demo"></p>
<script>
    function callArkTS() {
        // testObjName是Native侧注册的名称
        // test是Native侧暴露的方法名称
        let str = testObjName.test();
        document.getElementById("demo").innerHTML = str;
        console.info('ArkTS Hello World! :' + str);
    }
</script>
</body>
</html>

双方可以互相通信之后,就可以根据业务需要进行封装和分发事件了。

6.3 兼容DSBridge

当前项目iOS/Android和H5通信用的是DSBridge这个库(当时项目时间紧,所以没考虑自己封装)。

DSBridge的实现原理还是挺简单的,通过H5页面调用prompt方法来向Native传递数据,数据中会包括:

  • H5页面的回调函数名
  • 调用Native的目标方法名
  • 调用Native的目标方法参数

Native会拦截系统Webview的prompt方法,解析数据,在执行完方法之后通过H5页面传过来的回调函数名执行H5的回调方法。

如果我们要做鸿蒙版本的App,就需要考虑对鸿蒙版本App的兼容,有两个选择:

  • 鸿蒙App中实现DSBridge的逻辑,这样H5侧不需要任何改动;
  • 基于鸿蒙Web组件提供的方法封装一套鸿蒙App特有的逻辑,这需要H5侧在通信层进行系统判断处理;

于是翻看了下ArkWeb组件的API,确实有onPrompt方法,能够拦截到H5页面通过DSBridge的调用。在进行到一半的时候,突然发现有同学已经开发出了HarmonyOS版本的DSBridge:github.com/751496032/D...

由于当时这个库只支持了API9,我们是基于API11进行开发和学习的,为了避免作者更新不及时,我们选择自己仿照这个库的逻辑进行实现。

主要是能拦截到H5侧DSBridge的方法调用,先来看下DSBridge在H5侧的实现:

scss 复制代码
if(window._dsbridge) {
    // 这个是关键代码
    ret = _dsbridge.call(method, arg)
}else if(window._dswk || navigator.userAgent.indexOf("_dsbridge") != -1){
    ret = prompt("_dsbridge=" + method, arg);
}

_dsbridge.call(method, arg)是关键代码,跟上面6.2中H5调用Native的方法是很相似的。

所以,我们可以在Native侧对其进行注册:

typescript 复制代码
import web_webview from '@ohos.web.webview'

class JSBridge {

  // 暴露给H5的唯一的方法
  call(methodName: string, params: string): string {
    // 这里可以获取到methodName和params
    return ""
  }

}

@Entry
@Component
struct WebComponent {
  controller: web_webview.WebviewController = new web_webview.WebviewController();
  jsbridge: JSBridge = new JSBridge()

  build() {
    Column() {
      // 这个src可以先设置为空的字符串
      Web({ src: '', controller: this.controller })
        .onControllerAttached(()=>{
          let userAgent = this.controller.getUserAgent() + "xxx";
          this.controller.setCustomUserAgent(userAgent);
          // 设置完UA之后,再次加载url
          this.controller.loadUrl("www.baidu.com")
        })
        .javaScriptProxy({
          object: this.jsbridge,
          name: "_dsbridge", // _dsbridge是固定名字
          methodList: ["call"], // call也是固定方法名
          controller: this.controller
        })
    }
  }
}

这样DSBridge在H5页面就可以通过_dsbridge.call(method, arg)调用到Native的call方法了,然后Native在call方法中解析数据,按照iOS/Android的实现逻辑进行实现即可。

7. 页面加载优化

ArkWeb组件提供了提高Web页面加载速度的能力:

  • 预解析和预链接
  • 预渲染

7.1 预解析和预链接

可以通过prepareForPageLoad()来预解析或者预链接将要加载的页面。

  • 预解析:主要指的是DNS预解析(DNS Prefetching)。浏览器可以在实际发起请求之前,预先解析主机名(即域名)对应的IP地址。这意味着,当浏览器遇到需要访问的域名时,它可以提前进行DNS查找,将域名转换为IP地址,并将其缓存起来。当页面真正需要访问该域名时,浏览器可以直接使用缓存的IP地址,从而避免了额外的DNS查找延迟。
  • 预链接:prefetching,它是另一种网页优化技术。预链接的目标是提前获取用户可能需要的资源,并将这些资源缓存起来,以便在真正需要时能够更快地加载。预链接可以包括DNS预解析,但也可以涵盖其他类型的资源预加载,如JavaScript文件、CSS样式表、图片等。

这种方式很适合提前对首页进行预解析和预链接,下面是一个示例,再Ability的onCreate中提前初始化Web内核并对首页进行预链接:

scala 复制代码
// xxx.ets
import UIAbility from '@ohos.app.ability.UIAbility';
import web_webview from '@ohos.web.webview';
import AbilityConstant from '@ohos.app.ability.AbilityConstant';
import Want from '@ohos.app.ability.Want';

export default class EntryAbility extends UIAbility {
    onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
        console.log("EntryAbility onCreate");
        // 初始化Web内核
        web_webview.WebviewController.initializeWebEngine();
        // 预连接时,需要將'https://www.example.com'替换成真实要访问的网站地址。
        // 指定第二个参数为true,代表要进行预连接,如果为false该接口只会对网址进行dns预解析
        // 第三个参数为要预连接socket的个数。最多允许6个。
        web_webview.WebviewController.prepareForPageLoad("https://www.example.com/", true, 2);
        AppStorage.setOrCreate("abilityWant", want);
        console.log("EntryAbility onCreate done");
    }
}

7.2 预加载

如果能够预测到Web组件将要加载的页面或者即将要跳转的页面,可以使用prefetchPage()来预加载将要加载的页面。

预加载会提前下载页面所需要的资源,包括主资源子资源,但不会执行网页JS代码。预加载时WebviewController的实例方法,需要一个已经关联好Web组件的WebviewController实例。

在下面的示例中,在onPageEnd的时候触发写一个要访问的页面的预加载:

typescript 复制代码
// xxx.ets
import web_webview from '@ohos.web.webview';

@Entry
@Component
struct WebComponent {
  webviewController: web_webview.WebviewController = new web_webview.WebviewController();
  build() {
    Column() {
      Web({ src: 'https://www.example.com/', controller: this.webviewController})
        .onPageEnd(() => {
            // 预加载https://www.iana.org/help/example-domains。
            this.webviewController.prefetchPage('https://www.iana.org/help/example-domains');
        })
    }
  }
}

8. 使用Devtools工具调试前端页面

在iPhone设备上,我们可以用Safari调试开发证书签名的App中的Web页面。ArkWeb组件也支持使用DevTools工具调试Web页面。

DevTools是一个Web前端开发调试工具,提供了电脑上调试移动设备Web页面的能力。

我们需要调用Web组件的setWebDebuggingAccess()方法,允许调试:

typescript 复制代码
// xxx.ets
import web_webview from '@ohos.web.webview';

@Entry
@Component
struct WebComponent {
  controller: web_webview.WebviewController = new web_webview.WebviewController();
  aboutToAppear() {
    // 配置Web开启调试模式
    web_webview.WebviewController.setWebDebuggingAccess(true);
  }
  build() {
    Column() {
      Web({ src: 'www.example.com', controller: this.controller })
    }
  }
}

需要在module.json5文件中增加网络访问权限:

json 复制代码
"requestPermissions":[
   {
     "name" : "ohos.permission.INTERNET"
   }
 ]

在真机上尝试了很多次都没有成功,先以本地模拟器为例。

我们需要用到hdc工具,在SDK的toolchains目录下,首次使用我们需要配置环境变量: developer.huawei.com/consumer/cn...

然后需要使用模拟器启动应用(需要保持项目运行状态),并在电脑端配置端口映射:

c 复制代码
// 查找 devtools 远程调试所需的应用browser进程号,重启调试应用后,需要重复此步骤,以完成端口转发
hdc shell
// 显示当前app对应的进行,需要用到包名过滤
ps -ef | grep "com.example.studyapplication"
//显示browser进程和render进程
20020040     18671   121 11 18:22:12 ?    00:00:00 com.example.studyapplication
1000004      17932   303 8 18:22:12 ?     00:00:00 com.example.studyapplication
// 推出hdc shell
exit
// 添加映射 localabstract:webview_devtools_remote_[pid] pid为上面获取到的18671
hdc fport tcp:9222 localabstract:webview_devtools_remote_18671
// 用于查看映射
hdc fport ls

在电脑端Chrome浏览器地址栏中输入chrome://inspect/#devices ,页面识别到设备后,就可以开始页面调试。调试效果如下:

相关推荐
长弓三石1 小时前
鸿蒙网络编程系列44-仓颉版HttpRequest上传文件示例
前端·网络·华为·harmonyos·鸿蒙
SameX3 小时前
鸿蒙 Next 电商应用安全支付与密码保护实践
前端·harmonyos
SuperHeroWu74 小时前
【HarmonyOS】键盘遮挡输入框UI布局处理
华为·harmonyos·压缩·keyboard·键盘遮挡·抬起
sanzk8 小时前
华为鸿蒙应用开发
华为·harmonyos
SoraLuna13 小时前
「Mac畅玩鸿蒙与硬件28」UI互动应用篇5 - 滑动选择器实现
macos·ui·harmonyos
ClkLog-开源埋点用户分析14 小时前
ClkLog企业版(CDP)预售开启,更有鸿蒙SDK前来助力
华为·开源·开源软件·harmonyos
mg66814 小时前
鸿蒙系统的优势 开发 环境搭建 开发小示例
华为·harmonyos
lqj_本人14 小时前
鸿蒙next选择 Flutter 开发跨平台应用的原因
flutter·华为·harmonyos
lqj_本人14 小时前
使用 Flutter 绘制一个棋盘
harmonyos
lqj_本人17 小时前
Flutter&鸿蒙next 状态管理框架对比分析
flutter·华为·harmonyos