cordova-openharmony是cordova的鸿蒙化版本,所有接口兼容Cordova的Android和iOS版本
本文档说明
本文档仅说明cordova-openharmony框架部分的使用手册、开发说明,集成步骤等,不包括demo,demo部分参考everything版本仓库
开发说明
cordova-openharmony是cordova的鸿蒙化版本,并支持ArkTS侧和C/C++侧自定义插件研发,框架采用C/C++研发,底层使用自研Socket TCP/IP通讯,封装了HTTP/HTTPS协议通讯解决各种跨域访问问题,无需配置web服务端,同时结合webview的通讯协议栈,大大提高应用层网络请求效率。
附加说明
cordova-openharmony使用多页面视图研发,同时兼容Android和iOS原有的单页面视图,原有项目可以轻松移植;另外在复杂项目中,可以使用cordova-openharmony的多页面视图功能,创建多个webview协同工作。
开发背景
cordova是Apache基金会的开源项目,官方网站:https://cordova.apache.org,是移动端跨平台框架,地位不可撼动,大量厂商直接或间接采用此框架开发APP;但是目前不支持HarmonyOS Next版本,开发者将原Android和Ios项目移植到HarmonyOS Next版,无法适配,为此我们研发了cordova-openharmony,遵守cordova官方标准,原有项目无需投入任何研发轻松移植到鸿蒙系统;新开发的项目,一次研发就适用于Android、Ios和HarmonyOS三大平台,也节省了大量的时间和人力成本。
框架自带插件
CoreHarmony插件
名称:鸿蒙内核插件对标CoreAndroid插件
Cordova内部插件,主要Cordova内部使用,同时也提供了外部使用的方法,例如清除缓存,返回等,列举使用方法如下:
清除缓存:
cordova.exec(
function(successMessage) {
},
function(errorMessage) {
},
'CoreHarmony',
'clearCache',
[]
);
清除历史数据:
cordova.exec(
function(successMessage) {
},
function(errorMessage) {
},
'CoreHarmony',
'clearHistory',
[] // 空数组,因为不需要参数
);
打开外置浏览器:
打开内置浏览器建议使用cordova-plugin-inappbrowser
cordova.exec(
function(successMessage) {
},
function(errorMessage) {
},
'CoreHarmony',
'loadUrl',
["https://developer.huawei.com", {openexternal:true}]
);
cordova-plugin-inappbrowser
名称:内置浏览器
框架自带内置浏览器,主要用于在页面中打开新窗口时,自动打开内置浏览器加载页面,举例如下,另外框架部分不包括该插件的JS部分,如果使用JS侧动态控制,请安装cordova-plugin-inappbrowser插件的全量功能。
使用示例:
<!--自动激活内置浏览器和路由功能,打开新窗口加载页面 -->
<!--访问网络,请设置网络权限-->
<a href="https://developer.huawei.com" target="_blank">打开新页面</a>
config.xml配置如下:
<!--cordova内部系统导航条背景色 -->
<preference name="NavigationBarBackgroundColor" value="#F90707" />
<!--cordova内部系统导航条文字按钮颜色 -->
<preference name="NavigationBarFontColor" value="#ffffff" />
<!--cordova内部系统导航条title位置left|center|right -->
<preference name="NavigationBarFontAlign" value="center" />
<!--导航栏高度配置-->
<preference name="NavigationBarHeight" value="44" />
cordova-plugin-splashscreen
名称:web闪屏插件
cordova-openharmony框架自带闪屏功能,在web页面没有加载全部完成时先显示闪屏,页面加载完毕后闪屏消失,框架部分不包括该插件的JS侧部分功能,如果需要JS侧控制部分,请安装cordova-plugin-splashscreen的全量功能,多页面视图只有第一个视图会出现闪屏,其他视图不会自动出现闪屏,其他视图推荐使用预加载和预渲染功能实现页面秒显,无需闪屏。
鸿蒙闪屏资源图片说明:
资源图片名称竖屏:splash_portrait,横屏:splash_landscape,存放在在resource/media目录下,如果资源图片不存在,闪屏不启动
在config.xml配置相关参数举例如下:
- 在web加载完成后,自动隐藏:
<preference name="AutoHideSplashScreen" value="true" />
- 必须在web加载完成后,延迟3秒钟关闭(0禁用闪屏):
<preference name="SplashScreenDelay" value="3000" />
- 0禁用闪屏, 显示持续时间(在web初始化完后,显示的时间):
<preference name="SplashScreenDelay" value="1000"/>
- 显示Spinner:
<preference name="ShowSplashScreenSpinner" value="ture"/>
- 全屏显示:
<preference name="SplashMaintainAspectRatio" value="ture"/>
- 设置Spinner颜色:
<preference name="SplashScreenSpinnerColor" value="#ffffff"/>
- 闪屏消失动画持续时间:
<preference name="FadeSplashScreenDuration" value="2000"/>
- 闪屏全屏显示true:全屏,false:非全屏:
<preference name="SplashMaintainAspectRatio" value="true"/>
- 闪屏关闭后全屏显示true:全屏,false:非全屏:
<preference name="FullScreenAfterCloseSplash" value="false" />
cordova-openharmony源码集成使用说明
1. 创建项目
打开DevEco创建项目,选择Empty Ability进入下一步(next),填写必要信息,点击完成(finish),工程创建完成。
2. 集成源码
下载本har包工程,解压后将顶级目录改为cordova,放入主工程文件夹中,此时在DevEco中已经可以看到cordova的har工程了。
3. 安装依赖
控制台(Terminal)进入主工程entry目录执行ohpm install ../cordova,然后再修改entry/build-profile.json5(项目级)配置文件,在modules模块中增加:
{
"name": "cordova",
"srcPath": "./cordova",
}
以上三步操作后,已经在主工程中集成了cordova的源码har包
4. 项目移植
Android项目移植:
复制原有Android studio的工程assets目录下面的所有文件到鸿蒙工程entry/src/main/resources/rawfile目录下,原Android工程的assets目录必须包含www目录,www目录包含index.html(必须)、cordova.js(必须)、cordova_plugins.js(必须)、plugins目录(必须)、css目录、js目录等,如果要指定加载页面,不使用默认页面,请查看高级功能部分说明。复制成功后,仍需要安装Android包含的鸿蒙版插件。
iOS项目移植:
第一步:复制Xcode的Ios工程目录下的Staging目录下的www文件夹复制到鸿蒙工程entry/src/main/resources/rawfile目录下。
第二步:Xcode工程的config.xml文件在Staging目录下,Xcode工程的该文件不能直接被cordova-openharmony使用,需要进行转换,该文件主要记录的是插件的名称和初始化的类,因为鸿蒙版是根据android的config.xml进行插件初始化的,因此需要将Xcode工程config.xml转为安卓的config.xml,请将Xcode工程使用node加入安卓平台,系统会自动生成android版的config.xml。然后将文件复制到鸿蒙版工程的entry/src/main/resources/rawfile下。复制成功后,仍需要安装iOS包含的鸿蒙版插件。
新建项目:
如果您没有Android和iOS项目,需要使用cordova的命令化工具,创建Android项目,创建成功后,再按照Android项目移植方法操作即可。
5. 修改Index.ets文件
打开鸿蒙工程文件entry/src/main/etx/pages/Index.ets文件,修改代码如下(可以直接全部拷贝和复制到Index.ets文件中):
import { MainPage, pageBackPress, pageHideEvent, pageShowEvent, PluginEntry} from '@magongshou/harmony-cordova/Index';
//import { TestPlugin } from "../plugins/TestPlugin" //自定义插件TestPlugin,根据实际情况导入自己的自定义插件
@Entry
@Component
struct Index {
//ArkTs侧的自定义插件:配置插件名称和对象,请查看自定义查看开发部分
cordovaPlugs:Array< PluginEntry> = [];
/*
cordovaPlugs:Array< PluginEntry> =
[
{
pluginName: 'TestPlugin', //插件名称
pluginObject:new TestPlugin() //实例化插件对象供cordova调用
}
];
*/
onPageShow(){
pageShowEvent(); //页面显示通知cordova
}
onBackPress() {
pageBackPress(); //拦截返回键由cordova处理
return true;
}
onPageHide() {
pageHideEvent(); //页面隐藏通知cordova
}
build() {
RelativeContainer() {
//默认加载rawfile/www/index.html
//如果要指定加载页面参考高级功能部分
MainPage({isWebDebug:false,cordovaPlugs:this.cordovaPlugs});
}
.height('100%')
.width('100%')
}
}
6. 修改EntryAbility.ets文件
打开鸿蒙工程文件/entry/src/main/ets/entryAbility/EntryAbility.ets文件,修改onCreate函数如下:
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import { webview } from '@kit.ArkWeb'; //引入webview
... //省略部分代码
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
webview.WebviewController.initializeWebEngine();//webview引擎初始化
}
7. 完成
做以上代码修改后,鸿蒙的移植已经完毕,可以使用模拟器或者真机进行编译和测试了。
高级用法,区别于Android和iOS
- 自定义webview打开路径页面路径,支持rawfile资源文件、沙箱路径、在线网页
- 自定义用户scheme,用于cordova内部拦截scheme的请求
- 拦截自定义的scheme,在webview端并处理
- 在原生层,直接设置cordovaWebview属性和注入新的js代码,不通过插件实现,用于原生组件和webview组件显示在同一视图界面
- 多webview界面,即多页面视图,自定义webId,使用自定义插件各webview之间通讯,可用于平板等大屏幕研发需求
- 多webview之间的通讯,参考自定义插件研发示例代码说明
- 动态创建组件,在webview和NodeController相结合实现动态创建和显示组件时,切记一定要设定webId参数,避免重复创建webview
- 父组件感知MainPage子组件的所有生命周期,在不同的周期执行相应的操作
- 在同一个webview中加载在线网页,也可以本地页面和在线网页混合开发
- 加载不包含cordova.js页面,父组件控制webview的返回键,或者自己控制路由
- MainPage的路由开关控制,便于MainPage嵌套使用,路由子页面内再嵌套使用MainPage
- 自定义cookie,传入cookie键值对
- 自定义webview字体大小缩放百分比,支持适老化,屏蔽跟随系统字体大小变化
- 同层渲染,以及同层渲染组件和Cordova插件结合的使用的方法
示例代码如下:
import { MainPage, pageBackPress, pageHideEvent, pageShowEvent} from '@magongshou/harmony-cordova/Index';
import { CordovaWebView, PluginEntry, MainPageCycle } from '@magongshou/harmony-cordova/Index';
import { SetResourceReplace} from '@magongshou/harmony-cordova/Index';
import { TestPlugin } from '../plugins/TestPlugin';
import { BuilderNode, FrameNode, NodeController } from '@kit.ArkUI';
//动态创建MainPage的示例代码,主要用于原生界面和webview界面显示在同一个视图里面的混合式研发
//如果要传入其他参数,参考此文档详细了解
//https://developer.huawei.com/consumer/cn/doc/best-practices-V5/bpta-ui-dynamic-operations-V5
@Builder
function buildMainPage() {
Column() {
//直接加载在线网站
MainPage({webId:"123456", indexPage:"https://cn.bing.com", cordovaPlugs:[
{
pluginName: 'TestPlugin', //插件名称
pluginObject:new TestPlugin() //实例化插件对象供cordova调用
}
]});
}.width("100%").height("100%")
}
class TextNodeController extends NodeController {
private textNode: BuilderNode<[]> | null = null;
constructor() {
super();
}
makeNode(context: UIContext): FrameNode | null {
// 创建BuilderNode实例
this.textNode = new BuilderNode(context);
this.textNode.build(wrapBuilder<[]>(buildMainPage));
// 返回需要显示的节点
return this.textNode.getFrameNode();
}
}
///////////////////
//同层渲染示例代码,H5页面增加一个原生的TextInput组件
@Observed
declare class Params{
elementId: string
textOne: string
textTwo: string
width: number
height: number
onTextChange?: (value: string) => void;
}
@Component
struct TextInputComponent {
@Prop params: Params
@State bkColor: Color = Color.Blue
build() {
Column() {
TextInput({text: '', placeholder: 'please input your word...'})
.placeholderColor(Color.Gray)
.id(this.params?.elementId)
.placeholderFont({size: 13, weight: 400})
.caretColor(Color.Gray)
.width(this.params?.width)
.height(this.params?.height)
.fontSize(14)
.fontColor(Color.Black)
.onChange((value:string)=>{
if (this.params.onTextChange) {
this.params.onTextChange(value); // 触发回调
}
})
}
//自定义组件中的最外层容器组件宽高应该为同层标签的宽高
.width(this.params.width)
.height(this.params.height)
}
}
@Builder
function TextInputBuilder(params:Params) {
TextInputComponent({params: params})
.width(params.width)
.height(params.height)
.backgroundColor(Color.White)
}
class MyNodeController extends NodeController {
private rootNode: BuilderNode<[Params]> | undefined | null;
private embedId_: string = "";
private surfaceId_: string = "";
private renderType_: NodeRenderType = NodeRenderType.RENDER_TYPE_DISPLAY;
private width_: number = 0;
private height_: number = 0;
private type_: string = "";
private isDestroy_: boolean = false;
setRenderOption(params: ESObject) {
this.surfaceId_ = params.surfaceId;
this.renderType_ = params.renderType;
this.embedId_ = params.embedId;
this.width_ = params.width;
this.height_ = params.height;
this.type_ = params.type;
}
// 必须要重写的方法,用于构建节点数、返回节点数挂载在对应NodeContainer中。
// 在对应NodeContainer创建的时候调用、或者通过rebuild方法调用刷新。
makeNode(uiContext: UIContext): FrameNode | null {
if (this.isDestroy_) { // rootNode为null
return null;
}
if (!this.rootNode) {// rootNode 为undefined时
this.rootNode = new BuilderNode(uiContext, { surfaceId: this.surfaceId_, type: this.renderType_ });
if(this.rootNode) {
this.rootNode.build(wrapBuilder(TextInputBuilder),
{textOne: "myTextInput", width: this.width_, height: this.height_, onTextChange:(value:string)=>{
//TextInput值该表后,通知js侧,这里只是列举了一个简单的例子,以实际情况执行js代码
let jsFun:string = "setValue('"+value+"')";
try {
this.cordovaWebView?.getWebviewController().runJavaScript(jsFun);
} catch (error) {
console.log(error);
}
}})
return this.rootNode.getFrameNode();
}else{
return null;
}
}
return this.rootNode.getFrameNode();
}
updateNode(arg: Object): void {
this.rootNode?.update(arg);
}
getEmbedId(): string {
return this.embedId_;
}
setDestroy(isDestroy: boolean): void {
this.isDestroy_ = isDestroy;
if (this.isDestroy_) {
this.rootNode = null;
}
}
postEvent(event: TouchEvent | undefined): boolean {
return this.rootNode?.postTouchEvent(event) as boolean
}
}
///////////
@Entry
@Component
export struct Index {
@State indexPage:string = "/www/index.html";
@State isShow: boolean = true;
@State textZoomRatio:number = 100;
//同层渲染组件属性,这里列举的是TextInput的同层渲染组件
public nodeControllerMap: Map< string, MyNodeController> = new Map();
@State componentIdArr: Array< string> = [];
@State widthMap: Map< string, number> = new Map();
@State heightMap: Map< string, number> = new Map();
@State positionMap: Map< string, Edges> = new Map();
@State edges: Edges = {};
@State textValue:string = "hello";
/*
*控制mainPage的页面返回,需将此对象传入MainPage
*如果加载的页面不包含cordova.js,使用pageBackPress无法通知cordova返回,必须使用此对象控制页面返回
*也可以通过此对象控制webview的路由
*/
mainPageOnBackPress:MainPageOnBackPress = new MainPageOnBackPress();
//手动添加cookie,在发送POST或者Get请求时携带cookie,https的session cookie无需手动设置,cordova会自动处理
//http的非https的session cookie参考最后的https的cookie说明
this.cookies.set("https://mem.tongecn.com", ["key1=value1; path=/; Domain=.tongecn.com", "key2=value2"]);
//MainPage的生命周期的各回调函数,根据业务需要设置单个或多个生命周期回调函数添加业务功能
//生命周期的说明参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/web-event-sequence-V5
mainPageCycle?:MainPageCycle;
aboutToAppear() {
this.mainPageCycle = new MainPageCycle()
.setOnAboutToAppear((webviewController: webview.WebviewController,parentPage?:object)=>{
//page为当前页面对象,相当于当前页面的this指针,使用该对象,必须将this指针通过parentPage参数传入mainPage中
let page = parentPage as Index;
console.log("exec onAboutToAppear");
})
.setOnControllerAttached((webviewController: webview.WebviewController,parentPage?:object)=>{
console.log("exec onControllerAttached");
})
.setOnLoadIntercept((webResourceRequest: WebResourceRequest,parentPage?:object):boolean=>{
console.log("exec onLoadIntercept");
return false;
})
.setOnOverrideUrlLoading((webResourceRequest: WebResourceRequest,parentPage?:object):boolean=>{
console.log("exec onOverrideUrlLoading");
return false;
})
.setOnInterceptRequest((request: WebResourceRequest, webTag:string,parentPage?:object):WebResourceResponse|null=>{
console.log("exec setOnInterceptRequest");
return null;
})
.setOnPageBegin((url:string,parentPage?:object):void=>{
console.log("exec onPageBegin");
})
.setOnProgressChange((newProgress: number,parentPage?:object):void=>{
console.log("exec onProgressChange");
})
.setOnPageEnd((url:string, webviewController: webview.WebviewController,parentPage?:object):void=>{
console.log("exec onPageEnd");
})
.setOnPageVisible((url:string,parentPage?:object):void=>{
console.log("exec onPageVisible");
})
.setOnRenderExited((renderExitReason:RenderExitReason,parentPage?:object):void=>{
console.log("exec onRenderExited");
})
.setOnDisAppear((parentPage?:object):void=>{
console.log("exec onDisAppear");
});
//设置cookies,session cookie无需手动设置,cordova会自动处理
//http的非https的session cookie参考最后的https的cookie说明
this.cookies.set("https://ceshi.tongecn.com", "key1=value1;key2=value2");
}
onPageShow(){
pageShowEvent();
}
onBackPress() {
pageBackPress();
/*
*如果加载的页面没有包含cordova.js,例如加载https://cn.bing.com,
* 返回值
* true:已经到了页面等层
* false:返回了上一页
*/
// return this.mainPageOnBackPress.backPress();
return true;
}
onPageHide() {
pageHideEvent();
}
/*
* 这个MainPage父组件的测试函数,主要演示在插件中调用父组件的函数,以满足业务需求
* 请参考自定义插件研发示例代码
*/
DoTest() {
console.log("fjdkfjdkf");
}
/*
*拦截请求函数,根据需要拦截相应请求,一般用于自定义scheme,如果存在自定义scheme的必须使用此函数拦截处理
*使用此函数拦截自己的scheme进行处理,也可以在MainPage生命周期回调函数中拦截处理,二选一,不能同时拦截处理。
*拦截后处理有两种方式,推荐使用第一种方式
* 1,cordova webview内核处理,返回null,cordova可以处理替换所有资源,例如在线资源,本地资源,js、img、css等
* 2,自己处理,返回WebResourceRequest
*说明如下:
* 1,子组件的回调函数不能使用this指针,如果要使用this,请参考parentPage参数
* 2,采用第一种方式,写法简单,且效率高,推荐第一种方式
*/
onInterceptWebRequest(request: WebResourceRequest, webTag:string):ESObject {
let url= request.getRequestUrl();
//cordova webview内核处理替换
if(url == "cmp://v1.1.1/temp/test2.png") {
/*
*替换资源说明如下:
*本地资源请使用https://www.example.com的虚拟域名作为访问本地资源的标记
*详细了解www.example.com内置虚拟域名规则,查看最后面的常见问题说明
*被替换和替换内容可以是图片、css、js等
*替换资源举例如下:
*1,沙箱路径
* https://www.example.com/data/storage/el2/base/files/test.png
*2,rawfile目录的下的资源文件
* https://www.example.com/www/test.png
*3,网络在线资源
* https://www.chuzhitong.com/images/logo.png
*4,cdvfile协议的沙箱路径的文件,绝对路径
* cdvfile:///data/storage/el2/base/files/test.png
*此函数是通知cordova webview内核,后续加载页面实施资源替换
*/
SetResourceReplace(webTag, url, "https://www.chuzhitong.com/images/logo.png");
}
//cordova webview内核处理替换
if(url == "https://www.ext.com/v1.1.1/temp/test3.png") {
SetResourceReplace(webTag, url, "https://www.chuzhitong.com/images/logo.png");
}
//自己处理资源返回webview
if(url == "https://www.ext.com/v1.1.1/temp/test3.png") {
let response = new WebResourceResponse();
response.setResponseData($rawfile("www/picture/bao.png"));
response.setResponseEncoding('utf-8');
response.setResponseMimeType("image/png");
response.setResponseCode(200);
response.setReasonMessage('OK');
response.setResponseIsReady(true);
return response;
}
return null;
}
/*
*在原生层页面加载后,网页面中注入新的js,可以不通过插件来实现
*子组件的回调函数不能使用this指针
*/
onSetCordovaWebAttribute(cordovaWebView:CordovaWebView) {
if(cordovaWebView) {
//获取webview属性变量,用于动态修改webview属性,具体参考如下连接,页面加载完成后触发
//鸿蒙并不支持WebAttribute组件属性的动态设置,但是可以设置部分属性,不支持的属性会抛出"Method not implemented."、"is not callable"等异常信息
//如果要设置cordova没有开放的web属性,请于开发者联系以满足您的技术要求
//https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-webview-V5
//https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-universal-attributes-attribute-modifier#attributemodifier
cordovaWebView!.getWebAttribute()?.height('50%');
//获取webview的控制变量,用于实现具体的功能,示例代码实现在webviw执行js或者注入新的js,具体参考如下连接
//https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/ts-basic-components-web-V5
cordovaWebView!.getWebviewController().runJavaScript("alert(1);");
}
}
/*
*同层渲染生命周期回调函数
*参考连接:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/web-same-layer
*/
onNativeEmbedLifecycleChange(embed: NativeEmbedDataInfo,cordovaWebView:CordovaWebView,parentPage?:object) {
let page = parentPage as Index;//page为当前页面对象,相当于当前页面的this指针,使用该对象,必须将this指针传入到mainPage中
console.log("NativeEmbed surfaceId" + embed.surfaceId);
// 如果使用embed.info.id作为映射nodeController的key,请在h5页面显式指定id
const componentId = embed.info?.id?.toString() as string
if (embed.status == NativeEmbedStatus.CREATE) {
console.log("NativeEmbed create" + JSON.stringify(embed.info));
// 创建节点控制器、设置参数并rebuild
let nodeController = new MyNodeController()
// embed.info.width和embed.info.height单位是px格式,需要转换成ets侧的默认单位vp
nodeController.setRenderOption({surfaceId : embed.surfaceId as string,
type : embed.info?.type as string,
renderType : NodeRenderType.RENDER_TYPE_TEXTURE,
embedId : embed.embedId as string,
width : cordovaWebView.getUIContext().px2vp(embed.info?.width),
height : cordovaWebView.getUIContext().px2vp(embed.info?.height),
cordovaWebView:cordovaWebView,
textValue:page.textValue
})
page.edges = {left: `${embed.info?.position?.x as number}px`, top: `${embed.info?.position?.y as number}px`}
nodeController.setDestroy(false);
//根据web传入的embed的id属性作为key,将nodeController存入Map
page.nodeControllerMap.set(componentId, nodeController);
page.widthMap.set(componentId, cordovaWebView.getUIContext().px2vp(embed.info?.width));
page.heightMap.set(componentId, cordovaWebView.getUIContext().px2vp(embed.info?.height));
page.positionMap.set(componentId, page.edges);
// 将web传入的embed的id属性存入@State状态数组变量中,用于动态创建nodeContainer节点容器,需要将push动作放在set之后
page.componentIdArr.push(componentId)
} else if (embed.status == NativeEmbedStatus.UPDATE) {
let nodeController = page.nodeControllerMap.get(componentId);
console.log("NativeEmbed update" + JSON.stringify(embed));
page.edges = {left: `${embed.info?.position?.x as number}px`, top: `${embed.info?.position?.y as number}px`}
page.positionMap.set(componentId, page.edges);
page.widthMap.set(componentId, cordovaWebView.getUIContext().px2vp(embed.info?.width));
page.heightMap.set(componentId, cordovaWebView.getUIContext().px2vp(embed.info?.height));
nodeController?.updateNode({textOne: 'update',
width: cordovaWebView.getUIContext().px2vp(embed.info?.width),
height: cordovaWebView.getUIContext().px2vp(embed.info?.height),
text:page.textValue, onTextChange:page.onTextChangeCallBack} as ESObject);
} else if (embed.status == NativeEmbedStatus.DESTROY) {
console.log("NativeEmbed destroy" + JSON.stringify(embed));
let nodeController = page.nodeControllerMap.get(componentId);
nodeController?.setDestroy(true)
page.nodeControllerMap.clear();
page.positionMap.delete(componentId);
page.widthMap.delete(componentId);
page.heightMap.delete(componentId);
page.componentIdArr.filter((value: string) => value != componentId)
} else {
console.log("NativeEmbed status" + embed.status);
}
}
/*
*同层渲染手势事件
*参考连接:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/web-same-layer
*/
onNativeEmbedGestureEvent(touch: NativeEmbedTouchInfo,cordovaWebView:CordovaWebView,parentPage?:object) {
let page = parentPage as Index;//page为当前页面对象,相当于当前页面的this指针,使用该对象,必须将this指针传入到mainPage中
console.log("NativeEmbed onNativeEmbedGestureEvent" + JSON.stringify(touch.touchEvent));
page.componentIdArr.forEach((componentId: string) => {
let nodeController = page.nodeControllerMap.get(componentId);
// 将获取到的同层区域的事件发送到该区域embedId对应的nodeController上
if(nodeController?.getEmbedId() == touch.embedId) {
let ret = nodeController?.postEvent(touch.touchEvent)
if(ret) {
console.log("onNativeEmbedGestureEvent success " + componentId);
} else {
console.log("onNativeEmbedGestureEvent fail " + componentId);
}
if(touch.result) {
// 通知Web组件手势事件消费结果
touch.result.setGestureEventResult(ret);
}
}
})
}
/*
*同层渲染的TextInput文本改变后回调该函数
*可以通过自定义插件获取改变后的值,这里只是举一个例子,便于插件获取TextInput获取显示的文本
*/
onTextChangeCallBack(value:string) {
this.textValue = value;
}
/*
*设置同层渲染TextInput的显示文本
*可以通过自定义插件设置TextInput的显示文本
*/
setNativeValue(id:string, value:string){
this.textValue = value;
let nodeController = this.nodeControllerMap.get(id);
nodeController?.updateNode({textOne: 'update',
width: this.widthMap.get(id),
height: this.heightMap.get(id),
text:this.textValue, onTextChange:this.onTextChangeCallBack} as ESObject)
}
//Web组件可以通过W3C标准协议授权回调函数,例如拉起摄像头和麦克风,示例如下
onPermissionRequest(event:OnPermissionRequestEvent,parentPage?:object){
let page = parentPage as Index;//page为当前页面对象,相当于当前页面的this指针,使用该对象,必须将this指针传入到mainPage中
if (event) {
//拉起摄像头和麦克风,为确保用户拒绝后能二次拉起授权,需要多个授权时,单独分开授权
//单独分开授权会多次弹出窗口,仅供参考,也可以一次授权多个权限,但是用户拒绝后,无法拉起二次授权窗口
//授权摄像头和麦克风,弹窗授权
//const yourPermissions: Array< Permissions> = ['ohos.permission.CAMERA', 'ohos.permission.MICROPHONE'];
//授权加速度和陀螺仪,无弹窗用户无感知
const yourPermissions: Array< Permissions> = ['ohos.permission.ACCELEROMETER', 'ohos.permission.GYROSCOPE'];
for (let i = 0; i < yourPermissions.length; i++) {
let confirmPermissions: Array< Permissions> = [yourPermissions[i]];
let atManager = abilityAccessCtrl.createAtManager();
atManager.requestPermissionsFromUser(getContext(this), confirmPermissions).then((data) => {
let grantStatus: Array< number> = data.authResults;
if (grantStatus[0] != 0) {
// 用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限
atManager.requestPermissionOnSetting(getContext() as common.UIAbilityContext, confirmPermissions)
.then((data: Array< abilityAccessCtrl.GrantStatus>) => {
if (data.length > 0 && data[0] == 0 ) {
event.request.grant(event.request.getAccessibleResource());
}
console.info('data:' + JSON.stringify(data));
})
.catch((err: BusinessError) => {
console.error('data:' + JSON.stringify(err));
return;
});
} else{
event.request.grant(event.request.getAccessibleResource());
}
}).catch((error: BusinessError) => {
console.error(`Failed to request permissions from user. Code is ${error.code}, message is ${error.message}`);
})
}
}
}
build() {
Column() {
RelativeContainer() {
//同层渲染
ForEach(this.componentIdArr, (componentId: string) => {
NodeContainer(this.nodeControllerMap.get(componentId))
.position(this.positionMap.get(componentId))
.width(this.widthMap.get(componentId))
.height(this.heightMap.get(componentId))
}, (embedId: string) => embedId)
/*
*webId:自定义webId,用于多webview,各webview之间通讯,参考自定义插件研发示例代码
*indexPage:默认启动首页,举例如下:
* "/www/index.html":rawfile目录下的文件
* "/data/storage/el2/base/files/www/index.html":使用虚拟域名www.example.com加载沙箱路径下的文件,
* "https://cn.bing.com":加载在线网页,必须指定https或者http
* "file:///data/storage/el2/base/files/www/index.html":file协议加载el2级别沙箱路径文件
* "file:///data/storage/el1/bundle/entry/resources/resfile/www/index.html":file协议加载el1级别沙箱路径文件
* "file://"+getContext().resourceDir+"/www/index.html":file协议加载el1级别沙箱路径文件
* cordova支持使用虚拟域名www.example.com加载本地文件,也支持使用file协议加载本地文件
* 改变this.indexPage的值,webview会重新加载页面
*customSchemes:自定义scheme,多个scheme用","分隔
*onInterceptWebRequest 返回null放行,返回具体的WebResourceResponse
* 参考https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/web-cross-origin-V5 说明
*onSetCordovaWebAttribute 设置webview属性和注入新的js代码
*parentPage:传入this,就是webview父组件对象,也就是当前组件的对象,可以在插件里面调用
*lifeCycle:传入生命周期对象,让父组件感知MainPage的生命周期,进行相应业务处理
*backPress:传入控制webview路由的对象,加载的页面不包含cordova.js时控制webview路由
*isNavPath:true使用MainPage组件内的路由,默认是true,false:不使用MainPage内的路由,
* 特别是MainPage嵌套使用时,父组件要打开路由,子组件关闭路由,否则会路由冲突
*cookies:如果ArkTs侧有自定义的cookie,可以通过此参数传入
* 一般情况下cookie都是cordova自动处理的,无需ArkTS侧手动设置,不过ArkTS侧通过此参数可以手动设置cookie
* 如果您的请求是采用的http协议非https,分为跨域请求和非跨域请求,请查看最后的常见问题说明
*textZoomRatio:webview字体放大缩小百分比,默认是100保持默认
* 设置webview不跟随系统字体大小、并且屏蔽跟随显示大小缩放后
* 可以通过此参数统一设置webview字体大小变化百分比,避免页面错乱
* 也可以通过鸿蒙Device插件增加的字体大小百分比接口函数,在js侧设置,参考Device插件
* 参考常见问题屏蔽跟随系统字体大小和屏蔽跟随显示大小缩放
*webKeyboardAvoidMode:避让键盘模式,默认:WebKeyboardAvoidMode.RESIZE_VISUAL
*customHttpHeaders:自定义http头
* 前端withCredentials为true时添加自定义http头,没有自定义http头不用添加
* 参考常见问题的跨域说明
*isAllowCredentials:默认是false
* 前端请求设置withCredentials为true,要传入参数isAllowCredentials:true
* 参考常见问题的跨域说明
*nativeEmbedHtmlTag:注册同层渲染标签
* 默认是:<embed>的标签 ,如果要注册object,请传入object,同层渲染只支持这两个标签
*nativeEmbedHtmlType:注册同层选择标签类型
* 默认是:native类型,如要要传入其他类型,请随意取名字
*onNativeEmbedLifecycleChange:同层渲染元素生命周期函数
*onNativeEmbedGestureEvent:同层渲染手势回调函数
* 同层渲染参考连接:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/web-same-layer
*onPermissionRequest:web组件W3C标准拉起授权的回调函数
* 参考连接:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/web-rtc-V5?catalogVersion=V5
*/
MainPage({
webId:"123456",
isWebDebug:true,
indexPage:this.indexPage,
customSchemes:"cmp,xmp,xxx",
onInterceptWebRequest:this.onInterceptWebRequest,
onSetCordovaWebAttribute:this.onSetCordovaWebAttribute,
parentPage:this,
lifeCycle:this.mainPageCycle,
backPress:this.mainPageOnBackPress,
cookies:this.cookies,
textZoomRatio:this.textZoomRatio,
webKeyboardAvoidMode:WebKeyboardAvoidMode.RESIZE_VISUAL,
customHttpHeaders:"",
isAllowCredentials:false,
onNativeEmbedLifecycleChange:this.onNativeEmbedLifecycleChange,
onNativeEmbedGestureEvent:this.onNativeEmbedGestureEvent
});
//MainPage();//所有参数都不是必填项,根据需要自主设定
}
.height('80%')
.width('100%')
RelativeContainer() {
if (this.isShow) {
NodeContainer(this.textNodeController)
.width('100%')
.height("100%")
.backgroundColor('#FFF0F0F0')
}
}
.height('30%')
.width('100%')
//测试按钮,在webview不是全屏的时候,控制webview显示的页面实现原生控件和webiew的交互
Button("修改沙箱路径").onClick(()=>{
//这个是测试的示例路径,将页面直接加载沙箱路径
this.indexPage = "/data/storage/el2/base/haps/entry/cache/index.html";
//也可以直接加载在线网页,
//this.indexPage = "https://cn.bing.com";
})
Button("删除组件").onClick(()=>{
this.isShow = false;
})
}
}
}
自定义ArKTS插件研发
自定义插件研发,需具备cordova插件研发和鸿蒙原生研发能力,自定义插件接口遵守cordova sdk官方规范,以自定义插件TestPlugin、为例:
(1)新建ArkTs文件
新建ArkTs文件,取名字为TestPlugin,示例代码如下,具体功能参考示例代码注释说明。
import { CordovaPlugin,CordovaInterface, CallbackContext} from '@magongshou/harmony-cordova/Index';
import { CordovaWebView, MessageStatus, PluginResult} from '@magongshou/harmony-cordova/Index';
import { PromptAction } from '@kit.ArkUI';
import { common, Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { Index as Page } from '../pages/Index';
export class TestPlugin extends CordovaPlugin {
protected cordovaInterface?: CordovaInterface;
protected cordovaWebView?: CordovaWebView;
//插件初始化函数,初始化函数在页面显示前调用,因此在初始化中不能进行UI的相关操作。
initialize(cordovaInterface: CordovaInterface, cordovaWebView:CordovaWebView):void {
this.cordovaInterface = cordovaInterface;
this.cordovaWebView = cordovaWebView;
return;
}
execute(action: string, args: ESObject[], callbackContext: CallbackContext):boolean {
if(action == "eayHello") {
return this.eayHello(args, callbackContext);
}
//获取config.xml的preferences的配置
if(action == "getPreferences") {
let preferences = this.preferences!.getAll();
let jsonArray:Array< object> = new Array< object>();
preferences.forEach((value,key) => {
let pre:object = new Object();
pre["name"] = key;
pre["value"] = value;
jsonArray.push(pre);
});
callbackContext.successByJson(jsonArray);
}
if(action == "openSystemBrowser") {
return this.openSystemBrowser(args, callbackContext);
}
if(action == "openOtherPage") {
//系统路由功能,webview是根页面,跳转到原生的其他页面,具体使用参考如下连接
//https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-navigation-navigation-V5
let pathStack:NavPathStack = this.cordovaInterface!.getPageStack();
pathStack.pushPathByName("TestPage", "{test:10}");
}
if(action == "otherFunction") {
//获取webview属性变量,用于动态修改webview属性,具体参考如下连接
//https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-webview-V5
this.cordovaWebView!.getWebAttribute()?.height('50%');
//获取webview的控制变量,用于实现具体的功能,示例代码实现在webviw执行js,具体参考如下连接
//https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/ts-basic-components-web-V5
this.cordovaWebView!.getWebviewController().runJavaScript("alert(1);");
//多次执行js侧回调函数,例如在显示执行进度时,需要多次调用
let pluginResult:PluginResult = PluginResult.createByString(MessageStatus.OK, "success");
pluginResult.setKeepCallback(true);
callbackContext.sendPluginResult(pluginResult);
let pluginResult2:PluginResult = PluginResult.createByString(MessageStatus.OK, "success2");
callbackContext.sendPluginResult(pluginResult2);
//多次调用也可以采用如下写法
//callbackContext.successByString("success1", true);//第一次调用
//callbackContext.successByString("success2", true);//第二次调用
//callbackContext.successByString("success3");//最后一次调用
}
if(action == "resetPageInfo") {
return this.resetPageInfo(args, callbackContext);
}
if(action == "otherWebviewController") {
return this.otherWebviewController(args, callbackContext);
}
return true;
}
eayHello(args: ESObject[], callbackContext: CallbackContext):boolean {
//获取UI上下文,用于原生UI交互
let uiContext:UIContext = this.cordovaWebView!.getUIContext();
let promptAction: PromptAction = uiContext?.getPromptAction();
try {
//弹出系统原生窗口
promptAction.showDialog({
title: 'Title',
message: 'eay hello',
buttons: [
{
text: '确定',
color: '#000000'
}
]
}, (err, data) => {
if (err) {
return;
}
//执行成功通知js侧回调函数,通知函数有多个具体查看CallbackContext封装函数
callbackContext.success();
});
} catch (error) {
}
return true;
}
openSystemBrowser(args: ESObject[], callbackContext: CallbackContext):boolean {
if(args.length > 0) {
let url:string = args[0];
//获取UIAbilityContext
let context = getContext(this) as common.UIAbilityContext
let wantInfo: Want = {
action: 'ohos.want.action.viewData',
entities: ['entity.system.browsable'],
uri: url
}
//跳转一个新的ability
context.startAbility(wantInfo).then(() => {
console.log('[跳转至外部浏览器] - success')
}).catch((err: BusinessError) => {
console.error('[跳转至外部浏览器] - Failed to startAbility. Code: ' + err.code + 'message:' + err.message);
})
}
return true;
}
//获取父组件对象(定义为Page)通过父组件调用相关方法或设置属性
resetPageInfo(args: ESObject[], callbackContext: CallbackContext):boolean {
if(this.cordovaInterface) {
if(this.cordovaInterface.getPage()) {
let page:Page = this.cordovaInterface!.getPage() as Page;
page.indexPage = "/www2/index.html"; //加载其他页面
page.DoTest();//调用父组件方法
}
}
callbackContext.success();
return true;
}
/*
* 多Webview模式,一个webview和其他webview通讯,在多页面情况下使用,单页面视图的APP不需要
* 1,查询其他webview的插件
* 2,设置其他webview的属性
* 3,其他webview注入js
* 4, 在其他webview打开原生界面
* 5,在其他webview控制路由
* 6,可以灵活使用,需要技术支持联系开发者
*/
otherWebviewController(args: ESObject[], callbackContext: CallbackContext):boolean {
if(args.length > 0) {
let webId: string = args[0];
let cmd:string = args[1];
//打印所有webId
if(cmd == "printWebId" && this.mapWebIdToWebTag) {
this.mapWebIdToWebTag.forEach((value, key) => {
let pluginResult:PluginResult = PluginResult.createByString(MessageStatus.OK, key!);
pluginResult.setKeepCallback(true);
callbackContext.sendPluginResult(pluginResult);
});
}
//打印webId对应的webview自带的所有自定义ArkTS插件,不包含cordova(c++)内核自带插件
if(cmd == "printPlugins" && this.mapWebIdToWebTag) {
if(this.mapWebIdToWebTag.hasKey(webId)) {
let webTag:string = this.mapWebIdToWebTag.get(webId);
if(this.mapWebIdToCustomPlugins?.hasKey(webTag)) {
this.mapWebIdToCustomPlugins.get(webTag).forEach((value, key) => {
let pluginResult: PluginResult = PluginResult.createByString(MessageStatus.OK, key!);
pluginResult.setKeepCallback(true);
callbackContext.sendPluginResult(pluginResult);
});
}
}
}
//指定webId对应webview,设置属性
if(cmd == "setAttr" && this.mapWebIdToWebView) {
if(this.mapWebIdToWebView.hasKey(webId)) {
this.mapWebIdToWebView.get(webId).getWebAttribute()?.height('20%');
}
callbackContext.success();
}
//指定webId对应webview执行注入新的js代码
if(cmd == "injectJs" && this.mapWebIdToWebView) {
if(this.mapWebIdToWebView.hasKey(webId)) {
this.mapWebIdToWebView.get(webId).getWebviewController().runJavaScript("alert(1);");
}
callbackContext.successByString("OK");
}
//指定webId对应的webview打开原生界面,并使用指定webId的webview的路由
if(cmd == "openPage" && this.mapWebIdToInterface) {
if(this.mapWebIdToInterface.hasKey(webId)) {
let pathStack:NavPathStack = this.mapWebIdToInterface.get(webId).getPageStack();
pathStack.pushPathByName("TestPage", "{test:10}");
}
callbackContext.success();
}
//指定webId对应的webview弹窗
if(cmd == "openAlert" && this.mapWebIdToWebView) {
if(this.mapWebIdToWebView.hasKey(webId)) {
this.mapWebIdToWebView.get(webId).getWebviewController().runJavaScript("alert(1);");
let cordovaWebView:CordovaWebView = this.mapWebIdToWebView.get(webId);
//获取UI上下文,用于原生UI交互
let uiContext:UIContext = cordovaWebView!.getUIContext();
let promptAction: PromptAction = uiContext?.getPromptAction();
try {
//弹出系统原生窗口
promptAction.showDialog({
title: 'Title',
message: 'eay hello',
buttons: [
{
text: '确定',
color: '#000000'
}
]
}, (err, data) => {
if (err) {
return;
}
//执行成功通知js侧回调函数,通知函数有多个具体查看CallbackContext封装函数
callbackContext.success();
});
} catch (error) {
}
return true;
}
callbackContext.success();
}
}
return true;
}
/*
*同层渲染
*JS侧设置原生插件属性
*/
setNativeValue(args: ESObject[], callbackContext: CallbackContext):boolean {
let id: string = args[0];
let value: string = args[1];
if(this.cordovaInterface) {
if(this.cordovaInterface.getPage()) {
let page:Page = this.cordovaInterface!.getPage() as Page;
page.setNativeValue(id, value);
}
}
callbackContext.success();
return true;
}
/*
*同层渲染
*JS侧获取原生组件属性
*/
getNativeValue(args: ESObject[], callbackContext: CallbackContext):boolean {
if(this.cordovaInterface) {
if(this.cordovaInterface.getPage()) {
let page:Page = this.cordovaInterface!.getPage() as Page;
callbackContext.successByString(page.textValue);
}
}
return true;
}
}
(2)插件的配置
ArkTs侧插件写好以后,在entry/src/main/ets/pages/index.ets文件中配置,支持多页面视图,各个视图拥有自己的插件,以及各视图之间通讯:
import { MainPage, pageBackPress, pageHideEvent, pageShowEvent, PluginEntry} from '@magongshou/harmony-cordova/Index';
import { TestPlugin } from "../plugins/TestPlugin"//引入插件
struct Index {
/*
*ArkTs侧的自定义插件键值对:插件名称和实现对象,自定义插件开发,请查看自定义查看开发部分
*如果一个插件传入多个MainPage,务必单独定义对象传入,不可多MainPage使用一个对象,否则会使窗口操作串联
*/
cordovaPlugs:Array< PluginEntry> =
[
{
pluginName: 'TestPlugin', //插件名称
pluginObject:new TestPlugin() //实例化插件对象供cordova调用
}
];
cordovaPlugs2:Array< PluginEntry> =
[
{
pluginName: 'TestPlugin', //插件名称
pluginObject:new TestPlugin() //实例化插件对象供cordova调用
}
];
//省略其他代码
build() {
RelativeContainer() {
//isWebDebug:DevTools工具调试开关,cordovaPlugs:自定义插件列表,启动首页index.html
MainPage({isWebDebug:false,cordovaPlugs:this.cordovaPlugs});
}
.height('50%')
.width('100%')
RelativeContainer() {
//isWebDebug:DevTools工具调试开关,cordovaPlugs:自定义插件列表,指定加载rawfile资源目录下文件
MainPage({isWebDebug:false,indexPage:"/www2/index.html", cordovaPlugs:this.cordovaPlugs2});
}
.height('50%')
.width('100%')
}
}
(3)JS侧插件调用
js侧插件调用完全遵守cordova官方调用规范,主要有如下两种方式:
1. 直接调用,无需做任何配置,代码如下:
cordova.exec(function(result){
console.log(result);
},function(error){
console.log(error);
},"TestPlugin", "openOtherPage", [{name:'chenlh'},{name:'magongshou'}]);
2. 根据官方文档,对插件进行二次封装
根据官方文档,对插件进行二次封装plugins/***/www/***.js,封装完毕后,在config.xml和codova_plugins.js文件中配置,具体配置可以在线查找cordova sdk自定义插件研发相关知识,这里不做详细解释,请参考https://cordova.apache.org/docs/en/12.x/guide/hybrid/plugins/index.html
(4)自定义插件实现原理简述
由于HarmonyOS提供ArkTS和C/C++ API,Cordova sdk是使用C/C++研发,自定义插件是跨语言调用,调用顺序为:js侧->C/C++侧->ArkTs侧,回调是相反顺序,不过ArkTS侧的插件也可以直接调用JS侧。自定义插件的研发根据具体实现的功能,可以选择使用ArkTS开发,也可选择C/C++开发。目前开放的只有使用ArkTs研发自定义插件,只有本开发者才可以使用C/C++研发插件,如果您在研发自定义插件时,如果觉得插件本身使用C/C++研发更为合适,请和本开发者联系以提供技术支持,当然您在使用ArkTS研发自定义插件时遇到什么问题也可以联系本开发者提供技术支持。
自定义C++插件研发
研发自定义C++侧插件,您可以参考cordova/src/main/cpp/CoreHarmony的插件,编写C++侧插件,具体步骤如下:
- 在源码集成的cordova工程中,在源码的CPP目录内新建一个插件目录,保存您的自定义插件;
- 在新目录中新建一个class,该class要继承CordovaPlugin类;
- 实现execute函数,具体函数接口参考CordovaPlugin类注释说明;
- 在您的CPP文件中,添加REGISTER_PLUGIN_CLASS()注册您的插件名称;用于实例化您的插件对象;
- 如果您的插件中需要调用ArkTS侧的代码,需要调用executeArkTs(同步)或者executeArkTsAsync(异步)执行ArkTS侧代码,参数说明参考CordovaPlugin类注释说明;
- 如果您的ArkTS侧需要把执行结果通知到C++侧的插件,在ArkTS侧需要调用onArkTsResult函数通知C++侧,C++侧的插件也要实现onArkTsResult这个函数,并在execute中实现调用;
- 在完成您的插件研发后需要将您的cpp文件添加到CMakeLists.txt中,完成编译;
- C++侧的插件调用方法和ArkTS侧插件调用方法完全一样,请参考ArkTs插件调用方法,调用您的C++插件。
Web加载性能优化
1. 预启动web和预渲染
在应用启动后,在EntryAbility代码中,后台启动web引擎,并在后台渲染页面,进入page页面后,页面秒开,关闭页面后,页面进入后台,不会销毁web,下次打开仍可秒开;需提醒的是,在使用Cordova的页面预渲染时,会初始化cordova插件,有可能会出现在用户没有同意隐私政策前,初始化插件会访问系统资源。
该功能需要对mainPage的组件进行二次封装,自己可以根据需要修改代码,如需技术支持请联系本开发者,提供封装方法和源码如下:
参考连接:https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-web-develop-optimization
(1)在pages中新建ArkTs文件,命名为WebBuilder.ets,复制以下代码:
import { MainPage, MainPageCycle, PluginEntry } from '@magongshou/harmony-cordova';
import { BuilderNode, FrameNode, NodeController } from '@kit.ArkUI';
import { webview } from '@kit.ArkWeb';
import { TestPlugin } from '../plugins/TestPlugin';
//根据需要扩展参数,参数参考MainPage的参数,高级功能中对mainPage参数有说明
class DataParameters{
url?: string;
mainPageCycle?:MainPageCycle;
mainPagePageNodeController?:MainPagePageNodeController;
cordovaPlugs?:Array< PluginEntry>;
}
@Builder
function buildMainPage(data:DataParameters) {
Column() {
MainPage({indexPage:data.url, lifeCycle:data.mainPageCycle, parentPage:data.mainPagePageNodeController,cordovaPlugs:data.cordovaPlugs});
}.width("100%").height("100%")
}
let wrap = wrapBuilder< DataParameters[]>(buildMainPage);
class MainPagePageNodeController extends NodeController {
private rootNode: BuilderNode< DataParameters[]> | null = null;
private root: FrameNode | null = null;
private cordovaPlugs:Array< PluginEntry> = [
{
pluginName: 'TestPlugin', //插件名称
pluginObject:new TestPlugin() //实例化插件对象供cordova调用
}
];
private mainPageCycle:MainPageCycle = new MainPageCycle().setOnAboutToAppear((webviewController: webview.WebviewController,parentPage?:object)=>{
let page = parentPage as MainPagePageNodeController;//page为当前页面对象,相当于当前页面的this指针,使用该对象,必须将this指针传入到mainPage中
console.log("exec onAboutToAppear");
});
constructor() {
super();
}
makeNode(uiContext: UIContext): FrameNode | null {
if (this.rootNode != null) {
const parent = this.rootNode.getFrameNode()?.getParent();
if (parent) {
console.info(JSON.stringify(parent.getInspectorInfo()));
parent.removeChild(this.rootNode.getFrameNode());
this.root = null;
}
this.root = new FrameNode(uiContext);
this.root.appendChild(this.rootNode.getFrameNode());
return this.root;
}
return null;
}
initWeb(url:string, uiContext:UIContext) {
if(this.rootNode != null) {
return;
}
this.rootNode = new BuilderNode(uiContext);
//可以根据不同的页面传入不同的参数,单页面视图不存在这种情况,需要技术支持联系本开发者
if(url === "/www3/index.html") {
this.rootNode.build(wrap, {url:url, mainPageCycle:this.mainPageCycle,mainPagePageNodeController:this, cordovaPlugs:this.cordovaPlugs});
} else {
this.rootNode.build(wrap, {url:url});
}
}
}
let NodeMap:Map< string, MainPagePageNodeController | undefined> = new Map();
export const createNWeb = (url: string, uiContext: UIContext) : MainPagePageNodeController | undefined => {
let baseNode = new MainPagePageNodeController();
baseNode.initWeb(url, uiContext);
NodeMap.set(url, baseNode);
return baseNode;
}
export const getNWeb = (url : string, uiContext:UIContext) : MainPagePageNodeController | undefined => {
if(NodeMap.has(url)) {
return NodeMap.get(url);
} else {
return createNWeb(url, uiContext);
}
}
(2)修改EntryAbility.ets,添加预启动web和预渲染代码:
//省略了其他代码
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Splash', (err) => {
//启动预启动web和预渲染,多页面视图可以预选设置和初始化
createNWeb('/www3/index.html', windowStage.getMainWindowSync().getUIContext());
createNWeb('/www3/index2.html', windowStage.getMainWindowSync().getUIContext());
createNWeb('/www3/index3.html', windowStage.getMainWindowSync().getUIContext());
});
}
(3)修改Index.ets启动cordova封装的mainPage页面,此时秒开,效率和传统打开mainPage相比大大提高:
build() {
Column() {
RelativeContainer() {
NodeContainer(getNWeb('/www3/index.html', this.getUIContext()))
.height('100%')
.width('100%')
}
.height('100%')
.width('100%')
}
2. 资源拦截替换的JavaScript生成字节码缓存(Code Cache)
使用Cordova框架,根据Apache Cordova的标准,所有页面和JS文件都在本地,鸿蒙Cordova内部已经使用了拦截和替换功能,如果您加载的是在线资源或者JS文件,并且强制使用了Cordova协议栈(通过config.xml配置或者SetCordovaProtocolUrl函数设置),Cordova SDK也进行了资源缓存,如果您加载的是在线页面,使用webview的协议栈,可以结合MainPage提供的生命周期函数onInterceptWebRequest进行拦截,对于在线的js文件,也可以直接打包到本地的沙箱目录下,通过Cordova提供的SetResourceReplace函数进行拦截替换,以提供加载页面速度。示例代码如下:
//省略有其他代码,以下是js预编译示例代码
configs: Array< Config> = [
{
url: 'https://www.tongecn.com/example.js',
localPath: 'example.js',//文件在rawfile目录下
options: {
responseHeaders: [
{ headerKey: 'E-Tag', headerValue: 'xxx' },
{ headerKey: 'Last-Modified', headerValue: 'Web, 21 Mar 2024 10:38:41 GMT' }
]
}
}
]
mainPageCycle = new MainPageCycle().setOnControllerAttached((webviewController: webview.WebviewController,parentPage?:object)=>{
console.log("exec onControllerAttached");
for (const config of this.configs) {
let content = await this.getUIContext().getHostContext()?.resourceManager.getRawFileContentSync(config.localPath);
try {
this.controller.precompileJavaScript(config.url, content, config.options)
.then((errCode: number) => {
console.log('precompile successfully!' );
}).catch((errCode: number) => {
console.error('precompile failed.' + errCode);
})
} catch (err) {
console.error('precompile failed!.' + err.code + err.message);
}
}
})
//省略其他代码,以下是cordova拦截替换
onInterceptWebRequest(request: WebResourceRequest, webTag:string):ESObject {
//cordova webview内核处理替换
if(url == "https://www.tongecn.com/v1.1.1/temp/test3.js") {
//替换本地沙箱路径
SetResourceReplace(webTag, url, "https://localhost/data/storage/el2/base/files/test.js");
//替换本地rawfile文件
//SetResourceReplace(webTag, url, "https://www.example.com/test.js");
}
return null;
}
//省略有其他代码
MainPage({isWebDebug:true, indexPage:"https://www.tongecn.com", lifeCycle:data.mainPageCycle, parentPage:this,onInterceptWebRequest:this.onInterceptWebRequest});
常见问题
1. 鸿蒙返回键不起作用
鸿蒙返回键不起作用,就是手势事件,从左往右快速滑动,app不返回上一页面,或者到了顶层页面不退出应用。
不同的框架有不同的处理方式,如果不管使用的是什么框架,只在cordova层处理的,需要监听返回键事件,代码如下:
document.addEventListener("deviceready", onDeviceReady, false);
function onDeviceReady() {
document.addEventListener("backbutton", onBackKeyDown, false);
}
function onBackKeyDown() {
//自己处理返回
//退出app: navigator.app.exitApp();
}
如果采用ionic angularjs框架,可以采用如下代码:
function showConfirm() {
//处理退出应用的逻辑
}
$ionicPlatform.registerBackButtonAction(function (e) {
// Is there a page to go back to?
if ($location.path() == '/tab/message') { //到了顶层页面,/tab/message是顶层页面的路由,这里只是举个例子,实际情况根据您的项目设置
showConfirm();
return false
} else if ($ionicHistory.backView()) {
// Go back in history
$ionicHistory.goBack(); //自己处理返回
} else {
// This is the last page: Show confirmation popup
showConfirm();
return false;
}
e.preventDefault();
return false;
}, 101);
说明:无论采用什么框架都可以在cordova层通过监听backbutton返回事件自己处理。
如果加载的页面不包含cordova.js需要传入控制webview路由MainPageOnBackPress对象控制返回
2. 如何访问沙箱资源文件
采用cdvfile://访问沙箱文件,以downloadImage.png为例:
cdvfile:///data/storage/el2/base/files/chuzhitong/downloadedImage.png
如果是file://作为MainPage的入口页,也可以使用file://协议访问本地文件,沙箱资源文件可以是图片(png,jpg,svg等)、js、html等,请参考最后的file://协议说明
3. HTTP协议的cookie说明
如果您使用http协议非https协议,请参考如下cookie说明:
(1)同源请求:
例如您是直接在MainPage传入网址例如传入http://www.tongecn.com,cordova会自动处理cookie,无需手动处理
(2)跨域请求:
例如您加载的文件在沙箱路径或者rawfile目录下的文件,在html文件中使用的http发送的GET/POST请求,此时需要再在config.xml里面配置http请求的域名,以便以cordova为http处理cookie,配置如下:
在config.xml 静态配置,静态配置所有请求,包括img、css、js等有cordova处理:
<!--在config.xml 静态配置,静态配置所有请求,包括img、css、js等有cordova处理-->
<cordova-protocol-force value="***.***.com" />
在ArkTs侧运行态动态设置http的cookie,http的请GET/POST自动携带cookie,cordova不出来静态资源,静态资源有webview处理:
//在ArkTs侧运行态动态设置http的cookie,http的请GET/POST自动携带cookie,cordova不出来静态资源,静态资源有webview处理
aboutToAppear() {
SetCordovaProtocolUrl("***.****.com");
}
(3)HTTPS协议:
您发送的请求是https协议,非http协议,cordova会自动处理cookie,无需手动处理
(4)手动设置cookie:
如果您要在ArkTs侧运行态手动设置cookie,请参考不常用的高级功能部分
4. 虚拟域名www.example.com、自定义域名、localhost、file协议和cdvfile协议的详细说明
加载rawfile目录下的页面时,通过DevTools工具测试时或者在日志log中会看到https://www.example.com的域名,可能会感到疑虑或者惊慌,接下来详细介绍一下,为什么使用此域名:
- 鸿蒙无法使用file协议直接加载rawfile目录的文件,因此使用www.example.com虚拟域名代替file协议,因此您看到
https://www.example.com就理解为file://即可 - 在config.xml里面添加
<preference name="Hostname" value="app.com" />使用自定义域名加载本地文件 - 使用
http(s)://localhost可以加载沙箱目录文件 - 使用
cdvfile://协议加载沙箱目录文件
说明:如果您的h5程序中有使用file://协议,在MainPage中就必须使用file://协议进入首页,否则file协议无法加载本地文件,cordova完全支持file://协议加载文件,无论是从资源文件夹加载还是从沙箱路径加载鸿蒙cordova完全支持
5. 屏蔽跟随系统字体大小
- 在app.json5中增加configuration选项以屏蔽跟随系统字体大小,具体配置方法参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/app-configuration-file
- 在EntryAbility的onWindowStageCreate函数中增加
windowStage.setDefaultDensityEnabled(true);屏蔽跟随显示大小缩放,参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-window#setdefaultdensityenabled12
6. 跨域错误
cordova已经解决了所有的跨域访问,同时会自动携带cookie,并兼容所有的自定义http头,无需做任何配置,但是前端withCredentials设置为true时,需要相应的配置解决跨域。
在前端发送POST、GET请求,withCredentials为true时,同时您的服务器依赖于自定义http头例如X-Auth-Token Test-Type,mainPage要传入相应的参数如下:
mainPage({customHttpHeaders:"X-Auth-Token,Test-Type",isAllowCredentials:true})
如果没有设置会报跨域错误:
Access to XMLHttpRequest at '*****' from origin '****' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
**解决方法:**在mainPage增加参数isAllowCredentials:true
mainPage({isAllowCredentials:true})
Access to XMLHttpRequest at '*****' from origin '*****' has been blocked by CORS policy: Request header field ***** is not allowed by Access-Control-Allow-Headers in preflight response
**解决方法:**在mainPage增加自定义头,例如自定义http头X-Auth-Token Test-Type
mainPage({customHttpHeaders:"X-Auth-Token,Test-Type", isAllowCredentials:true})
7. iframe跨域设置cookie
如果您使用的iframe加载了第三方页面,第三方页面直接使用js设置cookie,并不是通过http头Set-Cookie设置cookie的,js设置cookie,一定要加上SameSite=None;Secure,否则iframe会出现页面无法显示问题,因为请求http头不会自动携带cookie,设置cookie示例如下:
document.cookie = 'token=467d1510-xxxx-xxxx-xxxx-73852620effa1; path=/; SameSite=None; Secure';
如果第三方页面无法更改,请使用内置浏览器打开页面,或者直接使用a标签打开页面,a标签在鸿蒙的cordova中会自动触发内置浏览器,android和ios不具备该功能。示例代码如下:
//内置浏览器打开,可以配置相关参数,需集成内置浏览器插件
window.open("https://www.*****.com/index.html", "title=测试标题");
<!--a标签打开,会自动触发内置浏览器-->
<a href="https://www.*****.com/index.html" target="_blank">打开链接</a>
8. Cordova内部缓存时长设置
默认请求下使用Cordova的协议栈访问网络,静态资源缓存一天,即24 * 60 * 60秒钟,如果您想自己配置缓存时长在config.xml里面添加如下配置:
<!--cordova静态资源缓存时长,单位秒钟-->
<cordova-cache-duration value="60" />
3185

被折叠的 条评论
为什么被折叠?



