C

CPF-Cordova

CPF-Cordova属于开源鸿蒙跨平台框架 Cordova SIG,用于孵化及运营Cordova相关的开源项目

公告

README

@cordova-ohos/ohos框架说明

@cordova-ohos/ohos是cordova的OHOS化版本,所有接口兼容Cordova的Android和iOS版本

本文档说明

本文档仅说明@cordova-ohos/ohos框架部分的使用手册、开发说明,集成步骤等。

依赖说明

本工程依赖openssl,官方网站https://openssl.org,编译之前先集成openssl,只有成功集成后,才可编译,集成方法:https://gitcode.com/chenlihuiabc/openharmony-cordova-openssl

开发说明

@cordova-ohos/ohos是cordova的OHOS化版本,并支持ArkTS侧和C/C++侧自定义插件研发,框架采用C/C++研发,底层使用自研Socket TCP/IP通讯,封装了HTTP/HTTPS协议通讯解决各种跨域访问问题,无需配置web服务端,同时结合webview的通讯协议栈,大大提高应用层网络请求效率。

附加说明

@cordova-ohos/ohos使用多页面视图研发,同时兼容Android和iOS原有的单页面视图,原有项目可以轻松移植;另外在复杂项目中,可以使用@cordova-ohos/ohos的多页面视图功能,创建多个webview协同工作。

开发背景

cordova是Apache基金会的开源项目,官方网站:https://cordova.apache.org,是移动端跨平台框架,地位不可撼动,大量厂商直接或间接采用此框架开发APP;但是目前不支持OHOS Next版本,开发者将原Android和iOS项目移植到OHOS Next版,无法适配,为此研发了@cordova-ohos/ohos,遵守cordova官方标准,原有项目无需投入任何研发轻松移植到OHOS系统;新开发的项目,一次研发就适用于Android、iOS和OHOS三大平台,也节省了大量的时间和人力成本。


框架自带插件

CoreHarmony插件

名称:OHOS内核插件对标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://www.openharmony.cn", {openexternal:true}]
);

cordova-plugin-inappbrowser

名称:内置浏览器

框架自带内置浏览器,主要用于在页面中打开新窗口时,自动打开内置浏览器加载页面,举例如下,另外框架部分不包括该插件的JS部分,如果使用JS侧动态控制,请安装cordova-plugin-inappbrowser插件的全量功能。

使用示例:

<!--自动激活内置浏览器和路由功能,打开新窗口加载页面 -->
<!--访问网络,请设置网络权限-->
<a href="https://www.openharmony.cn" 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-ohos/ohos框架自带闪屏功能,在web页面没有加载全部完成时先显示闪屏,页面加载完毕后闪屏消失,框架部分不包括该插件的JS侧部分功能,如果需要JS侧控制部分,请安装cordova-plugin-splashscreen的全量功能,多页面视图只有第一个视图会出现闪屏,web视图推荐使用预加载和预渲染功能实现页面秒显,无需闪屏。

OHOS闪屏资源图片说明:

资源图片名称竖屏: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-ohos/ohos源码集成使用说明

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目录下面的所有文件到DevEco工程entry/src/main/resources/rawfile目录下,原Android工程的assets目录必须包含www目录,www目录包含index.html(必须)、cordova.js(必须)、cordova_plugins.js(必须)、plugins目录(必须)、css目录、js目录等,如果要指定加载页面,不使用默认页面,请查看高级功能部分说明。复制成功后,仍需要安装Android包含的OHOS版插件。

iOS项目移植:

第一步:复制Xcode的iOS工程目录下的Staging目录下的www文件夹复制到DevEco工程entry/src/main/resources/rawfile目录下。

