cordova-openharmony框架开发鸿蒙指南

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

  1. 自定义webview打开路径页面路径,支持rawfile资源文件、沙箱路径、在线网页
  2. 自定义用户scheme,用于cordova内部拦截scheme的请求
  3. 拦截自定义的scheme,在webview端并处理
  4. 在原生层,直接设置cordovaWebview属性和注入新的js代码,不通过插件实现,用于原生组件和webview组件显示在同一视图界面
  5. 多webview界面,即多页面视图,自定义webId,使用自定义插件各webview之间通讯,可用于平板等大屏幕研发需求
  6. 多webview之间的通讯,参考自定义插件研发示例代码说明
  7. 动态创建组件,在webview和NodeController相结合实现动态创建和显示组件时,切记一定要设定webId参数,避免重复创建webview
  8. 父组件感知MainPage子组件的所有生命周期,在不同的周期执行相应的操作
  9. 在同一个webview中加载在线网页,也可以本地页面和在线网页混合开发
  10. 加载不包含cordova.js页面,父组件控制webview的返回键,或者自己控制路由
  11. MainPage的路由开关控制,便于MainPage嵌套使用,路由子页面内再嵌套使用MainPage
  12. 自定义cookie,传入cookie键值对
  13. 自定义webview字体大小缩放百分比,支持适老化,屏蔽跟随系统字体大小变化
  14. 同层渲染,以及同层渲染组件和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++侧插件,具体步骤如下:

  1. 在源码集成的cordova工程中,在源码的CPP目录内新建一个插件目录,保存您的自定义插件;
  2. 在新目录中新建一个class,该class要继承CordovaPlugin类;
  3. 实现execute函数,具体函数接口参考CordovaPlugin类注释说明;
  4. 在您的CPP文件中,添加REGISTER_PLUGIN_CLASS()注册您的插件名称;用于实例化您的插件对象;
  5. 如果您的插件中需要调用ArkTS侧的代码,需要调用executeArkTs(同步)或者executeArkTsAsync(异步)执行ArkTS侧代码,参数说明参考CordovaPlugin类注释说明;
  6. 如果您的ArkTS侧需要把执行结果通知到C++侧的插件,在ArkTS侧需要调用onArkTsResult函数通知C++侧,C++侧的插件也要实现onArkTsResult这个函数,并在execute中实现调用;
  7. 在完成您的插件研发后需要将您的cpp文件添加到CMakeLists.txt中,完成编译;
  8. 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函数进行拦截替换,以提供加载页面速度。示例代码如下:

参考连接:https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-web-develop-optimization#section172031338172719

//省略有其他代码,以下是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. 屏蔽跟随系统字体大小

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" />
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值