鸿蒙HarmonyOS:Web组件初体验与离线包方案探索
1. 引言
在当今数字化的世界中,操作系统的演进不仅仅是技术的进步,更是对用户体验和开发者挑战的不断重新定义。而在这场技术的激流中,最近一年来,鸿蒙HarmonyOS崭露头角,尤其是最近这几个月来,各大主流APP都已经陆续启动鸿蒙化的研发,让鸿蒙HarmonyOS成为备受关注的新一代操作系统,本文将聚焦于HarmonyOS中一个重要的组成部分——Web组件,以及与之息息相关的离线包。Web组件作为移动应用开发中不可或缺的一环,为开发者提供了在应用中嵌入Web内容的强大能力
本文基于 HarmonyOS NEXT(4.1)版本API,实现一个简单的Web容器页和离线包方案
源码地址 https://github.com/lovexiaobei/WebContainer
2. Web组件初探
初始化示例代码
1 | Web({ src: 'www.example.com', controller: new web_webview.WebviewController();}) |
HarmonyOS的Web组件构建参数是由src
和controller
构成的
1 | declare interface WebOptions { |
src
参数可以传递具体的Web链接,可以传递本地的资源文件,controller
参数是控制Web组件各种行为,也可以控制Web组件的引擎初始化和开启调试、设置dns等。Web
组件本身是一个WebAttribute
,可以使用它来做页面上的操作,比如页面打开回调、标题、网页进度监听等
如果类比Android的话,WebviewController
就是Android的WebView本身,可以用来控制加载网页等具体原生操作,Web
组件本身就是WebClient
、WebChromeClient
和WebSetting
的究极缝合怪的组合形式,里面很多方法都能在Web
组件上找到类似的,不能说是一摸一样,只能说是十分相像。
3. 简单Web容器搭建实战
WebAbility
首先我们按照最新的Stage模型
中的方式,给Web容器页
单独新建一个UIAbility组件
其中我们通过want
的参数传递一个url过来,然后通过LocalStorage
的方式给WebPage 传递过去
代码如下
1 | export default class WebAbility extends UIAbility { |
在module.json5注册下WebAbility组件 注意一定一定要加上网络权限ohos.permission.INTERNET
(和Android也基本一致😄😄)
1 | "requestPermissions":[ |
WebPage
在WebPag上我们简单实现一个带有返回按钮的原生标题栏和Web容器共同组成的一个页面,同时也支持返回键和Web 返回栈的联动
url参数接收
url参数是从WebAbility传递过来的1
2
3
4
5
6
7
8
9let storage = LocalStorage.getShared()
const TAG = 'WebPage';
@Entry(storage)
@Component
struct WebPage {
@State title: string = '标题';
@LocalStorageProp('url') url: string = '';
}- Web组件 自定义标题栏实现
定义一个标题的State,在Web组件的onTitleReceive
可以返回页面的标题
返回按钮和页面的onBackPress
绑定1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32@State title: string = '标题';
…………
Column() {
Row() {
//back
Button({ type: ButtonType.Circle, stateEffect: true }) {
Image($r('app.media.back')).width(30).height(30)
}.width(30)
.height(30)
.margin({ left: 10 })
.backgroundColor(0xFFFFFF)
.onClick(() => {
this.onBackPress();
})
Text(this.title) {
}.layoutWeight(1).height(30).width("100%").textAlign(TextAlign.Center).maxLines(1).margin({right:40})
}.height(50).width("100%").justifyContent(FlexAlign.Start)
Web({src:this.url,controller:this.webviewController})
.layoutWeight(1)
.width("100%")
.onPageBegin((event) => {
})
.onTitleReceive((event) => {
hilog.info(0x0000, TAG, 'onTitleReceive %{public}s', event?.title??"");
if (event?.title){
this.title = event.title;
}
})
;
}.width("100%").height("100%")
- Web组件 自定义标题栏实现
返回事件判断和Web 返回栈的联动
1
2
3
4
5
6
7
8onBackPress() {
if (this.webviewController.accessBackward()) {
this.webviewController.backward();
return true;
}
router.back()
return false;
}我们启动下这个试试
4离线包方案探索
在前面介绍Web组件的时候有说过 Web组件初始化或者webviewController 在loadUrl的时候是可以传递Resource资源的,但这种只能做到包体静态离线包,没办法做到动态离线包,loadUrl 也没有像Android WebView 一样 支持file协议加载本地文件的离线包方法。那么动态离线包怎么做呢?
在WebAttribute我们看到了熟悉的 onInterceptRequest方法源码如下
1 | /** |
这边的callback只需要返回一个WebResourceResponse即可,注释里也写到,如果返回为null Web 将继续使用系统的继续加载,我们只需要实现这个 WebResourceResponse 是不是等于说可使用本地的资源了?看看 WebResourceResponse的参数
1 | setResponseData(data: string | number | Resource); |
可以看到 setResponseData是支持string的data数据的,那么基于拦截请求的离线包方案呼之欲出,我们直接看代码
离线包方案
离线包加载流程
加载本地离线包
我们先判断本地文件夹有没有,如果有了,那就简单认为已经下载好了,没有就去下载离线包压缩包1
2
3
4
5
6
7
8
9
10
11
12
13
14loadOfflinePackage() {
//判断本地离线包是否存在
let context = getContext(this) as common.UIAbilityContext;
let filesDir = context.filesDir + '/offlineWeb';
if (fs.accessSync(filesDir)) {
//如果离线包存在直接初始化
console.info('offline package is exist');
initOffLine(filesDir);
} else {
//不存在就去下载离线包
console.info('offline package is not exist');
this.downloadOfflinePackage(filesDir);
}
}下载离线包并解压到本地文件夹
这里我们利用系统提供的request请求工具和zlib解压缩工具对压缩包进行下载和解压1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19request.downloadFile(context, {
url: 'https://chenshengyu.cn/public.zip',
filePath: zipDir,
}).then((downloadTask: request.DownloadTask) => {
downloadTask.on('progress', (receivedSize: number, totalSize: number) => {
console.info(`download progress: ${receivedSize}/${totalSize}`);
})
downloadTask.on('complete', () => {
//解压缩
zlib.decompressFile(zipDir, path).then(() => {
console.info('decompressFile success');
initOffLine(path);
}).catch((err: BusinessError) => {
console.error(`Invoke decompressFile failed, code is ${err.code}, message is ${err.message}`);
});
})
}).catch((err: BusinessError) => {
console.error(`Invoke downloadTask failed, code is ${err.code}, message is ${err.message}`);
});把离线包加载到本地内存里
在这里通过本地一个HashMap 来保存离线包数据 遍历离线包文件夹,并将所有的文件通过文件系统读取成text ,然后创建一个WebResourceResponse来存储,用文件的路径做为key方便读取的时候匹配文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36const offLineMap :HashMap<string, WebResourceResponse> = new HashMap<string, WebResourceResponse>()
const initOffLine = (path:string)=>{
offLineMap.clear();
initOffLineList(path,path+"/public");
}
const initOffLineList = (path:string,suffix:string)=>{
let files = fs.listFileSync(path);
files.forEach((file)=>{
let pathDir = path+"/"+file;
fs.stat(pathDir, (err: BusinessError, stat: fs.Stat) => {
if (err) {
console.info("get file info failed with error message: " + err.message + ", error code: " + err.code);
} else {
if(stat.isDirectory()){
initOffLineList(pathDir,suffix)
}else if(stat.isFile()){
initOffLineResponse(pathDir,suffix)
}
}
});
})
}
const initOffLineResponse = (path:string,suffix:string)=>{
fs.readText(path).then((data)=>{
let response = new WebResourceResponse();
response.setResponseData(data);
response.setResponseEncoding("utf-8");
response.setResponseMimeType(path2MimeType(path));
response.setResponseCode(200);
response.setReasonMessage('OK');
let key = path.replace(suffix,"");
console.info("key:"+key);
offLineMap.set(key,response);
})
}
离线包资源拦截流程
这里我们拦截下 onInterceptRequest 请求,如果匹配到离线包资源就可以返回给Web组件我们自己构建的WebResourceResponse,否则就走系统的网络正常加载,这样做的好处是,离线包没加载或者离线包没有资源的时候,也能正常加载网页,网络和离线包走同一个方案,随时切换离线包和在线方式
代码如下
1 | .onInterceptRequest((event) => { |
这样一个简单的离线包方案就好了,我们运行下看下效果
网络关闭后网页可以正常加载,图片因为编码问题未展示出来,由此可见基于onInterceptRequest拦截请求的离线包方式总体可行的
总结
本文只是简单验证下离线包方案,离线包技术远远没有这么简单,对于离线包的加载时机和加载流程、离线包管理、安全验证、方案降级等还有众多需要完善的地方,我们后面接着探索
我们使用了HarmonyOS的下载、压缩、文件管理等多个API,基本上没有费劲就完成了简单的方案验证,由此可见 HarmonyOS对于开发者来说是一个很完善的方案了,也提供了大量的开发者工具,让我们开发者可以快速上手鸿蒙开发,总之鸿蒙,未来可期。
鸣谢
感谢ChatGPT 对本文的写作帮助
感谢Github Copilot 对本文代码帮助