第二步:Xcode工程的config.xml文件在Staging目录下,Xcode工程的该文件不能直接被@cordova-ohos/ohos使用,需要进行转换,该文件主要记录的是插件的名称和初始化的类,因为OHOS版是根据android的config.xml进行插件初始化的,因此需要将Xcode工程config.xml转为Android的config.xml,请将Xcode工程使用node加入Android平台,系统会自动生成android版的config.xml。然后将文件复制到OHOS版工程的entry/src/main/resources/rawfile下。复制成功后,仍需要安装iOS包含的OHOS版插件。

新建项目:

如果您没有Android和iOS项目,需要使用cordova的命令化工具,创建Android项目,创建成功后,再按照Android项目移植方法操作即可。

5. 修改Index.ets文件

打开DevEco工程文件entry/src/main/etx/pages/Index.ets文件,修改代码如下(可以直接全部拷贝和复制到Index.ets文件中):

import { MainPage, pageBackPress, pageHideEvent, pageShowEvent, PluginEntry} from '@cordova-ohos/ohos/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文件

打开DevEco工程文件/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. 完成

做以上代码修改后,OHOS的移植已经完毕,可以使用模拟器或者真机进行编译和测试了。


高级用法,区别于Android和iOS

1. MainPage传入indexPage参数设置自定义启动路径,支持rawfile、resfile和沙箱路径

    /*
    *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会重新加载页面
    */
    //省略其他代码
    MainPage({indexPage:"/www/index.html"}); 
    //省略其他代码

2. 注册自定义用户scheme,用于cordova内部拦截scheme的请求

import { RegisterCustomSchemes } from '@cordova-ohos/ohos';
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
   hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
   RegisterCustomSchemes("cmp"); //注册自定义scheme
   webview.WebviewController.initializeWebEngine();//webview引擎初始化
}
  //customSchemes:自定义scheme,多个scheme用","分隔
  //省略其他代码
  MainPage({customSchemes:"cmp,xmp,xxx"});
  //省略其他代码

3. 拦截自定义的scheme,在webview端拦截并处理,也可以拦截http(s)请求处理

    /*
    *拦截请求函数,根据需要拦截相应请求,一般用于自定义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");
      }

      //自己处理资源返回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;
    }
    
    //省略其他代码
    /*
    *onInterceptWebRequest 返回null放行,返回具体的WebResourceResponse
    *    参考https://docs.openharmony.cn/pages/v5.1.0/zh-cn/application-dev/web/web-cross-origin.md 说明
    */
    MainPage({onInterceptWebRequest:this.onInterceptWebRequest});
    //省略其他代码

4. 在原生层,动态设置webview属性

    /*
    *在原生层页面加载后,网页面中注入新的js,也可以在mainPage的生命周期页面加载完毕后注入js
    */
    onSetCordovaWebAttribute(cordovaWebView:CordovaWebView) {
        if(cordovaWebView) {
        //获取webview属性变量,用于动态修改webview属性,具体参考如下连接,页面加载完成后触发
        //OHOS并不支持WebAttribute组件属性的动态设置,但是可以设置部分属性,不支持的属性会抛出"Method not implemented."、"is not callable"等异常信息
        //https://docs.openharmony.cn/pages/v5.1.0/zh-cn/application-dev/reference/apis-arkweb/js-apis-webview.md
        //https://docs.openharmony.cn/pages/v5.1.0/zh-cn/application-dev/ui/arkts-user-defined-extension-attributeModifier.md
        cordovaWebView!.getWebAttribute()?.height('50%');
        //获取webview的控制变量,用于实现具体的功能,示例代码实现在webviw执行js或者注入新的js,具体参考如下连接
        //https://docs.openharmony.cn/pages/v5.1.0/zh-cn/application-dev/reference/apis-arkweb/ts-basic-components-web.md
        cordovaWebView!.getWebviewController().runJavaScript("alert(1);");
    }
    //省略其他代码
    MainPage({onSetCordovaWebAttribute:this.onSetCordovaWebAttribute});
    //省略其他代码

5. 多webview界面,即多页面视图,自定义webId,使用自定义插件各webview之间通讯,可用于平板等大屏幕研发需求

    //省略其他代码
    //webId:自定义webId,用于多webview,各webview之间通讯,webId确保唯一,参考自定义插件研发示例代码
    MainPage({ webId:"123456"})
    //省略其他代码

6. 动态创建组件,在webview和NodeController相结合实现动态创建和显示组件时,切记一定要设定webId参数,避免重复创建webview

//动态创建MainPage的示例代码,主要用于原生界面和webview界面显示在同一个视图里面的混合式研发
//如果要传入其他参数,参考此文档详细了解
//https://docs.openharmony.cn/pages/v6.0/zh-cn/application-dev/reference/apis-arkui/js-apis-arkui-frameNode.md
@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();
  }
}
  //省略其他代码
  private textNodeController = new TextNodeController();
  
  //省略其他代码
  RelativeContainer() {
    if (this.isShow) {
      NodeContainer(this.textNodeController)
        .width('100%')
        .height("100%")
        .backgroundColor('#FFF0F0F0')
    }
  }
  .height('30%')
  .width('100%')

  Button("显示和隐藏web").onClick(()=>{
    this.isShow = false;
  })

7,W3C WEB授权webview权限,例如webview调起摄像头和麦克风

    //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}`);
          })
        }
      }
    }

    /*
    *onPermissionRequest:web组件W3C标准拉起授权的回调函数
    *    参考连接:https://docs.openharmony.cn/pages/v5.1.0/zh-cn/application-dev/reference/apis-arkweb/ts-basic-components-web.md  
    */
MainPage({parentPage:this, onPermissionRequest:this.onPermissionRequest,});

8. 父组件感知MainPage子组件的所有生命周期,在不同的周期执行相应的操作

  //MainPage的生命周期的各回调函数,根据业务需要设置单个或多个生命周期回调函数添加业务功能
  //生命周期的说明参考:https://docs.openharmony.cn/pages/v6.0/zh-cn/application-dev/web/web-event-sequence.md
  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");
    });
  }

    //省略其他代码
    /*
    *lifeCycle:传入生命周期对象,让父组件感知MainPage的生命周期,进行相应业务处理
    *parentPage:传入this,就是webview父组件对象,也就是当前组件的对象,可以在插件里面调用
    */
    MainPage({lifeCycle:this.mainPageCycle, parentPage:this})
    //省略其他代码

9. 在同一个Page中加载多个webview,实现本地、在线页面混合研发

   build() {
      Column() {
        RelativeContainer() {
          MainPage({indexPage:"/www/index.html"});
        }
        .height('30%')
        .width('100%')
        RelativeContainer() {
          MainPage({indexPage:"https://www.openharmony.cn"});
        }
        .height('30%')
        .width('100%')
      }
    }

10. 加载不包含cordova.js页面,父组件控制webview的返回键,或者自己控制路由

  /*
   *控制mainPage的页面返回,需将此对象传入MainPage
   *如果加载的页面不包含cordova.js,使用pageBackPress无法通知cordova返回,必须使用此对象控制页面返回
   *也可以通过此对象控制webview的路由
   */
  mainPageOnBackPress:MainPageOnBackPress = new MainPageOnBackPress();

  onBackPress() {
    pageBackPress();
    /*
     *如果加载的页面没有包含cordova.js,例如加载https://cn.bing.com,
     * 返回值
     *  true:已经到了页面等层
     *  false:返回了上一页
     */
   //return this.mainPageOnBackPress.backPress();
    return true;
  }
  //backPress:传入控制webview路由的对象,加载的页面不包含cordova.js时控制webview路由,需要传
  mainPage({ backPress: this.mainPageOnBackPress })

11. MainPage的路由开关控制,便于MainPage嵌套使用,路由子原生页面内再嵌套使用MainPage

    /*
    *isNavPath:true使用MainPage组件内的路由,默认是true,false:不使用MainPage内的路由,
    *     特别是MainPage嵌套使用时,父组件要打开路由,子组件关闭路由,否则会路由冲突
    */
    MainPage({isNavPath:false});

12. 自定义cookie,传入cookie键值对

  //手动添加cookie,在发送POST或者Get请求时携带cookie,https的session cookie无需手动设置,cordova会自动处理
  //http的session cookie参考最后的https的cookie说明
  this.cookies.set("https://mem.tongecn.com", ["key1=value1; path=/; Domain=.tongecn.com", "key2=value2"]);

  /*
  *cookies:如果ArkTs侧有自定义的cookie,可以通过此参数传入
  *     一般情况下cookie都是cordova自动处理的,无需ArkTS侧手动设置,不过ArkTS侧通过此参数可以手动设置cookie
  *     如果您的请求是采用的http协议非https,分为跨域请求和非跨域请求,请查看最后的常见问题说明  
  */
  MainPage({cookies:this.cookies});

13. 自定义webview字体大小缩放百分比,支持适老化,屏蔽跟随系统字体大小变化

    /*
    *textZoomRatio:webview字体放大缩小百分比,默认是100保持默认
    *     设置webview不跟随系统字体大小、并且屏蔽跟随显示大小缩放后
    *     可以通过此参数统一设置webview字体大小变化百分比,避免页面错乱
    *     也可以通过Device插件增加的字体大小百分比接口函数,在js侧设置,参考Device插件
    *     参考常见问题屏蔽跟随系统字体大小和屏蔽跟随显示大小缩放
    */
  MainPage({textZoomRatio:110});

14. 同层渲染,以及同层渲染组件和Cordova插件结合的使用的方法

//同层渲染示例代码,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 {
    //省略其他代码
    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";
    /*
    *同层渲染生命周期回调函数
    */
    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({page:page, 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);
      }
    }

    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文本改变后回调该函数
    *可以通过自定义插件获取改变后的值
    */
    onTextChangeCallBack(page:Index, value:string) {
      page.textValue = value;
    }
    
    getTextValue():string {
      return this.textValue;
    }
    
    /*
    *设置同层渲染TextInput的显示文本
    *可以通过自定义插件设置TextInput的显示文本
    */
    setNativeValue(id:string, value:string){
      this.textValue = value;
      let nodeController = this.nodeControllerMap.get(id);
      nodeController?.updateNode({page:this, textOne: 'update', width: this.widthMap.get(id), height: this.heightMap.get(id), text:this.textValue, onTextChange:this.onTextChangeCallBack} as ESObject)
    }

  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)
      /*
       *nativeEmbedHtmlTag:注册同层渲染标签
       *     默认是:<embed>的标签 ,如果要注册object,请传入object,同层渲染只支持这两个标签,可以直接保持默认
       *nativeEmbedHtmlType:注册同层选择标签类型
       *     默认是:native类型,如要要传入其他类型,请随意取名字
       *onNativeEmbedLifecycleChange:同层渲染元素生命周期函数
       *onNativeEmbedGestureEvent:同层渲染手势回调函数
       *    同层渲染参考连接:https://docs.openharmony.cn/pages/v5.1.0/zh-cn/application-dev/web/web-same-layer.md
       */
      MainPage({
        parentPage: this,
        onNativeEmbedLifecycleChange: this.onNativeEmbedLifecycleChange,
        onNativeEmbedGestureEvent: this.onNativeEmbedGestureEvent
      });
    }
}
    

15, 键盘避让模式

//webKeyboardAvoidMode:避让键盘模式,默认:WebKeyboardAvoidMode.RESIZE_VISUAL
mainPage({webKeyboardAvoidMode:WebKeyboardAvoidMode.RESIZE_VISUAL})

16, 自定义http头

    /*
    *customHttpHeaders:自定义http头
    *     前端withCredentials为true时添加自定义http头,没有自定义http头不用添加
    *     参考常见问题的跨域说明   
    *isAllowCredentials:默认是false
    *     前端请求设置withCredentials为true,要传入参数isAllowCredentials:true
    *     参考常见问题的跨域说明
    */
    mainPage({customHttpHeaders:"X-AUTH", isAllowCredentials:true})

17, 传入当前页面对象parentPage,在mainPage的生命周期函数中可以引用当前页面的变量

//parentPage:传入this,就是webview父组件对象,也就是当前组件的对象,可以在插件里面调用
mainPage({parentPage:this})

18,双向认证证书配置,Https双向认证即服务端也要验证客户端,客户端需提供证书,证书配置说明如下:

(1) 通过config.xml配置p12证书

<!--cordova双向认证对应客户端证书,配置路径在rawfile目录下的文件,p12证书不支持别名-->
<client-auth-cert url="map.tongecn.cn:8090" p12="server.p12" password="123456897dfs"/>

(2) 通过SetClientAuthP12Cert设置p12的base64字符串证书

mainPageCycle:MainPageCycle = new MainPageCycle().setOnAboutToAppear((webviewController: webview.WebviewController,parentPage?:object)=>{
  let page = parentPage as Index;
  //设置客户端证书
  SetClientAuthP12Cert("map.tongecn.cn:8090", "123456897dfs",
    "MIIKDwIBAzCCCcUGCSqGSIb3DQEHAaCCCb..."
  );
})

//省略其他代码
mainPage({lifeCycle:this.mainPageCycle, parentPage:this})

(3) 证书制作说明

证书支持自签名证书,使用现代加密算法,推荐openssl 3.0以上制作自签名证书,部分执行命令如下:

# 从p12证书中提取key
openssl pkcs12 -in client.p12 -nocerts -nodes -out private.key -legacy -password pass:123456897dfs
# 从p12证书中提取证书
openssl pkcs12 -in client.p12 -clcerts -nokeys -out certificate.crt -legacy -password pass:123456897dfs
# 根据证书和key生成p12证书,如果存在证书链,可以把证书链和证书放在一个文件里面,证书链在上面,证书在下面
openssl pkcs12 -export -out client_new.p12 -inkey private.key -in certificate.crt -password pass:123456897dfs
# 根据P12证书生成base64字符串
base64 -w 0 client_new.p12 >> server.txt

(4)常见问题

error:0308010:digital envelope routines::unsupported,证书使用的过时的加密算法,需要重新制作证书

自定义ArKTS插件研发

自定义插件研发,需具备cordova插件研发和OHOS原生研发能力,自定义插件接口遵守cordova sdk官方规范,以自定义插件TestPlugin、为例:

(1)新建ArkTs文件

新建ArkTs文件,取名字为TestPlugin,示例代码如下,具体功能参考示例代码注释说明。

