[鸿蒙开发] 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 ,页面识别到设备后,就可以开始页面调试。调试效果如下:

相关推荐
SameX1 小时前
HarmonyOS Next 安全生态构建与展望
前端·harmonyos
SameX1 小时前
HarmonyOS Next 打造智能家居安全系统实战
harmonyos
Random_index9 小时前
#Uniapp篇:支持纯血鸿蒙&发布&适配&UIUI
uni-app·harmonyos
鸿蒙自习室13 小时前
鸿蒙多线程开发——线程间数据通信对象02
ui·harmonyos·鸿蒙
SuperHeroWu715 小时前
【HarmonyOS】鸿蒙应用接入微博分享
华为·harmonyos·鸿蒙·微博·微博分享·微博sdk集成·sdk集成
zhangjr057517 小时前
【HarmonyOS Next】鸿蒙实用装饰器一览(一)
前端·harmonyos·arkts
诗歌难吟4641 天前
初识ArkUI
harmonyos
SameX1 天前
HarmonyOS Next 设备安全特性深度剖析学习
harmonyos
郭梧悠1 天前
HarmonyOS(57) UI性能优化
ui·性能优化·harmonyos
郝晨妤2 天前
鸿蒙原生应用开发元服务 元服务是什么?和App的关系?(保姆级步骤)
android·ios·华为od·华为·华为云·harmonyos·鸿蒙