鸿蒙HarmonyOS:Web组件初体验与离线包方案探索
作者:访客发布时间:2023-12-27分类:程序开发学习浏览:111
1. 引言
在当今数字化的世界中,操作系统的演进不仅仅是技术的进步,更是对用户体验和开发者挑战的不断重新定义。而在这场技术的激流中,最近一年来,鸿蒙HarmonyOS崭露头角,尤其是最近这几个月来,各大主流APP都已经陆续启动鸿蒙化的研发,让鸿蒙HarmonyOS成为备受关注的新一代操作系统,本文将聚焦于HarmonyOS中一个重要的组成部分——Web组件,以及与之息息相关的离线包。Web组件作为移动应用开发中不可或缺的一环,为开发者提供了在应用中嵌入Web内容的强大能力
本文基于 HarmonyOS NEXT版本 API10,实现一个简单的Web容器页和离线包方案 源码地址 github.com/lovexiaobei…
2. Web组件初探
初始化示例代码
Web({ src: 'www.example.com', controller: new web_webview.WebviewController();})
HarmonyOS的Web组件构建参数是由src
和controller
构成的
declare interface WebOptions {
src: string | Resource;
controller: WebController | WebviewController;
}
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 传递过去
代码如下
export default class WebAbility extends UIAbility {
private urlStorage:LocalStorage = new LocalStorage();
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
const url = want?.parameters?.url as string;
if (url ==undefined || url == null || url == '') {
this.context.terminateSelf();
}
this.urlStorage.setOrCreate('url',url);
}
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('web/page/WebPage',this.urlStorage, (err, data) => {
});
}
}
在module.json5注册下WebAbility组件 注意一定一定要加上网络权限ohos.permission.INTERNET (和Android也基本一致😄😄)
"requestPermissions":[
{
"name" : "ohos.permission.INTERNET",
"reason": "$string:module_desc",
"usedScene": {
"abilities": [
……
……
……
"WebAbility"
],
"when":"inuse"
}
},],
……
……
……
"abilities": [
……
……
……
{
"name": "WebAbility",
"srcEntry": "./ets/web/WebAbility.ets",
"description": "$string:WebAbility_desc",
"icon": "$media:icon",
"label": "$string:WebAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background"
}
]
WebPage
在WebPag上我们简单实现一个带有返回按钮的原生标题栏和Web容器共同组成的一个页面,同时也支持返回键和Web 返回栈的联动
- url参数接收 url参数是从WebAbility传递过来的
let storage = LocalStorage.getShared()
const TAG = 'WebPage';
@Entry(storage)
@Component
struct WebPage {
@State title: string = '标题';
@LocalStorageProp('url') url: string = '';
}
- Web组件 自定义标题栏实现
定义一个标题的State,在Web组件的onTitleReceive
可以返回页面的标题
返回按钮和页面的onBackPress
绑定
@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 返回栈的联动
onBackPress() {
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方法源码如下
/**
* Triggered when the resources loading is intercepted. * * @param { function } callback The triggered callback when the resources loading is intercepted. * @returns { WebAttribute } If the response value is null, the Web will continue to load the resources. Otherwise, the response value will be used * @syscap SystemCapability.Web.Webview.Core * @since 9 */
onInterceptRequest(callback: (event?: {request: WebResourceRequest;}) => WebResourceResponse): WebAttribute;
这边的callback只需要返回一个WebResourceResponse即可,注释里也写到,如果返回为null Web 将继续使用系统的继续加载,我们只需要实现这个 WebResourceResponse 是不是等于说可使用本地的资源了?看看 WebResourceResponse的参数
setResponseData(data: string | number | Resource);
setResponseEncoding(encoding: string);
setResponseMimeType(mimeType: string);
setReasonMessage(reason: string);
setResponseHeader(header: Array<Header>);
setResponseCode(code: number);
setResponseIsReady(IsReady: boolean);
可以看到 setResponseData是支持string的data数据的,那么基于拦截请求的离线包方案呼之欲出,我们直接看代码
离线包方案
离线包加载流程
- 加载本地离线包 我们先判断本地文件夹有没有,如果有了,那就简单认为已经下载好了,没有就去下载离线包压缩包
let path = context.filesDir + localPathSuffix;
//判断是否存在public目录
if (fs.accessSync(path)) {
//存在public目录,初始化离线资源
console.info("initOffLine 存在public目录");
initOffLine(context.filesDir);
return;
}
//不存在public目录,判断是否存在public.zip
path = context.filesDir + downloadFilePathSuffix;
if (fs.accessSync(path)) {
//存在public.zip,解压public.zip
console.info("initOffLine 存在public.zip");
unzip(path, context.filesDir);
return;
}
- 下载离线包并解压到本地文件夹 这里我们利用系统提供的request请求工具和zlib解压缩工具对压缩包进行下载和解压
//不存在public目录和public.zip,下载public.zip
try {
request.downloadFile(context, {
url: 'https://chenshengyu.cn/public.zip',
filePath: path,
}).then((downloadTask: request.DownloadTask) => {
downloadTask.on('progress', (receivedSize: number, totalSize: number) => {
console.info(`download progress: ${receivedSize}/${totalSize}`);
})
downloadTask.on('complete', () => {
console.info('download complete');
unzip(path, context.filesDir);
})
}).catch((err: BusinessError) => {
console.error(`Invoke downloadTask failed, code is ${err.code}, message is ${err.message}`);
});
} catch (error) {
let err: BusinessError = error as BusinessError;
console.error(`Invoke downloadFile failed, code is ${err.code}, message is ${err.message}`);
}
}
.....
//解压缩代码
const unzip = (zipPath: string, path: string) => {
zlib.decompressFile(zipPath, path).then(() => {
console.info('decompressFile success');
initOffLine(path);
}).catch((err: BusinessError) => {
console.error(`Invoke decompressFile failed, code is ${err.code}, message is ${err.message}`);
});
}
- 把离线包加载到本地内存里 在这里通过本地一个HashMap 来保存离线包数据 遍历离线包文件夹,并将所有的文件通过文件系统读取成text ,然后创建一个WebResourceResponse来存储,用文件的路径做为key方便读取的时候匹配文件
const 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,否则就走系统的网络正常加载,这样做的好处是,离线包没加载或者离线包没有资源的时候,也能正常加载网页,网络和离线包走同一个方案,随时切换离线包和在线方式 代码如下
.onInterceptRequest((event) => {
//获取请求地址
const requestUrl = event?.request?.getRequestUrl();
let key ="";
//判断是不是静态资源
if (requestUrl?.startsWith(this.url)){
key = requestUrl.substring(this.url.length);
// 对/结尾的资源特殊处理 追加上index.html
if (key.endsWith("/")) {
key += "index.html"
}
//判断本地离线包是否命中
let response = offLineMap.get(key)
if ( response != null) {
//命中离线包资源,使用本地离线包数据
return response
}
} //没有命中离线包,正常请求
return;
})
这样一个简单的离线包方案就好了,我们运行下看下效果
网络关闭后网页可以正常加载,图片因为是CDN链接没有打到离线资源导致图片未显示,由此可见基于onInterceptRequest拦截请求的离线包方式总体可行的
总结
本文只是简单验证下离线包方案,离线包技术远远没有这么简单,对于离线包的加载时机和加载流程、离线包管理、安全验证、方案降级等还有众多需要完善的地方,我们后面接着探索
我们使用了HarmonyOS的下载、压缩、文件管理等多个API,基本上没有费劲就完成了简单的方案验证,由此可见 HarmonyOS对于开发者来说是一个很完善的方案了,也提供了大量的开发者工具,让我们开发者可以快速上手鸿蒙开发,总之鸿蒙,未来可期。
鸣谢
感谢ChatGPT 对本文的写作帮助
感谢Github Copilot 对本文代码帮助
相关推荐
- 鸿蒙HarmonyOS实战-ArkUI组件(Stack)
- 鸿蒙HarmonyOS实战-ArkUI组件(Flex)
- 鸿蒙HarmonyOS实战-ArkUI组件(RelativeContainer)
- 鸿蒙HarmonyOS实战-ArkUI组件(GridRow/GridCol)
- 鸿蒙HarmonyOS实战-ArkUI组件(mediaquery)
- HarmonyOS 穿戴应用
- 开启Android学习之旅-2-架构组件实现数据列表及添加(kotlin)
- 鸿蒙HarmonyOS实战-ArkTS语言(基本语法)
- 鸿蒙HarmonyOS实战-ArkTS语言(状态管理)
- HarmonyOS鸿蒙应用开发——数据持久化Preferences
- 程序开发学习排行
-
- 1鸿蒙HarmonyOS:Web组件网页白屏检测
- 2HTTPS协议是安全传输,为啥还要再加密?
- 3HarmonyOS鸿蒙应用开发——数据持久化Preferences
- 4记解决MaterialButton背景颜色与设置值不同
- 5鸿蒙HarmonyOS实战-ArkUI组件(RelativeContainer)
- 6鸿蒙HarmonyOS实战-ArkUI组件(Stack)
- 7[Android][NDK][Cmake]一文搞懂Android项目中的Cmake
- 8Android广播如何解决Sending non-protected broadcast问题
- 9鸿蒙HarmonyOS实战-ArkUI组件(mediaquery)
- 最近发表
-
- WooCommerce最好的WordPress常用插件下载博客插件模块的相关产品
- 羊驼机器人最好的WordPress常用插件下载博客插件模块
- IP信息记录器最好的WordPress常用插件下载博客插件模块
- Linkly for WooCommerce最好的WordPress常用插件下载博客插件模块
- 元素聚合器Forms最好的WordPress常用插件下载博客插件模块
- Promaker Chat 最好的WordPress通用插件下载 博客插件模块
- 自动更新发布日期最好的WordPress常用插件下载博客插件模块
- WordPress官方最好的获取回复WordPress常用插件下载博客插件模块
- Img to rss最好的wordpress常用插件下载博客插件模块
- WPMozo为Elementor最好的WordPress常用插件下载博客插件模块添加精简版