import { CordovaPlugin,CordovaInterface, CallbackContext} from '@cordova-ohos/ohos/Index';
import { CordovaWebView, MessageStatus, PluginResult} from '@cordova-ohos/ohos/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://docs.openharmony.cn/pages/v6.0/zh-cn/application-dev/reference/apis-arkui/arkui-ts/ts-basic-components-navigation.md
      let pathStack:NavPathStack = this.cordovaInterface!.getPageStack();
      pathStack.pushPathByName("TestPage", "{test:10}");
    }

    if(action == "otherFunction") {
      //获取webview属性变量,用于动态修改webview属性,具体参考如下连接
      //https://docs.openharmony.cn/pages/v5.1.0/zh-cn/application-dev/reference/apis-arkweb/js-apis-webview.md
      this.cordovaWebView!.getWebAttribute()?.height('50%');
      //获取webview的控制变量,用于实现具体的功能,示例代码实现在webviw执行js,具体参考如下连接
      //https://docs.openharmony.cn/pages/v5.1.0/zh-cn/application-dev/reference/apis-arkweb/ts-basic-components-web.md
      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 '@cordova-ohos/ohos/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和cordova_plugins.js文件中配置,具体配置可以在线查找cordova sdk自定义插件研发相关知识,这里不做详细解释,请参考https://cordova.apache.org/docs/en/12.x/guide/hybrid/plugins/index.html

(4)自定义插件实现原理简述

由于OHOS提供ArkTS和C/C++ API,Cordova sdk是使用C/C++研发,自定义插件是跨语言调用,调用顺序为:js侧->C/C++侧->ArkTs侧,回调是相反顺序,不过ArkTS侧的插件也可以直接调用JS侧。自定义插件的研发根据具体实现的功能,可以选择使用ArkTS开发,也可选择C/C++开发。


自定义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://docs.openharmony.cn/pages/v5.1.0/zh-cn/application-dev/performance/performance-overview.md

(1)在pages中新建ArkTs文件,命名为WebBuilder.ets,复制以下代码:

import { MainPage, MainPageCycle, PluginEntry } from '@cordova-ohos/ohos';
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文件都在本地,OHOS Cordova内部已经使用了拦截和替换功能,如果您加载的是在线资源或者JS文件,并且强制使用了Cordova协议栈(通过config.xml配置或者SetCordovaProtocolUrl函数设置),Cordova SDK也进行了资源缓存,如果您加载的是在线页面,使用webview的协议栈,可以结合MainPage提供的生命周期函数onInterceptWebRequest进行拦截,对于在线的js文件,也可以直接打包到本地的沙箱目录下,通过Cordova提供的SetResourceReplace函数进行拦截替换,以提供加载页面速度。示例代码如下:

参考连接:https://docs.openharmony.cn/pages/v5.1.0/zh-cn/application-dev/web/web-render-mode.md

//省略有其他代码,以下是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.mainPageCycleparentPage: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://协议加载文件,无论是从资源文件夹加载还是从沙箱路径加载OHOS 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标签在OHOS的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

cordova-openharmony 框架和所有插件遵守Apache Cordova官方接口规范,原android和Ios项目在几分钟内就可以完成鸿蒙项目的生成,编译生成原生鸿蒙APP,无需做任何研发,节省大量时间和人力成本。

3

鸿蒙cordova命令化工具

  • cordova-openharmony框架仓库

    C++
    9
    4
    Apache-2.0
    更新于 4月28日
    Star
  • 鸿蒙cordova文件管理插件

    JavaScript
    2
    1
    Apache-2.0
    更新于 4月27日
    Star
  • 鸿蒙cordova支付宝支付插件

    C++
    1
    1
    Apache-2.0
    更新于 4月27日
    Star
  • 鸿蒙cordova的退出app插件

    C++
    1
    1
    Apache-2.0
    更新于 4月27日
    Star
  • 暂无简介

    1
    1
    更新于 3 天前
    Star
  • 鸿蒙cordova媒体获取插件

    JavaScript
    1
    0
    Apache-2.0
    更新于 4月27日
    Star
  • 鸿蒙cordova将canvas保存到相册插件

    ArkTS
    1
    0
    Apache-2.0
    更新于 4月27日
    Star
  • 鸿蒙cordova的内置浏览器插件

    C++
    1
    1
    Apache-2.0
    更新于 4月27日
    Star
  • 鸿蒙cordova的解压缩插件

    C
    1
    1
    Apache-2.0
    更新于 4月27日
    Star
  • cordova-openharmony 框架和所有插件遵守Apache Cordova官方接口规范,原android和Ios项目在几分钟内就可以完成鸿蒙项目的生成,编译生成原生鸿蒙APP,无需做任何研发,节省大量时间和人力成本。

    C
    10
    6
    Apache-2.0
    更新于 3月20日
    Star
查看全部项目 >
公告
成就
107
Star
32
Fork
512.99 K
Download
常用语言
C++
JavaScript
C
Shell