@cordova-ohos/ohos
v14.0.2
Published
Cordova ohos
Readme
@cordova-ohos/ohos 框架说明
@cordova-ohos/ohos 是 cordova 的 OHOS 化版本,所有接口兼容 Cordova 的 Android 和 iOS 版本
本项目基于 [email protected] 开发,并适配了 OHOS 平台特性,本文档重点阐述其在 OpenHarmony(OHOS)系统中的具体应用。
- @cordova-ohos/ohos 框架说明
- 本文档说明
- 依赖说明
- 开发说明
- 附加说明
- 开发背景
- 兼容性
- 框架自带插件
- @cordova-ohos/ohos 源码集成使用说明
- 命令行工具创建项目
- 高级用法,区别于 Android 和 iOS
- 1. MainPage 传入 indexPage 参数设置自定义启动路径,支持 rawfile、resfile 和沙箱路径
- 2. 注册自定义用户 scheme,用于 cordova 内部拦截 scheme 的请求
- 3. 拦截自定义的 scheme,在 webview 端拦截并处理,也可以拦截 http(s) 请求处理
- 4. 在原生层,动态设置 webview 属性
- 5. 多 webview 界面,即多页面视图,自定义 webId,使用自定义插件各 webview 之间通讯,可用于平板等大屏幕研发需求
- 6. 动态创建组件,在 webview 和 NodeController 相结合实现动态创建和显示组件时,切记一定要设定 webId 参数,避免重复创建 webview
- 7. W3C WEB 授权 webview 权限,例如 webview 调起摄像头和麦克风
- 8. 父组件感知 MainPage 子组件的所有生命周期,在不同的周期执行相应的操作
- 9. 在同一个 Page 中加载多个 webview,实现本地、在线页面混合研发
- 10. 加载不包含 cordova.js 页面,父组件控制 webview 的返回键,或者自己控制路由
- 11. MainPage 的路由开关控制,便于 MainPage 嵌套使用,路由子原生页面内再嵌套使用 MainPage
- 12. 自定义 cookie,传入 cookie 键值对
- 13. 自定义 webview 字体大小缩放百分比,支持适老化,屏蔽跟随系统字体大小变化
- 14. 同层渲染,以及同层渲染组件和 Cordova 插件结合的使用的方法
- 15. 键盘避让模式
- 16. 自定义 http 头
- 17. 传入当前页面对象 parentPage,在 mainPage 的生命周期函数中可以引用当前页面的变量
- 18. 双向认证证书配置,Https 双向认证即服务端也要验证客户端,客户端需提供证书,证书配置说明如下:
- 自定义 ArkTS 插件研发
- 自定义 C++ 插件研发
- Web 加载性能优化
- 目录结构
- 常见问题
- 贡献代码
- 许可证
- 官方资源
本文档说明
本文档仅说明 @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 三大平台,也节省了大量的时间和人力成本。
兼容性
支持:
| 项目 | 版本/信息 | |-----|--------| | SDK | API12+ | | IDE | DevEco Studio: 5.0+ | | ROM | 5.1+ | | Emulator | OpenHarmony 6.0+ |
在以下版本中已测试通过:
| 项目 | 版本/信息 | |-----|--------| | @cordova-ohos/ohos | 14.0.1-ohos-14.0.2 | | SDK | 5.0.0(12) | | IDE | DevEco Studio: 6.0.13.200 | | ROM | 5.1.0.120 SP3 | | Emulator | OpenHarmony 6.0.1(21) |
框架自带插件
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" />- 显示 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://www.xxxx.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://www.xxxx.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(this.getUIContext().getHostContext() 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://www.xxxx.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_});
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 = 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="your_domain:8090" p12="your_cert_name.p12" password="your_cert_password"/>(2) 通过 SetClientAuthP12Cert 设置 p12 的 base64 字符串证书
mainPageCycle:MainPageCycle = new MainPageCycle().setOnAboutToAppear((webviewController: webview.WebviewController,parentPage?:object)=>{
let page = parentPage as Index;
// 设置客户端证书
SetClientAuthP12Cert("your_domain:8090", "your_password",
"your_cert_base64_str"
);
})
// 省略其他代码
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:YOUR_PASSWORD
# 从 p12 证书中提取证书
openssl pkcs12 -in client.p12 -clcerts -nokeys -out certificate.crt -legacy -password pass:YOUR_PASSWORD
# 根据证书和 key 生成 p12 证书,如果存在证书链,可以把证书链和证书放在一个文件里面,证书链在上面,证书在下面
openssl pkcs12 -export -out client_new.p12 -inkey private.key -in certificate.crt -password pass:YOUR_PASSWORD
# 根据 P12 证书生成 base64 字符串
base64 -w 0 client_new.p12 >> server.txt(4) 常见问题
error:0308010C: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 == "sayHello") {
return this.sayHello(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;
}
sayHello(args: ESObject[], callbackContext: CallbackContext):boolean {
// 获取 UI 上下文,用于原生 UI 交互
let uiContext:UIContext = this.cordovaWebView!.getUIContext();
let promptAction: PromptAction = uiContext?.getPromptAction();
try {
// 弹出系统原生窗口
promptAction.showDialog({
title: 'Title',
message: 'say hello',
buttons: [
{
text: '确定',
color: '#000000'
}
]
}, (err, data) => {
if (err) {
return;
}
// 执行成功通知 js 侧回调函数,通知函数有多个具体查看 CallbackContext 封装函数
callbackContext.success();
});
} catch (error) {
console.error('Show dialog failed:', error);
}
return true;
}
openSystemBrowser(args: ESObject[], callbackContext: CallbackContext):boolean {
if(args.length > 0 && this.cordovaInterface && this.cordovaInterface.getPage()) {
let url:string = args[0];
// 获取 UIAbilityContext
let page:Page = this.cordovaInterface!.getPage() as Page;
let context = page.getUIContext().getHostContext() 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: 'say 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++ 侧插件,具体步骤如下:
- 在源码集成的 cordova 工程中,在源码的 CPP 目录内新建一个插件目录,保存您的自定义插件;
- 在新目录中新建一个 class,该 class 要继承 CordovaPlugin 类;
- 实现 execute 函数,具体函数接口参考 CordovaPlugin 类注释说明;
- 在您的 CPP 文件中,添加 REGISTER_PLUGIN_CLASS() 注册您的插件名称;用于实例化您的插件对象;
- 如果您的插件中需要调用 ArkTS 侧的代码,需要调用 executeArkTs(同步)或者 executeArkTsAsync(异步)执行 ArkTS 侧代码,参数说明参考 CordovaPlugin 类注释说明;
- 如果您的 ArkTS 侧需要把执行结果通知到 C++ 侧的插件,在 ArkTS 侧需要调用 onArkTsResult 函数通知 C++ 侧,C++ 侧的插件也要实现 onArkTsResult 这个函数,并在 execute 中实现调用;
- 在完成您的插件研发后需要将您的 cpp 文件添加到 CMakeLists.txt 中,完成编译;
- C++ 侧的插件调用方法和 ArkTS 侧插件调用方法完全一样,请参考 ArkTs 插件调用方法,调用您的 C++ 插件。
Web 加载性能优化
1. 预启动 web 和预渲染
在应用启动后,在 EntryAbility 代码中,后台启动 web 引擎,并在后台渲染页面,进入 page 页面后,页面秒开,关闭页面后,页面进入后台,不会销毁 web,下次打开仍可秒开;需提醒的是,在使用 Cordova 的页面预渲染时,会初始化 cordova 插件,有可能会出现在用户没有同意隐私政策前,初始化插件会访问系统资源。
该功能需要对 mainPage 的组件进行二次封装,自己可以根据需要修改代码,如需技术支持请联系本开发者,提供封装方法和源码如下:
参考连接:https://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 = this.getUIContext().getHostContext()?.resourceManager.getRawFileContent(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});目录结构
cordova-ohos # [根目录] Cordova OHOS 移植项目根目录
├── build-profile.json5 # [构建配置] DevEco Studio 的构建配置文件,定义编译模式和签名信息
├── BuildProfile.ets # [构建脚本] 用于构建过程的 ETS 脚本
├── consumer-rules.txt # [混淆规则] 库被其他模块引用时生效的混淆规则
├── hvigorfile.ts # [构建工具] Hvigor 构建工具的配置文件(OHOS 新一代构建工具)
├── Index.ets # [入口文件] 应用或页面的 ArkTS 入口文件
├── libs # [依赖库] 存放第三方预编译的 .so 或 .jar/.har 库文件
├── LICENSE # [协议] 开源许可证文件
├── OAT.xml # [开源声明] Open Source Acknowledgment Tool 配置文件,用于开源软件声明
├── obfuscation-rules.txt # [混淆规则] 代码混淆配置文件(类似 ProGuard 规则)
├── oh_modules # [依赖目录] OpenHarmony 的依赖包目录(类似 npm_modules,但针对 OHOS)
├── oh-package-lock.json5 # [锁定文件] OHOS 包管理器的依赖版本锁定文件
├── oh-package.json5 # [包配置] OHOS 系统的包描述文件,定义包名、版本和依赖
├── package.json # [包配置] 标准的 NPM 包配置文件(可能用于兼容 Node.js 工具链)
├── README.md # [文档] 项目说明文档
├── src # [源码目录] 核心源代码目录
│ └── main # [主源码] 主代码目录
│ └── cpp # [C++ 源码] Native 层核心代码(C++ 实现)
│ ├── Application.cpp # [C++ 实现] 应用生命周期管理类实现
│ ├── Application.h # [C++ 声明] 应用生命周期管理类声明
│ ├── Base64.cpp # [工具类] Base64 编码/解码实现
│ ├── Base64.h # [工具类] Base64 头文件
│ ├── CallbackContext.cpp # [核心类] 回调上下文管理,用于处理 JS 与 Native 的回调交互
│ ├── CallbackContext.h # [核心类] 回调上下文头文件
│ ├── cJSON.cpp # [工具库] JSON 解析库 cJSON 的 C++ 封装实现
│ ├── cJSON.h # [工具库] JSON 解析库头文件
│ ├── CMakeLists.txt # [构建脚本] CMake 构建配置文件,用于编译 C++ 代码
│ ├── ConfigXmlParser.cpp # [解析器] config.xml 配置文件解析器实现
│ ├── ConfigXmlParser.h # [解析器] 配置文件解析器头文件
│ ├── ConnPool.cpp # [网络/线程] 连接池管理实现(网络请求或线程池)
│ ├── ConnPool.h # [网络/线程] 连接池管理头文件
│ ├── CordovaArgs.h # [核心类] 封装传递给插件的参数对象
│ ├── CordovaBridge.cpp # [核心桥接] JS 与 Native 通信的桥梁核心实现
│ ├── CordovaBridge.h # [核心桥接] 通信桥梁头文件
│ ├── CordovaExposedJsApi.cpp # [JS 接口] 暴露给 JS 环境的 Native API 绑定实现
│ ├── CordovaExposedJsApi.h # [JS 接口] 暴露给 JS 环境的 API 头文件
│ ├── CordovaPlugin.h # [核心类] 所有插件必须继承的基类定义
│ ├── CordovaPreferences.cpp # [配置类] 应用偏好设置读取实现
│ ├── CordovaPreferences.h # [配置类] 应用偏好设置头文件
│ ├── CordovaViewController.cpp# [UI 控制] 视图控制器,管理原生视图与 WebView 的交互
│ ├── CordovaViewController.h # [UI 控制] 视图控制器头文件
│ ├── CordovaWebViewEngine.cpp # [核心引擎] WebView 引擎抽象层实现
│ ├── CordovaWebViewEngine.h # [核心引擎] WebView 引擎抽象层头文件
│ ├── CoreHarmony # [OHOS 适配] Cordova 核心插件
│ │ ├── CoreHarmony.cpp # [适配实现] Cordova 核心插件适配代码
│ │ └── CoreHarmony.h # [适配声明] Cordova 核心插件头文件
│ ├── FileCache.cpp # [文件模块] 文件缓存管理实现
│ ├── FileCache.h # [文件模块] 文件缓存管理头文件
│ ├── FileSystem.cpp # [文件模块] 文件系统操作封装实现
│ ├── FileSystem.h # [文件模块] 文件系统操作封装头文件
│ ├── HttpCode.h # [网络模块] HTTP 状态码定义
│ ├── HttpUrl.cpp # [网络模块] URL 处理工具实现
│ ├── HttpUrl.h # [网络模块] URL 处理工具头文件
│ ├── Lock.cpp # [线程安全] 锁机制实现(用于多线程同步)
│ ├── Lock.h # [线程安全] 锁机制头文件
│ ├── Log.h # [工具类] 日志打印宏或工具定义
│ ├── MemPool.cpp # [性能优化] 内存池管理实现
│ ├── MemPool.h # [性能优化] 内存池管理头文件
│ ├── napi_init.cpp # [N-API] Node-API 初始化入口,用于 ArkTS 与 C++ 交互
│ ├── NativeToJsMessageQueue.cpp # [消息队列] Native 向 JS 发送消息的队列实现
│ ├── NativeToJsMessageQueue.h # [消息队列] 消息队列头文件
│ ├── openssl # [安全库] OpenSSL 加密库相关代码(不在本工程目录内,由开发者构建)
│ ├── PluginEntry.h # [插件管理] 插件注册条目定义
│ ├── PluginManager.cpp # [插件管理] 插件管理器实现,负责加载和调度插件
│ ├── PluginManager.h # [插件管理] 插件管理器头文件
│ ├── PluginResult.cpp # [核心类] 插件执行结果封装(成功/失败/回调 ID)
│ ├── PluginResult.h # [核心类] 插件执行结果头文件
│ ├── rawfile_request.cpp # [资源访问] 访问 OHOS rawfile 资源的请求实现
│ ├── rawfile_request.h # [资源访问] 资源请求头文件
│ ├── Socket.cpp # [网络通信] Socket 通信封装实现
│ ├── Socket.h # [网络通信] Socket 通信封装头文件
│ ├── SSLSocket.cpp # [网络安全] SSL 安全套接字实现
│ ├── SSLSocket.h # [网络安全] SSL 安全套接字头文件
│ ├── SystemCookieManager.cpp # [网络模块] 系统 Cookie 管理器实现
│ ├── SystemCookieManager.h # [网络模块] 系统 Cookie 管理器头文件
│ ├── Thread.cpp # [线程管理] 线程封装实现
│ ├── Thread.h # [线程管理] 线程封装头文件
│ ├── TsCordovaPlugin # [TS 插件] TypeScript 插件的 C++ 桥接层
│ │ ├── TsCordovaPlugin.cpp # [桥接实现] TS 插件桥接逻辑
│ │ └── TsCordovaPlugin.h # [桥接声明] TS 插件桥接头文件
│ ├── types # [类型定义] 类型声明目录
│ │ └── libcordova # [库类型] libcordova 的类型定义
│ │ ├── Index.d.ts # [类型声明] TypeScript 定义文件
│ │ └── oh-package.json5 # [包配置] 该子模块的包配置
│ ├── XMLNode.cpp # [XML 解析] XML 节点操作实现
│ ├── XMLNode.h # [XML 解析] XML 节点操作头文件
│ ├── XMLParser.cpp # [XML 解析] XML 解析器主逻辑实现
│ └── XMLParser.h # [XML 解析] XML 解析器头文件
├── ets # [ArkTS 源码] OHOS 端 UI 与逻辑层代码
│ └── module.json5 # [模块配置] OHOS 模块的配置文件(定义 pages, abilities 等)
├── resources # [资源目录] 存放图片、字符串、布局等应用资源
├── ohosTest # [测试目录] OHOS 系统下的单元测试或 UI 测试代码
└── test # [测试目录] 通用测试代码常见问题
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. 屏蔽跟随系统字体大小
- 在 app.json5 中增加 configuration 选项以屏蔽跟随系统字体大小,具体配置方法参考:[https://docs.openharmony.cn/pages/v5.0.3/zh-cn/application-dev/quick-star
