前言:最近做的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 ,页面识别到设备后,就可以开始页面调试。调试效果如下: