{"_id":"@capacitor-ohos/ohos","_rev":"4-daf373152bbab6b0504558da99d2e2f9","name":"@capacitor-ohos/ohos","dist-tags":{"latest":"8.0.2"},"versions":{"8.0.1":{"name":"@capacitor-ohos/ohos","version":"8.0.1","author":{"url":"Group","name":"Huawei Device Co., Ltd and iSoftStone Information Technology"},"license":"MIT","_id":"@capacitor-ohos/ohos@8.0.1","maintainers":[{"name":"luqi_tan","email":"295099422@qq.com"}],"homepage":"https://gitcode.com/CPF-Ionic/openHarmony-capacitor","bugs":{"url":"https://gitcode.com/CPF-Ionic/openHarmony-capacitor/issues"},"dist":{"shasum":"e493b5303f64ab0968701e51438bfdab130a52a9","tarball":"https://registry.npmjs.org/@capacitor-ohos/ohos/-/ohos-8.0.1.tgz","fileCount":145,"integrity":"sha512-sKwGWxXLjjcnHq125urjRAxAMInvQUMHWcbM/CqEi+bcIF/qiY2RXTiDMz/owPCE+dGX67Vano/I+oUD2osYxg==","signatures":[{"sig":"MEUCIDOZqVK6hp+lZYPacSmVEY1WT6CIK4QnS3Xlsx472DH1AiEApuKxwfYwTEWwCXGIsNbELpZnTLzt3d/QZG5wdcOklGo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1356284},"gitHead":"9d757f95c22d6f71d5bb65805ce96ba4f64c5f71","_npmUser":{"name":"luqi_tan","email":"295099422@qq.com"},"repository":{"url":"git+https://gitcode.com/CPF-Ionic/openHarmony-capacitor.git","type":"git"},"_npmVersion":"10.5.1","description":"Capacitor: Cross-platform apps with JavaScript and the web","directories":{},"_nodeVersion":"22.0.0","_hasShrinkwrap":false,"peerDependencies":{"@capacitor/core":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/ohos_8.0.1_1777547540132_0.11759734776452913","host":"s3://npm-registry-packages-npm-production"}},"8.0.2":{"name":"@capacitor-ohos/ohos","version":"8.0.2","description":"Capacitor: Cross-platform apps with JavaScript and the web","homepage":"https://gitcode.com/CPF-Ionic/openHarmony-capacitor","author":{"name":"Huawei Device Co., Ltd and iSoftStone Information Technology","url":"Group"},"license":"MIT","repository":{"type":"git","url":"git+https://gitcode.com/CPF-Ionic/openHarmony-capacitor.git"},"bugs":{"url":"https://gitcode.com/CPF-Ionic/openHarmony-capacitor/issues"},"peerDependencies":{"@capacitor/core":"^8.0.0"},"_id":"@capacitor-ohos/ohos@8.0.2","gitHead":"66051d1bc0b32551cf319bf29fa791523001c02f","_nodeVersion":"22.0.0","_npmVersion":"10.5.1","dist":{"integrity":"sha512-BvKuFiIPxHNaIixzkaL4KkNZGnFcDjv3jkQ1XsWhByOM9Z/IZdcjqGj7D4G+VQ0ss3ciDSWaahIt5ScIbEfv4A==","shasum":"4aecf139a51c5f81ee539273bff2ec6d3ea26cc6","tarball":"https://registry.npmjs.org/@capacitor-ohos/ohos/-/ohos-8.0.2.tgz","fileCount":145,"unpackedSize":1356284,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQD2D9h+E/kA5C9Q6Ihy0AIJCrM6j3p5EexAGda3wkCEIQIgBYL80STWImYa5ZavIHlZfB7TcHbBMNkEq1pr9G6ou44="}]},"_npmUser":{"name":"luqi_tan","email":"295099422@qq.com"},"directories":{},"maintainers":[{"name":"luqi_tan","email":"295099422@qq.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/ohos_8.0.2_1784797524432_0.2800253535587487"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-30T11:12:20.009Z","modified":"2026-07-23T09:05:24.741Z","8.0.1-beta.1":"2026-04-16T12:39:43.246Z","8.0.1":"2026-04-30T11:12:20.319Z","8.0.2":"2026-07-23T09:05:24.559Z"},"bugs":{"url":"https://gitcode.com/CPF-Ionic/openHarmony-capacitor/issues"},"author":{"name":"Huawei Device Co., Ltd and iSoftStone Information Technology","url":"Group"},"license":"MIT","homepage":"https://gitcode.com/CPF-Ionic/openHarmony-capacitor","repository":{"type":"git","url":"git+https://gitcode.com/CPF-Ionic/openHarmony-capacitor.git"},"description":"Capacitor: Cross-platform apps with JavaScript and the web","maintainers":[{"name":"luqi_tan","email":"295099422@qq.com"}],"readme":"# <center>capacitor</center>\r\n[![zh-CN](https://img.shields.io/badge/lang-中文-blue.svg)](README.md)\r\n[![en](https://img.shields.io/badge/lang-English-blue.svg)](README.en.md)\r\n\r\n本项目基于 [@capacitor/android@8.0.0](https://www.npmjs.com/package/@capacitor/android) 开发。\r\n\r\n## 简介\r\n\r\nopenHarmony-capacitor 是 capacitor 的 OpenHarmony 化版本，所有接口兼容 capacitor 的 Android 和 iOS 版本，本文档仅说明 openHarmony-capacitor 框架部分的使用手册、开发说明、集成步骤等。\r\n\r\n## 支持平台\r\n\r\n- **OpenHarmony**：5.0+\r\n\r\n## 依赖说明\r\n\r\n本工程依赖 openssl，官方网站 [https://openssl.org](https://openssl.org/)，编译之前先集成 openssl，只有成功集成后，才可编译，集成方法：[https://gitcode.com/li_in/openharmony-capacitor-openssl3.5](https://gitcode.com/li_in/openharmony-capacitor-openssl3.5)。\r\n\r\n## 开发说明\r\n\r\nopenHarmony-capacitor 是 capacitor 的 OpenHarmony 化版本，并支持 ArkTS 侧和 C/C++ 侧自定义插件研发，框架采用 C/C++ 研发，底层使用自研 Socket TCP/IP 通讯，封装了 HTTP/HTTPS 协议通讯解决各种跨域访问问题，无需配置 web 服务端，同时结合 webview 的通讯协议栈，大大提高应用层网络请求效率。\r\n\r\n## 附加说明\r\n\r\nopenHarmony-capacitor 使用多页面视图研发，同时兼容 Android 和 iOS 原有的单页面视图，原有项目可以轻松移植；另外在复杂项目中，可以使用 openHarmony-capacitor 的多页面视图功能，创建多个 webview 协同工作。\r\n\r\n## 开发背景\r\n\r\ncapacitor 官方网站：[https://capacitorjs.com](https://capacitorjs.com)，是移动端跨平台的新框架，大量厂商直接或间接采用此框架开发 APP；但是目前不支持 OpenHarmony 版本，开发者将原 Android 和 iOS 项目移植到 OpenHarmony 版，无法适配，为此研发了 openHarmony-capacitor，遵守 capacitor 官方标准，原有项目无需投入任何研发轻松移植到 OpenHarmony 系统；新开发的项目，一次研发就适用于 Android、iOS 和 OpenHarmony 三大平台，也节省了大量的时间和人力成本。\r\n\r\n本框架为 cordova-openharmony 框架的升级版本，沿用了部分 cordova-openharmony 框架的能力，在插件和通信部分针对 capacitor 做了优化，部分功能仍沿用 cordova-openharmony 框架。\r\n\r\n## 兼容性\r\n\r\n在以下版本中已测试通过：\r\n\r\n1. SDK: 5.0.5(17); IDE: DevEco Studio: 6.0.0; ROM: 5.1.0.150;\r\n\r\n## 使用说明\r\n\r\n### 1. 创建项目\r\n\r\n打开 DevEco 创建项目，选择 Empty Ability 进入下一步 (next)，填写必要信息，点击完成 (finish)，工程创建完成。\r\n\r\n### 2. 集成源码\r\n\r\n下载本工程，放入主工程文件夹中，此时在 DevEco 中已经可以看到 capacitor 的模块工程了。\r\n\r\n### 3. 引入依赖\r\n\r\n在根目录的 oh-package.json 中引入依赖：\r\n```json\r\n{\r\n  \"dependencies\": {\r\n    \"openHarmony-capacitor\": \"file:./capacitor\"\r\n  }\r\n}\r\n```\r\n\r\n然后再修改 build-profile.json5（项目级）配置文件，在 modules 模块中增加：\r\n\r\n```json5\r\n{\r\n    \"name\": \"capacitor\",\r\n    \"srcPath\": \"./capacitor\",\r\n}\r\n```\r\n\r\n以上三步操作后，已经在主工程中集成了 capacitor 的源码了。\r\n\r\n### 4. 项目移植\r\n\r\n**前端工程打包**\r\n\r\n为提升项目部署灵活性，需将前端工程的资源引用及路由跳转逻辑从「绝对根路径依赖」调整为「相对路径引用」：\r\n\r\n调整页面基准路径：\r\n\r\n1. 在项目根目录的 index.html 文件中，将 `<base>` 标签的 href 属性由默认的绝对根路径 `/` 修改为相对路径 `./`，作为页面所有相对路径资源的基准锚点；\r\n2. 配置打包输出路径：如在 Vue 工程中，将 vue.config.js 配置文件中，控制 Webpack 静态资源打包路径的核心属性 publicPath 由默认的 `/` 调整为 `./`，确保打包后 JS、CSS、图片等静态资源均采用相对路径引用。\r\n\r\n**Android 项目移植：**\r\n\r\n复制原有 Android studio 的工程 assets 目录下面的所有文件到 OpenHarmony 工程 entry/src/main/resources/rawfile 目录下，原 Android 工程的 assets 目录包含 config.xml（如果有）、capacitor.config.json（必须）、capacitor.plugins.json（必须）和 dist 目录，将 dist 文件夹名修改为 www，www 目录包含 index.html（必须）、cordova.js（如果有）、cordova_plugins.js（如果有）、css 目录、js 目录等，如果要指定加载页面，不使用默认页面，请查看高级功能部分说明。复制成功后，仍需要安装 Android 包含的 OpenHarmony 版插件。\r\n\r\n**iOS 项目移植：**\r\n\r\n第一步：复制原有 iOS App 目录下的 public 文件夹到 OpenHarmony 工程 entry/src/main/resources/rawfile 目录下，将 public 文件夹名修改为 www，文件包含：index.html（必须）、cordova.js（如果有）、cordova_plugins.js（如果有）、css 目录、js 目录等。\r\n\r\n第二步：Xcode 工程的配置文件在 App 目录下，Xcode 工程的该文件不能直接被 openHarmony-capacitor 使用，需要进行转换，该文件主要记录的是框架配置信息、插件的名称和初始化的类，因为 OpenHarmony 版是根据 Android 的配置文件进行插件初始化的，因此需要将 Xcode 工程配置文件转为 Android 的配置文件，请将 Xcode 工程使用 node 加入 Android 平台，系统会自动生成 Android 版的 config.xml（如果有）、capacitor.config.json（必须）、capacitor.plugins.json（必须）。然后将文件复制到 OpenHarmony 版工程的 entry/src/main/resources/rawfile 下。复制成功后，仍需要安装 iOS 包含的 OpenHarmony 版插件。\r\n\r\n**新建项目：**\r\n\r\n如果您没有 Android 和 iOS 项目，需要使用 capacitor 的框架，创建 Android 项目，创建成功后，再按照 Android 项目移植方法操作即可。\r\n\r\n**添加配置：**\r\n\r\n在 capacitor.config.json 中添加 harmony 属性，在此可配置自定义配置。\r\n\r\n```json\r\n{\r\n  \"appId\": \"appId\",\r\n  \"appName\": \"appName\",\r\n  \"webDir\": \"www\",\r\n  \"harmony\": {\r\n    // 自定义配置。\r\n  }\r\n}\r\n```\r\n\r\n**特殊使用：**\r\n\r\n如果是非 capacitor，或者不涉及插件等需要进行原生通信功能，仅仅用于 UI 渲染展示，可以在 MainPages 设置 `isInjectBridgeJs:false`，不进行 native-bridge.js 的注入，从而提高加载速度。\r\n\r\n### 5. 修改 Index.ets 文件\r\n\r\n打开 OpenHarmony 工程文件 entry/src/main/ets/pages/Index.ets 文件，修改代码如下（可以直接全部拷贝到 Index.ets 文件中）：\r\n\r\n```typescript\r\nimport { MainPage, pageBackPress, pageHideEvent, pageShowEvent, PluginEntry, MainPageOnBackPress} from 'openHarmony-capacitor';\r\n//import { TestPlugin } from \"../plugins/TestPlugin\" //自定义插件TestPlugin，根据实际情况导入自己的自定义插件\r\n@Entry\r\n@Component\r\nstruct Index {\r\n  //ArkTs侧的自定义插件：配置插件名称和对象，请查看自定义开发部分\r\n  cordovaPlugs: Array<PluginEntry> = [];\r\n  mainPageOnBackPress: MainPageOnBackPress = new MainPageOnBackPress();\r\n  /*\r\n  cordovaPlugs: Array<PluginEntry> =\r\n  [\r\n      {\r\n        pluginName: 'TestPlugin', //插件名称\r\n        pluginObject: new TestPlugin() //实例化插件对象供框架调用\r\n      }\r\n  ];\r\n  */\r\n  onPageShow() {\r\n    pageShowEvent(); //页面显示通知框架\r\n  }\r\n  onBackPress() {\r\n    pageBackPress(); //拦截返回键由框架处理\r\n    return this.mainPageOnBackPress.backPress();\r\n  }\r\n  onPageHide() {\r\n    pageHideEvent(); //页面隐藏通知框架\r\n  }\r\n  build() {\r\n    RelativeContainer() {\r\n      //默认加载rawfile/www/index.html\r\n      //如果要指定加载页面参考高级功能部分\r\n      MainPage({ isWebDebug: false, cordovaPlugs: this.cordovaPlugs });\r\n    }\r\n    .height('100%')\r\n    .width('100%')\r\n  }\r\n}\r\n```\r\n\r\n### 6. 修改 EntryAbility.ets 文件\r\n\r\n打开 OpenHarmony 工程文件 entry/src/main/ets/entryAbility/EntryAbility.ets 文件，修改 onCreate 函数如下：\r\n\r\n```typescript\r\nimport { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';\r\nimport { hilog } from '@kit.PerformanceAnalysisKit';\r\nimport { window } from '@kit.ArkUI';\r\nimport { webview } from '@kit.ArkWeb';  //引入webview\r\n\r\n//省略部分代码\r\nonCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {\r\n   hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');\r\n   webview.WebviewController.initializeWebEngine();//webview引擎初始化\r\n}\r\n```\r\n\r\n### 7. 完成\r\n\r\n做以上代码修改后，OpenHarmony 的移植已经完毕，可以使用模拟器或者真机进行编译和测试了。\r\n\r\n## 高级用法（区别于 Android 和 iOS）\r\n\r\n### 1. MainPage 传入 indexPage 参数设置自定义启动路径，支持 rawfile、resfile 和沙箱路径\r\n\r\n```typescript\r\n/*\r\n*    indexPage:默认启动首页，举例如下:\r\n*    \"/www/index.html\"：rawfile目录下的文件\r\n*    \"/data/storage/el2/base/files/www/index.html\"：使用虚拟域名www.example.com加载沙箱路径下的文件，\r\n*    \"https://cn.bing.com\":加载在线网页，必须指定https或者http\r\n*    \"file:///data/storage/el2/base/files/www/index.html\":file协议加载el2级别沙箱路径文件\r\n*    \"file:///data/storage/el1/bundle/entry/resources/resfile/www/index.html\":file协议加载el1级别沙箱路径文件\r\n*    \"file://\" + getContext().resourceDir + \"/www/index.html\":file协议加载el1级别沙箱路径文件\r\n*    capacitor支持使用虚拟域名www.example.com加载本地文件，也支持使用file协议加载本地文件\r\n*    改变this.indexPage的值，webview会重新加载页面\r\n*/\r\n//省略其它代码\r\nMainPage({indexPage:\"/www/index.html\"});\r\n//省略其它代码\r\n```\r\n\r\n### 2. 注册自定义用户 scheme，用于 capacitor 内部拦截 scheme 的请求\r\n\r\n```typescript\r\nimport { RegisterCustomSchemes } from 'openHarmony-capacitor';\r\nonCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {\r\n   hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');\r\n   RegisterCustomSchemes(\"cmp\"); //注册自定义scheme\r\n   webview.WebviewController.initializeWebEngine();//webview引擎初始化\r\n}\r\n```\r\n\r\n```typescript\r\n//customSchemes:自定义scheme，多个scheme用\",\"分隔\r\n//省略其它代码\r\nMainPage({customSchemes:\"cmp,xmp,xxx\"});\r\n//省略其它代码\r\n```\r\n\r\n### 3. 拦截自定义的 scheme，在 webview 端拦截并处理，也可以拦截 http(s) 请求处理\r\n\r\n```typescript\r\n/*\r\n*拦截请求函数，根据需要拦截相应请求，一般用于自定义scheme，如果存在自定义scheme的必须使用此函数拦截处理\r\n*使用此函数拦截自己的scheme进行处理，也可以在MainPage生命周期回调函数中拦截处理，二选一，不能同时拦截处理。\r\n*拦截后处理有两种方式，推荐使用第一种方式\r\n*   1. capacitor webview内核处理，返回null，capacitor可以处理替换所有资源，例如在线资源，本地资源，js、img、css等\r\n*   2. 自己处理，返回WebResourceResponse\r\n*说明如下：\r\n*   1. 子组件的回调函数不能使用this指针，如果要使用this，请参考parentPage参数\r\n*   2. 采用第一种方式，写法简单，且效率高，推荐第一种方式\r\n*/\r\nonInterceptWebRequest(request: WebResourceRequest, webTag: string): ESObject {\r\n  let url = request.getRequestUrl();\r\n  //capacitor webview内核处理替换\r\n  if (url == \"cmp://v1.1.1/temp/test2.png\") {\r\n    /*\r\n    *替换资源说明如下：\r\n    *本地资源请使用https://www.example.com的虚拟域名作为访问本地资源的标记\r\n    *详细了解www.example.com内置虚拟域名规则，查看最后面的常见问题说明\r\n    *被替换和替换内容可以是图片、css、js等\r\n    *替换资源举例如下：\r\n    *1. 沙箱路径\r\n    *   https://www.example.com/data/storage/el2/base/files/test.png\r\n    *2. rawfile目录的下的资源文件\r\n    *   https://www.example.com/www/test.png\r\n    *3. 网络在线资源\r\n    *   https://www.chuzhitong.com/images/logo.png\r\n    *4. cdvfile协议的沙箱路径的文件，绝对路径\r\n    *   cdvfile:///data/storage/el2/base/files/test.png\r\n    *此函数是通知capacitor webview内核，后续加载页面实施资源替换\r\n    */\r\n    SetResourceReplace(webTag, url, \"https://www.chuzhitong.com/images/logo.png\");\r\n  }\r\n\r\n  //自己处理资源返回webview\r\n  if(url == \"https://www.ext.com/v1.1.1/temp/test3.png\") {\r\n    let response = new WebResourceResponse();\r\n    response.setResponseData($rawfile(\"www/picture/bao.png\"));\r\n    response.setResponseEncoding('utf-8');\r\n    response.setResponseMimeType(\"image/png\");\r\n    response.setResponseCode(200);\r\n    response.setReasonMessage('OK');\r\n    response.setResponseIsReady(true);\r\n    return response;\r\n  }\r\n  return null;\r\n}\r\n\r\n//省略其它代码\r\n/*\r\n*onInterceptWebRequest 返回null放行，返回具体的WebResourceResponse\r\n*\r\n*/\r\nMainPage({onInterceptWebRequest: this.onInterceptWebRequest});\r\n//省略其它代码\r\n```\r\n\r\n### 4. 在原生层，动态设置 webview 属性\r\n\r\n```typescript\r\n/*\r\n*在原生层页面加载后，往页面中注入新的js，也可以在mainPage的生命周期页面加载完毕后注入js\r\n*/\r\nonSetCordovaWebAttribute(cordovaWebView: CordovaWebView) {\r\n    if(cordovaWebView) {\r\n    //获取webview属性变量，用于动态修改webview属性，具体参考如下连接，页面加载完成后触发\r\n    //OpenHarmony并不支持WebAttribute组件属性的动态设置，但是可以设置部分属性，不支持的属性会抛出\"Method not implemented.\"、\"is not callable\"等异常信息\r\n    //https://docs.openharmony.cn/pages/v5.1.0/zh-cn/application-dev/reference/apis-arkweb/js-apis-webview.md\r\n    //https://docs.openharmony.cn/pages/v6.0/zh-cn/application-dev/reference/apis-arkui/arkui-ts/ts-universal-attributes-attribute-modifier.md\r\n    cordovaWebView!.getWebAttribute()?.height('50%');\r\n    //获取webview的控制变量，用于实现具体的功能，示例代码实现在webview执行js或者注入新的js，具体参考如下连接\r\n    //https://docs.openharmony.cn/pages/v5.1.0/zh-cn/application-dev/reference/apis-arkweb/ts-basic-components-web.md\r\n    cordovaWebView!.getWebviewController().runJavaScript(\"alert(1);\");\r\n  }\r\n}\r\n//省略其它代码\r\nMainPage({onSetCordovaWebAttribute: this.onSetCordovaWebAttribute});\r\n//省略其它代码\r\n```\r\n\r\n### 5. 多 webview 界面，即多页面视图，自定义 webId，使用自定义插件各 webview 之间通讯，可用于平板等大屏幕研发需求\r\n\r\n```typescript\r\n//省略其它代码\r\n//webId:自定义webId，用于多webview，各webview之间通讯，webId确保唯一，参考自定义插件研发示例代码\r\nMainPage({ webId:\"123456\"})\r\n//省略其它代码\r\n```\r\n\r\n### 6. 动态创建组件，在 webview 和 NodeController 相结合实现动态创建和显示组件时，切记一定要设定 webId 参数，避免重复创建 webview\r\n\r\n```typescript\r\n//动态创建MainPage的示例代码，主要用于原生界面和webview界面显示在同一个视图里面的混合式研发\r\n//如果要传入其它参数，参考此文档详细了解\r\n//https://docs.openharmony.cn/pages/v6.0/zh-cn/application-dev/reference/apis-arkui/arkui-js/js-components-create-elements.md\r\n@Builder\r\nfunction buildMainPage() {\r\n  Column() {\r\n    //直接加载在线网站\r\n    MainPage({webId: \"123456\", indexPage: \"https://cn.bing.com\", cordovaPlugs: [\r\n      {\r\n        pluginName: 'TestPlugin', //插件名称\r\n        pluginObject: new TestPlugin() //实例化插件对象\r\n      }\r\n    ]});\r\n  }.width(\"100%\").height(\"100%\")\r\n}\r\n\r\nclass TextNodeController extends NodeController {\r\n  private textNode: BuilderNode<[]> | null = null;\r\n\r\n  constructor() {\r\n    super();\r\n  }\r\n\r\n  makeNode(context: UIContext): FrameNode | null {\r\n    // 创建BuilderNode实例\r\n    this.textNode = new BuilderNode(context);\r\n    this.textNode.build(wrapBuilder<[]>(buildMainPage));\r\n    // 返回需要显示的节点\r\n    return this.textNode.getFrameNode();\r\n  }\r\n}\r\n```\r\n\r\n```typescript\r\n//省略其它代码\r\nprivate textNodeController = new TextNodeController();\r\n\r\n//省略其它代码\r\nRelativeContainer() {\r\n    if (this.isShow) {\r\n      NodeContainer(this.textNodeController)\r\n        .width('100%')\r\n        .height(\"100%\")\r\n        .backgroundColor('#FFF0F0F0')\r\n    }\r\n}\r\n.height('30%')\r\n.width('100%')\r\n\r\nButton(\"显示和隐藏web\").onClick(()=>{\r\n  this.isShow = false;\r\n})\r\n```\r\n\r\n### 7. W3C WEB 授权 webview 权限，例如 webview 调起摄像头和麦克风\r\n\r\n```typescript\r\n//Web组件可以通过W3C标准协议授权回调函数，例如拉起摄像头和麦克风，示例如下\r\nonPermissionRequest(event:OnPermissionRequestEvent, parentPage?: object){\r\n  let page = parentPage as Index;//page为当前页面对象，相当于当前页面的this指针，使用该对象，必须将this指针传入到mainPage中\r\n  if (event) {\r\n    //拉起摄像头和麦克风，为确保用户拒绝后能二次拉起授权，需要多个授权时，单独分开授权\r\n    //单独分开授权会多次弹出窗口，仅供参考，也可以一次授权多个权限，但是用户拒绝后，无法拉起二次授权窗口\r\n    //授权摄像头和麦克风，弹窗授权\r\n    //const yourPermissions: Array< Permissions> = ['ohos.permission.CAMERA', 'ohos.permission.MICROPHONE'];\r\n    //授权加速度和陀螺仪，无弹窗用户无感知\r\n    const yourPermissions: Array< Permissions> = ['ohos.permission.ACCELEROMETER', 'ohos.permission.GYROSCOPE'];\r\n    for (let i = 0; i < yourPermissions.length; i++) {\r\n      let confirmPermissions: Array< Permissions> = [yourPermissions[i]];\r\n      let atManager = abilityAccessCtrl.createAtManager();\r\n      atManager.requestPermissionsFromUser(getContext(this), confirmPermissions).then((data) => {\r\n        let grantStatus: Array< number> = data.authResults;\r\n        if (grantStatus[0] != 0) {\r\n          // 用户拒绝授权，提示用户必须授权才能访问当前页面的功能，并引导用户到系统设置中打开相应的权限\r\n          atManager.requestPermissionOnSetting(getContext() as common.UIAbilityContext, confirmPermissions)\r\n            .then((data: Array< abilityAccessCtrl.GrantStatus>) => {\r\n              if (data.length > 0 && data[0] == 0 ) {\r\n                event.request.grant(event.request.getAccessibleResource());\r\n              }\r\n              console.info('data:' + JSON.stringify(data));\r\n            })\r\n            .catch((err: BusinessError) => {\r\n              console.error('data:' + JSON.stringify(err));\r\n              return;\r\n            });\r\n        } else {\r\n          event.request.grant(event.request.getAccessibleResource());\r\n        }\r\n      }).catch((error: BusinessError) => {\r\n        console.error(`Failed to request permissions from user. Code is ${error.code}, message is ${error.message}`);\r\n      })\r\n    }\r\n  }\r\n}\r\n\r\n/*\r\n*onPermissionRequest:web组件W3C标准拉起授权的回调函数\r\n*    参考连接:https://docs.openharmony.cn/pages/v6.0/zh-cn/application-dev/web/web-rtc.md\r\n*/\r\nMainPage({parentPage: this, onPermissionRequest: this.onPermissionRequest,});\r\n```\r\n\r\n### 8. 父组件感知 MainPage 子组件的所有生命周期，在不同的周期执行相应的操作\r\n\r\n```typescript\r\n//MainPage的生命周期的各回调函数，根据业务需要设置单个或多个生命周期回调函数添加业务功能\r\n//生命周期的说明参考：https://docs.openharmony.cn/pages/v6.0/zh-cn/application-dev/web/web-event-sequence.md\r\nmainPageCycle?: MainPageCycle;\r\naboutToAppear() {\r\n    this.mainPageCycle = new MainPageCycle()\r\n        .setOnAboutToAppear((webviewController: webview.WebviewController, parentPage?: object)=>{\r\n          //page为当前页面对象，相当于当前页面的this指针，使用该对象，必须将this指针通过parentPage参数传入mainPage中\r\n          let page = parentPage as Index;\r\n          console.log(\"exec onAboutToAppear\");\r\n        })\r\n        .setOnControllerAttached((webviewController: webview.WebviewController, parentPage?: object)=>{\r\n          console.log(\"exec onControllerAttached\");\r\n        })\r\n        .setOnLoadIntercept((webResourceRequest: WebResourceRequest, parentPage?: object):boolean=>{\r\n          console.log(\"exec onLoadIntercept\");\r\n          return false;\r\n        })\r\n        .setOnOverrideUrlLoading((webResourceRequest: WebResourceRequest, parentPage?: object):boolean=>{\r\n          console.log(\"exec onOverrideUrlLoading\");\r\n          return false;\r\n        })\r\n        .setOnInterceptRequest((request: WebResourceRequest, webTag:string, parentPage?: object):WebResourceResponse|null=>{\r\n          console.log(\"exec setOnInterceptRequest\");\r\n          return null;\r\n        })\r\n        .setOnPageBegin((url:string, parentPage?: object):void=>{\r\n          console.log(\"exec onPageBegin\");\r\n        })\r\n        .setOnProgressChange((newProgress: number, parentPage?: object):void=>{\r\n          console.log(\"exec onProgressChange\");\r\n        })\r\n        .setOnPageEnd((url:string, webviewController: webview.WebviewController, parentPage?: object):void=>{\r\n          console.log(\"exec onPageEnd\");\r\n        })\r\n        .setOnPageVisible((url:string, parentPage?: object):void=>{\r\n          console.log(\"exec onPageVisible\");\r\n        })\r\n        .setOnRenderExited((renderExitReason:RenderExitReason, parentPage?: object):void=>{\r\n          console.log(\"exec onRenderExited\");\r\n        })\r\n        .setOnDisAppear((parentPage?: object):void=>{\r\n          console.log(\"exec onDisAppear\");\r\n        });\r\n}\r\n```\r\n\r\n```typescript\r\n//省略其它代码\r\n/*\r\n*lifeCycle:传入生命周期对象，让父组件感知MainPage的生命周期，进行相应业务处理\r\n*parentPage:传入this，就是webview父组件对象，也就是当前组件的对象，可以在插件里面调用\r\n*/\r\nMainPage({lifeCycle: this.mainPageCycle, parentPage: this})\r\n//省略其它代码\r\n```\r\n\r\n### 9. 在同一个 Page 中加载多个 webview，实现本地、在线页面混合研发\r\n\r\n```typescript\r\nbuild() {\r\n  Column() {\r\n    RelativeContainer() {\r\n      MainPage({indexPage:\"/www/index.html\"});\r\n    }\r\n    .height('30%')\r\n    .width('100%')\r\n    RelativeContainer() {\r\n      MainPage({indexPage:\"https://developer.huawei.com\"});\r\n    }\r\n    .height('30%')\r\n    .width('100%')\r\n  }\r\n}\r\n```\r\n\r\n### 10. 加载不包含 cordova.js 和 capacitor.js 页面，父组件控制 webview 的返回键，或者自己控制路由\r\n\r\n```typescript\r\n/*\r\n*控制mainPage的页面返回，需将此对象传入MainPage\r\n*如果加载的页面不包含cordova.js和capacitor.js，使用pageBackPress无法通知capacitor返回，必须使用此对象控制页面返回\r\n*也可以通过此对象控制webview的路由\r\n*/\r\nmainPageOnBackPress: MainPageOnBackPress = new MainPageOnBackPress();\r\n\r\nonBackPress() {\r\n\tpageBackPress();\r\n    /*\r\n     *如果加载的页面没有包含cordova.js和capacitor.js，例如加载https://cn.bing.com，\r\n     * 返回值\r\n     *  true:已经到了页面顶层\r\n     *  false:返回了上一页\r\n     */\r\n    //return this.mainPageOnBackPress.backPress();\r\n\treturn true;\r\n}\r\n//backPress:传入控制webview路由的对象，加载的页面不包含cordova.js和capacitor.js时控制webview路由，需要传\r\nMainPage({ backPress: this.mainPageOnBackPress })\r\n```\r\n\r\n### 11. MainPage 的路由开关控制，便于 MainPage 嵌套使用，路由子原生页面内再嵌套使用 MainPage\r\n\r\n```typescript\r\n/*\r\n*isNavPath：true使用MainPage组件内的路由，默认是true，false:不使用MainPage内的路由，\r\n*     特别是MainPage嵌套使用时，父组件要打开路由，子组件关闭路由，否则会路由冲突\r\n*/\r\nMainPage({isNavPath:false});\r\n```\r\n\r\n### 12. 自定义 cookie，传入 cookie 键值对\r\n\r\n```typescript\r\n//手动添加cookie，在发送POST或者Get请求时携带cookie，https的session cookie无需手动设置，cordova会自动处理\r\n//http的session cookie参考最后的https的cookie说明\r\nthis.cookies.set(\"https://mem.tongecn.com\", [\"key1=value1; path=/; Domain=.tongecn.com\", \"key2=value2\"]);\r\n\r\n/*\r\n*cookies:如果ArkTs侧有自定义的cookie，可以通过此参数传入\r\n*     一般情况下cookie都是cordova自动处理的，无需ArkTS侧手动设置，不过ArkTS侧通过此参数可以手动设置cookie\r\n*     如果您的请求是采用的http协议非https，分为跨域请求和非跨域请求，请查看最后的常见问题说明\r\n*/\r\nMainPage({cookies: this.cookies});\r\n```\r\n\r\n### 13. 自定义 webview 字体大小缩放百分比，支持适老化，屏蔽跟随系统字体大小变化\r\n\r\n```typescript\r\n/*\r\n*     textZoomRatio:webview字体放大缩小百分比，默认是100保持默认\r\n*     设置webview不跟随系统字体大小、并且屏蔽跟随显示大小缩放后\r\n*     可以通过此参数统一设置webview字体大小变化百分比，避免页面错乱\r\n*     也可以通过Device插件增加的字体大小百分比接口函数，在js侧设置，参考Device插件\r\n*     参考常见问题屏蔽跟随系统字体大小和屏蔽跟随显示大小缩放\r\n*/\r\nMainPage({textZoomRatio:110});\r\n```\r\n\r\n### 14. 同层渲染，以及同层渲染组件和插件结合的使用的方法\r\n\r\n```typescript\r\n//同层渲染示例代码，H5页面增加一个原生的TextInput组件\r\n@Observed\r\ndeclare class Params{\r\n  elementId: string\r\n  textOne: string\r\n  textTwo: string\r\n  width: number\r\n  height: number\r\n  onTextChange?: (value: string) => void;\r\n}\r\n\r\n@Component\r\nstruct TextInputComponent {\r\n  @Prop params: Params\r\n  @State bkColor: Color = Color.Blue\r\n\r\n  build() {\r\n    Column() {\r\n      TextInput({text: '', placeholder: 'please input your word...'})\r\n        .placeholderColor(Color.Gray)\r\n        .id(this.params?.elementId)\r\n        .placeholderFont({size: 13, weight: 400})\r\n        .caretColor(Color.Gray)\r\n        .width(this.params?.width)\r\n        .height(this.params?.height)\r\n        .fontSize(14)\r\n        .fontColor(Color.Black)\r\n        .onChange((value:string)=>{\r\n          if (this.params.onTextChange) {\r\n            this.params.onTextChange(value); // 触发回调\r\n          }\r\n        })\r\n    }\r\n    //自定义组件中的最外层容器组件宽高应该为同层标签的宽高\r\n    .width(this.params.width)\r\n    .height(this.params.height)\r\n  }\r\n}\r\n\r\n@Builder\r\nfunction TextInputBuilder(params:Params) {\r\n  TextInputComponent({params: params})\r\n    .width(params.width)\r\n    .height(params.height)\r\n    .backgroundColor(Color.White)\r\n}\r\n\r\nclass MyNodeController extends NodeController {\r\n  private rootNode: BuilderNode<[Params]> | undefined | null;\r\n  private surfaceId_: string = \"\";\r\n  private renderType_: NodeRenderType = NodeRenderType.RENDER_TYPE_DISPLAY;\r\n  private width_: number = 0;\r\n  private height_: number = 0;\r\n  private embedId_: string = \"\";\r\n  private isDestroy_: boolean = false;\r\n\r\n  setRenderOption(params: ESObject) {\r\n    this.surfaceId_ = params.surfaceId;\r\n    this.renderType_ = params.renderType;\r\n    this.embedId_ = params.embedId;\r\n    this.width_ = params.width;\r\n    this.height_ = params.height;\r\n  }\r\n\r\n  // 必须要重写的方法，用于构建节点数、返回节点数挂载在对应NodeContainer中。\r\n  makeNode(uiContext: UIContext): FrameNode | null {\r\n    if (this.isDestroy_) { // rootNode为null\r\n      return null;\r\n    }\r\n    if (!this.rootNode) {// rootNode 为undefined时\r\n      this.rootNode = new BuilderNode(uiContext, { surfaceId: this.surfaceId_, type: this.renderType_ });\r\n      if(this.rootNode) {\r\n        this.rootNode.build(wrapBuilder(TextInputBuilder),\r\n            {textOne: \"myTextInput\", width: this.width_, height: this.height_, onTextChange:(value:string)=>{\r\n         //TextInput值改变后，通知js侧，这里只是列举了一个简单的例子，以实际情况执行js代码\r\n         let jsFun:string = \"setValue('\"+value+\"')\";\r\n         try {\r\n           this.cordovaWebView?.getWebviewController().runJavaScript(jsFun);\r\n         } catch (error) {\r\n           console.log(error);\r\n         }\r\n        }})\r\n        return this.rootNode.getFrameNode();\r\n      } else {\r\n        return null;\r\n      }\r\n    }\r\n    return this.rootNode.getFrameNode();\r\n  }\r\n\r\n  updateNode(arg: Object): void {\r\n    this.rootNode?.update(arg);\r\n  }\r\n\r\n  getEmbedId(): string {\r\n    return this.embedId_;\r\n  }\r\n\r\n  setDestroy(isDestroy: boolean): void {\r\n    this.isDestroy_ = isDestroy;\r\n    if (this.isDestroy_) {\r\n      this.rootNode = null;\r\n    }\r\n  }\r\n\r\n  postEvent(event: TouchEvent | undefined): boolean {\r\n    return this.rootNode?.postTouchEvent(event) as boolean\r\n  }\r\n}\r\n```\r\n\r\n```typescript\r\n@Entry\r\n@Component\r\nexport struct Index {\r\n    //省略其它代码\r\n    public nodeControllerMap: Map<string, MyNodeController> = new Map();\r\n    @State componentIdArr: Array<string> = [];\r\n    @State widthMap: Map<string, number> = new Map();\r\n    @State heightMap: Map<string, number> = new Map();\r\n    @State positionMap: Map<string, Edges> = new Map();\r\n    @State edges: Edges = {};\r\n    @State textValue:string = \"hello\";\r\n    /*\r\n    *同层渲染生命周期回调函数\r\n    */\r\n    onNativeEmbedLifecycleChange(embed: NativeEmbedDataInfo,cordovaWebView:CordovaWebView,parentPage?:object) {\r\n      let page = parentPage as Index;//page为当前页面对象，相当于当前页面的this指针，使用该对象，必须将this指针传入到mainPage中\r\n      console.log(\"NativeEmbed surfaceId\" + embed.surfaceId);\r\n      // 如果使用embed.info.id作为映射nodeController的key，请在h5页面显式指定id\r\n      const componentId = embed.info?.id?.toString() as string\r\n      if (embed.status == NativeEmbedStatus.CREATE) {\r\n        console.log(\"NativeEmbed create\" + JSON.stringify(embed.info));\r\n        // 创建节点控制器、设置参数并rebuild\r\n        let nodeController = new MyNodeController()\r\n        // embed.info.width和embed.info.height单位是px格式，需要转换成ets侧的默认单位vp\r\n        nodeController.setRenderOption({\r\n          surfaceId : embed.surfaceId as string,\r\n          type : embed.info?.type as string,\r\n          renderType : NodeRenderType.RENDER_TYPE_TEXTURE,\r\n          embedId : embed.embedId as string,\r\n          width : cordovaWebView.getUIContext().px2vp(embed.info?.width),\r\n          height : cordovaWebView.getUIContext().px2vp(embed.info?.height),\r\n          cordovaWebView:cordovaWebView,\r\n          textValue:page.textValue\r\n        })\r\n        page.edges = {left: `${embed.info?.position?.x as number}px`, top: `${embed.info?.position?.y as number}px`}\r\n        nodeController.setDestroy(false);\r\n        //根据web传入的embed的id属性作为key，将nodeController存入Map\r\n        page.nodeControllerMap.set(componentId, nodeController);\r\n        page.widthMap.set(componentId, cordovaWebView.getUIContext().px2vp(embed.info?.width));\r\n        page.heightMap.set(componentId, cordovaWebView.getUIContext().px2vp(embed.info?.height));\r\n        page.positionMap.set(componentId, page.edges);\r\n        // 将web传入的embed的id属性存入@State状态数组变量中，用于动态创建nodeContainer节点容器，需要将push动作放在set之后\r\n        page.componentIdArr.push(componentId)\r\n      } else if (embed.status == NativeEmbedStatus.UPDATE) {\r\n        let nodeController = page.nodeControllerMap.get(componentId);\r\n        console.log(\"NativeEmbed update\" + JSON.stringify(embed));\r\n        page.edges = {left: `${embed.info?.position?.x as number}px`, top: `${embed.info?.position?.y as number}px`}\r\n        page.positionMap.set(componentId, page.edges);\r\n        page.widthMap.set(componentId, cordovaWebView.getUIContext().px2vp(embed.info?.width));\r\n        page.heightMap.set(componentId, cordovaWebView.getUIContext().px2vp(embed.info?.height));\r\n        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);\r\n      } else if (embed.status == NativeEmbedStatus.DESTROY) {\r\n        console.log(\"NativeEmbed destroy\" + JSON.stringify(embed));\r\n        let nodeController = page.nodeControllerMap.get(componentId);\r\n        nodeController?.setDestroy(true)\r\n        page.nodeControllerMap.clear();\r\n        page.positionMap.delete(componentId);\r\n        page.widthMap.delete(componentId);\r\n        page.heightMap.delete(componentId);\r\n        page.componentIdArr.filter((value: string) => value != componentId)\r\n      } else {\r\n        console.log(\"NativeEmbed status\" + embed.status);\r\n      }\r\n    }\r\n\r\n    onNativeEmbedGestureEvent(touch: NativeEmbedTouchInfo,cordovaWebView:CordovaWebView,parentPage?:object) {\r\n      let page = parentPage as Index;//page为当前页面对象，相当于当前页面的this指针，使用该对象，必须将this指针传入到mainPage中\r\n      console.log(\"NativeEmbed onNativeEmbedGestureEvent\" + JSON.stringify(touch.touchEvent));\r\n      page.componentIdArr.forEach((componentId: string) => {\r\n        let nodeController = page.nodeControllerMap.get(componentId);\r\n        // 将获取到的同层区域的事件发送到该区域embedId对应的nodeController上\r\n        if(nodeController?.getEmbedId() == touch.embedId) {\r\n          let ret = nodeController?.postEvent(touch.touchEvent)\r\n          if(ret) {\r\n            console.log(\"onNativeEmbedGestureEvent success \" + componentId);\r\n          } else {\r\n            console.log(\"onNativeEmbedGestureEvent fail \" + componentId);\r\n          }\r\n          if(touch.result) {\r\n            // 通知Web组件手势事件消费结果\r\n            touch.result.setGestureEventResult(ret);\r\n          }\r\n        }\r\n      })\r\n    }\r\n\r\n    /*\r\n    *同层渲染的TextInput文本改变后回调该函数\r\n    *可以通过自定义插件获取改变后的值\r\n    */\r\n    onTextChangeCallBack(page:Index, value:string) {\r\n      page.textValue = value;\r\n    }\r\n    \r\n    getTextValue():string {\r\n      return this.textValue;\r\n    }\r\n    \r\n    /*\r\n    *设置同层渲染TextInput的显示文本\r\n    *可以通过自定义插件设置TextInput的显示文本\r\n    */\r\n    setNativeValue(id:string, value:string){\r\n      this.textValue = value;\r\n      let nodeController = this.nodeControllerMap.get(id);\r\n      nodeController?.updateNode({page: this, textOne: 'update', width: this.widthMap.get(id), height: this.heightMap.get(id), text: this.textValue, onTextChange:this.onTextChangeCallBack} as ESObject)\r\n    }\r\n\r\n  RelativeContainer() {\r\n      //同层渲染\r\n      ForEach(this.componentIdArr, (componentId: string) => {\r\n        NodeContainer(this.nodeControllerMap.get(componentId))\r\n          .position(this.positionMap.get(componentId))\r\n          .width(this.widthMap.get(componentId))\r\n          .height(this.heightMap.get(componentId))\r\n      }, (embedId: string) => embedId)\r\n      /*\r\n       *nativeEmbedHtmlTag:注册同层渲染标签\r\n       *     默认是：<embed>的标签，如果要注册object，请传入object，同层渲染只支持这两个标签，可以直接保持默认\r\n       *nativeEmbedHtmlType:注册同层选择标签类型\r\n       *     默认是：native类型，如要要传入其它类型，请随意取名字\r\n       *onNativeEmbedLifecycleChange:同层渲染元素生命周期函数\r\n       *onNativeEmbedGestureEvent:同层渲染手势回调函数\r\n       *    同层渲染参考连接：https://docs.openharmony.cn/pages/v6.0/zh-cn/application-dev/web/web-same-layer.md\r\n       */\r\n      MainPage({\r\n        parentPage: this,\r\n        onNativeEmbedLifecycleChange: this.onNativeEmbedLifecycleChange,\r\n        onNativeEmbedGestureEvent: this.onNativeEmbedGestureEvent\r\n      });\r\n    }\r\n}  \r\n```\r\n\r\n### 15. 键盘避让模式\r\n\r\n```typescript\r\n//webKeyboardAvoidMode:避让键盘模式，默认：WebKeyboardAvoidMode.RESIZE_VISUAL\r\nMainPage({webKeyboardAvoidMode:WebKeyboardAvoidMode.RESIZE_VISUAL})\r\n```\r\n\r\n### 16. 自定义 http 头\r\n\r\n```typescript\r\n/*\r\n*     customHttpHeaders:自定义http头\r\n*     前端withCredentials为true时添加自定义http头，没有自定义http头不用添加\r\n*     参考常见问题的跨域说明\r\n*isAllowCredentials:默认是false\r\n*     前端请求设置withCredentials为true，要传入参数isAllowCredentials:true\r\n*     参考常见问题的跨域说明\r\n*/\r\nMainPage({customHttpHeaders:\"X-AUTH\", isAllowCredentials:true})\r\n```\r\n\r\n### 17. 传入当前页面对象 parentPage，在 mainPage 的生命周期函数中可以引用当前页面的变量\r\n\r\n```typescript\r\n//parentPage:传入this，就是webview父组件对象，也就是当前组件的对象，可以在插件里面调用\r\nMainPage({parentPage:this})\r\n```\r\n\r\n## 热更新\r\n\r\ncapacitor 原框架官方不支持热更新，OpenHarmony 化框架基于 Cordova 官方热更新插件功能，改造成适配 capacitor 框架，框架自带，无需安装。\r\n\r\n### 前置条件\r\n\r\n在服务器上放置两个文件（通过 cordova-hot-code-push-cli 命令生成，参考：[https://www.npmjs.com/package/cordova-hot-code-push-cli](https://www.npmjs.com/package/cordova-hot-code-push-cli)）：\r\n\r\n#### 安装 CLI 工具（全局）\r\n\r\n```\r\nnpm install -g cordova-hot-code-push-cli\r\n```\r\n\r\n#### 在项目根目录执行\r\n\r\n```\r\nchcp init\r\n```\r\n\r\n#### 根据提示输入更新服务器地址（如 https://www.example.com/chcp）\r\n\r\n#### 构建本地 Web 资源，生成 chcp.json 与 chcp.manifest\r\n\r\n```\r\nchcp build\r\n```\r\n\r\n在 chcp 目录下：\r\n\r\n- **chcp.json**：定义版本、更新内容 URL 等。\r\n- **chcp.manifest**：列出所有文件及其哈希值（用于校验）。\r\n- **www** 目录：放置结构与工程中 rawfile/www 中的更新文件，需要与原结构保持一致。\r\n\r\n### 基本配置步骤\r\n\r\n1. 修改 capacitor.config.json，增加如下配置。\r\n\r\n```json\r\n{\r\n\t\"plugins\": {\r\n\t\t\"chcp\": {\r\n\t\t\t\"auto-download\": true,\r\n\t\t\t\"auto-install\": true,\r\n\t\t\t\"config-file\": \"http://www.example.com/chcp/chcp.json\"\r\n\t\t}\r\n\t}\r\n}\r\n```\r\n\r\n2. chcp.json 配置文件示例\r\n\r\n该文件在本地 rawfile/www 目录下存放一份，然后在服务器存储一份。\r\n\r\n- **release:** 版本号，chcp 会判断该版本号和本地版本号比较，判断是否要更新；\r\n- **content_url:** 更新文件的存储位置；\r\n- **其它参数:** 其它参数 chcp 暂不使用；\r\n\r\n```json\r\n{\r\n  \"name\": \"capacitor\",\r\n  \"autogenerated\": true,\r\n  \"update\": \"now\",\r\n  \"min_native_interface\": 1,\r\n  \"content_url\": \"http://www.example.com/chcp/www\",\r\n  \"release\": \"2025.03.05-16.47.30\"\r\n}\r\n```\r\n\r\n3. chcp.manifest 配置文件\r\n\r\n该文件在本地 rawfile/www 目录下存放一份，然后在服务器存储一份，服务端存储在 chcp.json 项目目录内。\r\n\r\n```json\r\n[\r\n  {\r\n    \"file\": \"assets/icon/favicon.png\",\r\n    \"hash\": \"988be98f12b400c41a22b59b82cfeab1\"\r\n  }\r\n]\r\n```\r\n\r\n4. js 代码部分\r\n\r\n在本地工程中调用以下代码，实现热更新功能。\r\n\r\n```javascript\r\nfunction chcpUpdate() {\r\n\t//配置新的更新地址，如果不传option，更新地址使用www/chcp.json配置的地址\r\n\twindow.Capacitor.Plugins.HotCodePushPlugin.fetchUpdate({\r\n\t\t\"config-file\":\"http://www.example.com/chcp/chcp.json\" // 服务端配置信息\r\n\t}).then(result => {\r\n\t\tif (result.action == 'chcp_updateIsReadyToInstall') {\r\n\t\t\tconsole.log('插件有更新');\r\n\t\t\t//检测到更新，更新成功后会自动重启app，每次更新间隔周期需大于1分钟\r\n\t\t\t//如果要进行测试，修改www/chcp.json的release版本号，修改www/chcp.manifest文件内，其中文件对应的md5值\r\n\t\t\twindow.Capacitor.Plugins.HotCodePushPlugin.installUpdate().then(result2 => {\r\n\t\t\t\tconsole.log('更新完成');\r\n\t\t\t});\r\n\t\t}\r\n\t\telse {\r\n\t\t\tconsole.log('插件无更新');\r\n\t\t}\r\n\t});\r\n}\r\n```\r\n\r\n5. 更新流程\r\n\r\n应用启动 → 检查服务器 chcp.json → 对比版本 → 下载差异文件 → 安装更新。\r\n\r\n## 自定义 ArkTS 插件研发\r\n\r\n自定义 ArkTS 插件研发复用了 cordova-openharmony 能力进行实现，自定义插件接口遵守 capacitor sdk 官方规范，以自定义插件 TestPlugin 为例：\r\n\r\n### 1. 新建 ArkTS 文件\r\n\r\n新建 ArkTS 文件，命名为 TestCapPlugin，示例代码如下。\r\n\r\n```typescript\r\nimport { CapacitorPlugin, PluginCall, NormalizeError, PluginMethod } from 'openHarmony-capacitor';\r\n\r\nexport class TestCapPlugin extends CapacitorPlugin {\r\n  constructor() {\r\n    super();\r\n    try {\r\n      this.registerMethod(\"testMethod\", (call: PluginCall) => {\r\n        this.testMethod(call);\r\n      });\r\n      this.registerMethod(\"notifyEvent\", (call: PluginCall) => {\r\n        this.notifyEvent(call);\r\n      });\r\n      this.registerMethod(\"removeEvent\", (call: PluginCall) => {\r\n        this.removeEvent(call);\r\n      });\r\n      this.registerPermission([\"ohos.permission.LOCATION\"]);\r\n    } catch (error) {\r\n      const cordovaError = NormalizeError(error);\r\n      console.error(`Failed to onWatchIndexPageUpdate. Cause code: ${cordovaError.code}, message: ${cordovaError.message}`);\r\n    }\r\n  }\r\n\r\n  async testMethod(call: PluginCall): Promise<void> {\r\n    let ret:object = new Object();\r\n    ret[\"time\"] = new Date().getTime().toString();\r\n    call.resolve(ret);\r\n  }\r\n\r\n  notifyEvent(call:PluginCall):void {\r\n    if(this.hasListeners(\"onDataReceived\")) {\r\n      let obj:object = new Object;\r\n      obj[\"name\"] = \"value\";\r\n      this.notifyListeners(\"onDataReceived\", obj);\r\n    }\r\n\r\n    let ret:object = new Object;\r\n    ret[\"return\"] = \"success\";\r\n    call.resolve(ret);\r\n  }\r\n\r\n  removeEvent(call:PluginCall):void {\r\n    this.removeEventListener(\"onDataReceived\", call);\r\n  }\r\n\r\n  handleOnStart():void{\r\n    console.log(\"handleOnStart\");\r\n  }\r\n  \r\n  handleOnPageStart():void {\r\n    console.log(\"handleOnPageStart\");\r\n  }\r\n  \r\n  handleOnEnd():void{\r\n    console.log(\"handleOnEnd\");\r\n  }\r\n\r\n  handleOnResume():void{\r\n    console.log(\"handleOnResume\");\r\n  }\r\n\r\n  handleOnPause():void{\r\n    console.log(\"handleOnPause\");\r\n  }\r\n\r\n  handleOnDestroy():void{\r\n    console.log(\"handleOnDestroy\");\r\n  }\r\n}\r\n```\r\n\r\n### 2. 插件的配置\r\n\r\nArkTS 侧插件写好以后，在 entry/src/main/ets/pages/index.ets 文件中配置：\r\n\r\n```typescript\r\nimport { MainPage, pageBackPress, pageHideEvent, pageShowEvent, PluginEntry} from 'openHarmony-capacitor';\r\nimport { TestCapPlugin } from '../plugins/TestCapPlugin';//引入插件\r\nstruct Index {\r\n    /*\r\n    *ArkTs侧的自定义插件键值对：插件名称和实现对象，自定义插件开发，请查看自定义开发部分\r\n    *如果一个插件传入多个MainPage，务必单独定义对象传入，不可多MainPage使用一个对象，否则会使窗口操作串联\r\n    */\r\n  capacitorPlugins:Array<PluginEntry> = [\r\n    {\r\n      pluginName:\"TestCapPlugin\",\r\n      pluginObject:new TestCapPlugin()\r\n    }\r\n  ]\r\n\r\n    //省略其它代码\r\n \r\n    build() {\r\n        RelativeContainer() {\r\n          //isWebDebug:工具调试开关，capacitorPlugins：自定义插件列表，启动首页index.html\r\n          MainPage({isWebDebug:false,capacitorPlugins:this.capacitorPlugins}); \r\n        }\r\n        .height('50%')\r\n        .width('100%')\r\n\r\n        RelativeContainer() {\r\n          //isWebDebug:工具调试开关，capacitorPlugins：自定义插件列表，指定加载rawfile资源目录下文件\r\n          MainPage({isWebDebug:false,indexPage:\"/www2/index.html\", capacitorPlugins:this.capacitorPlugins}); \r\n        }\r\n        .height('50%')\r\n        .width('100%')\r\n    }\r\n}\r\n```\r\n\r\n### 3. JS 侧插件调用\r\n\r\nJS 侧插件调用完全遵守 capacitor 官方调用规范：\r\n\r\n直接调用，无需做任何配置，代码如下：\r\n\r\n```javascript\r\nlet data = await window.Capacitor.Plugins.TestCapPlugin.testMethod({\r\n    message: 'Exec testMethod'\r\n})\r\n```\r\n\r\n### 4. 自定义插件实现原理简述\r\n\r\n由于 OpenHarmony 提供 ArkTS 和 C/C++ API，capacitor sdk 是使用 C/C++ 研发，自定义插件是跨语言调用，调用顺序为：JS 侧 → C/C++ 侧 → ArkTS 侧，回调是相反顺序，不过 ArkTS 侧的插件也可以直接调用 JS 侧。自定义插件的研发根据具体实现的功能，可以选择使用 ArkTS 开发，也可选择 C/C++ 开发。\r\n\r\n## 自定义 C++ 插件研发\r\n\r\n研发自定义 C++ 侧插件，您可以参考已移植的 capacitor 官方插件，编写 C++ 侧插件。\r\n\r\n### 1. 开发步骤\r\n\r\n1. 在源码集成的 capacitor 工程中，在源码的 CPP 目录内新建一个插件目录，保存您的自定义插件；\r\n2. 在新目录中新建一个 class，该 class 要继承 Plugin 类，同时新建对应插件的 CMakeLists.txt；\r\n3. 在您的 CPP 文件中，添加 `REGISTER_CAP_PLUGIN()` 注册您的插件名称，如 CapacitorPlugin；用于实例化您的插件对象；\r\n4. 在您的 CPP 文件中，添加 `REGISTER_PLUGIN_METHOD()` 注册您的插件方法，如 PluginHello；用于实现您的插件功能；\r\n5. 如果您的插件中需要调用 ArkTS 侧的代码，需要调用 `executeArkTs`（同步）或者 `executeArkTsAsync`（异步）执行 ArkTS 侧代码，参数说明参考 Plugin 类注释说明，ArkTS 实现文件需要在 capacitor 模块的 build-profile.json5 下的 buildOption → arkOptions → runtimeOnly → sources 下导入该文件；\r\n6. 如果您的 ArkTS 侧需要把执行结果通知到 C++ 侧的插件，在 ArkTS 侧需要调用 `onArkTsResult` 函数通知 C++ 侧，C++ 侧的插件也要注册和实现 `onArkTsResult` 这个函数；\r\n7. 在完成您的插件研发后需要将您的 cpp 文件添加到 CMakeLists.txt 中，完成编译；\r\n\r\n### 2. 配置\r\n\r\n在 rawfile/capacitor.plugins.json 文件中，添加插件名和 c++ 插件实现类名。\r\n\r\n```json\r\n[\r\n  {\r\n    \"pkg\": \"@capacitor/CapacitorPlugin\",\r\n    \"classpath\": \"CapacitorPlugin\"\r\n  }\r\n]\r\n```\r\n\r\n### 3. JS 调用\r\n\r\n```javascript\r\nconst result = await window.Capacitor.Plugins.CapacitorPlugin.PluginHello({\r\n    message: 'Exec PluginHello'\r\n});\r\n```\r\n\r\n## Web 加载性能优化\r\n\r\n### 1. 预启动 web 和预渲染\r\n\r\n在应用启动后，在 EntryAbility 代码中，后台启动 web 引擎，并在后台渲染页面，进入 page 页面后，页面秒开，关闭页面后，页面进入后台，不会销毁 web，下次打开仍可秒开；需提醒的是，在使用 capacitor 的页面预渲染时，会初始化 capacitor 插件，有可能会出现在用户没有同意隐私政策前，初始化插件会访问系统资源。\r\n\r\n该功能需要对 mainPage 的组件进行二次封装，自己可以根据需要修改代码，如需技术支持请联系本开发者，提供封装方法和源码如下：\r\n\r\n参考链接：[https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-web-develop-optimization](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-web-develop-optimization)\r\n\r\n**1. 在 pages 中新建 ArkTS 文件，命名为 WebBuilder.ets，复制以下代码：**\r\n\r\n```typescript\r\nimport { MainPage, MainPageCycle, PluginEntry } from 'openHarmony-capacitor';\r\nimport { BuilderNode, FrameNode, NodeController } from '@kit.ArkUI';\r\nimport { webview } from '@kit.ArkWeb';\r\nimport { TestPlugin } from '../plugins/TestPlugin';\r\n\r\n//根据需要扩展参数，参数参考MainPage的参数\r\nclass DataParameters{\r\n  url?: string;\r\n  mainPageCycle?:MainPageCycle;\r\n  mainPagePageNodeController?:MainPagePageNodeController;\r\n  cordovaPlugs?:Array< PluginEntry>;\r\n}\r\n\r\n@Builder\r\nfunction buildMainPage(data:DataParameters) {\r\n  Column() {\r\n    MainPage({indexPage:data.url, lifeCycle:data.mainPageCycle, parentPage:data.mainPagePageNodeController,cordovaPlugs:data.cordovaPlugs});\r\n  }.width(\"100%\").height(\"100%\")\r\n}\r\n\r\nlet wrap = wrapBuilder< DataParameters[]>(buildMainPage);\r\n\r\nclass MainPagePageNodeController extends NodeController {\r\n  private rootNode: BuilderNode< DataParameters[]> | null = null;\r\n  private root: FrameNode | null = null;\r\n  private cordovaPlugs:Array< PluginEntry> = [\r\n      {\r\n        pluginName: 'TestPlugin', //插件名称\r\n        pluginObject:new TestPlugin() //实例化插件对象\r\n      }\r\n  ];\r\n  private mainPageCycle:MainPageCycle = new MainPageCycle().setOnAboutToAppear((webviewController: webview.WebviewController,parentPage?:object)=>{\r\n    let page = parentPage as MainPagePageNodeController;//page为当前页面对象，相当于当前页面的this指针，使用该对象，必须将this指针传入到mainPage中\r\n    console.log(\"exec onAboutToAppear\");\r\n  });\r\n\r\n  constructor() {\r\n    super();\r\n  }\r\n\r\n  makeNode(uiContext: UIContext): FrameNode | null {\r\n    if (this.rootNode != null) {\r\n      const parent = this.rootNode.getFrameNode()?.getParent();\r\n      if (parent) {\r\n        console.info(JSON.stringify(parent.getInspectorInfo()));\r\n        parent.removeChild(this.rootNode.getFrameNode());\r\n        this.root = null;\r\n      }\r\n      this.root = new FrameNode(uiContext);\r\n      this.root.appendChild(this.rootNode.getFrameNode());\r\n      return this.root;\r\n    }\r\n    return null;\r\n  }\r\n\r\n  initWeb(url:string, uiContext:UIContext) {\r\n    if(this.rootNode != null) {\r\n      return;\r\n    }\r\n    this.rootNode = new BuilderNode(uiContext);\r\n    //可以根据不同的页面传入不同的参数，单页面视图不存在这种情况，需要技术支持联系本开发者\r\n    if(url === \"/www3/index.html\") {\r\n      this.rootNode.build(wrap, {url:url, mainPageCycle:this.mainPageCycle,mainPagePageNodeController:this, cordovaPlugs:this.cordovaPlugs});\r\n    } else {\r\n      this.rootNode.build(wrap, {url:url});\r\n    }\r\n  }\r\n}\r\n\r\nlet NodeMap:Map< string, MainPagePageNodeController | undefined> = new Map();\r\n\r\nexport const createNWeb = (url: string, uiContext: UIContext) : MainPagePageNodeController | undefined => {\r\n  let baseNode = new MainPagePageNodeController();\r\n  baseNode.initWeb(url, uiContext);\r\n  NodeMap.set(url, baseNode);\r\n  return baseNode;\r\n}\r\n\r\nexport const getNWeb = (url : string, uiContext:UIContext) : MainPagePageNodeController | undefined => {\r\n  if(NodeMap.has(url)) {\r\n    return NodeMap.get(url);\r\n  } else {\r\n    return createNWeb(url, uiContext);\r\n  }\r\n}\r\n```\r\n\r\n**2. 修改 EntryAbility.ets，添加预启动 web 和预渲染代码：**\r\n\r\n```typescript\r\n//省略了其它代码\r\nonWindowStageCreate(windowStage: window.WindowStage): void {\r\n    windowStage.loadContent('pages/Splash', (err) => {\r\n      //启动预启动web和预渲染，多页面视图可以预选设置和初始化\r\n      createNWeb('/www3/index.html', windowStage.getMainWindowSync().getUIContext());\r\n      createNWeb('/www3/index2.html', windowStage.getMainWindowSync().getUIContext());\r\n      createNWeb('/www3/index3.html', windowStage.getMainWindowSync().getUIContext());\r\n    });\r\n}\r\n```\r\n\r\n**3. 修改 Index.ets 启动 capacitor 封装的 mainPage 页面，此时秒开，效率和传统打开 mainPage 相比大大提高：**\r\n\r\n```typescript\r\nbuild() {\r\n    Column() {\r\n      RelativeContainer() {\r\n        NodeContainer(getNWeb('/www3/index.html', this.getUIContext()))\r\n          .height('100%')\r\n          .width('100%')\r\n      }\r\n      .height('100%')\r\n      .width('100%')\r\n\t}\r\n}\r\n```\r\n\r\n### 2. 资源拦截替换的 JavaScript 生成字节码缓存（Code Cache）\r\n\r\n使用 capacitor 框架，根据 capacitor 的标准，所有页面和 JS 文件都在本地，openHarmony-capacitor 内部已经使用了拦截和替换功能，如果您加载的是在线资源或者 JS 文件，并且强制使用了 capacitor 协议栈（通过 capacitor.config.json 配置或者 SetCordovaProtocolUrl 函数设置），capacitor 框架也进行了资源缓存，如果您加载的是在线页面，使用 webview 的协议栈，可以结合 MainPage 提供的生命周期函数 onInterceptWebRequest 进行拦截，对于在线的 js 文件，也可以直接打包到本地的沙箱目录下，通过 capacitor 提供的 SetResourceReplace 函数进行拦截替换，以提供加载页面速度。示例代码如下：\r\n\r\n参考链接：[https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-web-develop-optimization#section172031338172719](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-web-develop-optimization#section172031338172719)\r\n\r\n```typescript\r\n//省略有其它代码，以下是js预编译示例代码\r\nconfigs: Array< Config> = [\r\n{\r\n  url: 'https://www.tongecn.com/example.js',\r\n  localPath: 'example.js',//文件在rawfile目录下\r\n  options: {\r\n    responseHeaders: [\r\n      { headerKey: 'E-Tag', headerValue: 'xxx' },\r\n      { headerKey: 'Last-Modified', headerValue: 'Web, 21 Mar 2024 10:38:41 GMT' }\r\n    ]\r\n  }\r\n}]\r\n\r\nmainPageCycle = new MainPageCycle().setOnControllerAttached((webviewController: webview.WebviewController,parentPage?:object)=>{\r\n  console.log(\"exec onControllerAttached\");\r\n  for (const config of this.configs) {\r\n      let content = await this.getUIContext().getHostContext()?.resourceManager.getRawFileContentSync(config.localPath);\r\n      try {\r\n        this.controller.precompileJavaScript(config.url, content, config.options)\r\n          .then((errCode: number) => {\r\n            console.log('precompile successfully!' );\r\n          }).catch((errCode: number) => {\r\n          console.error('precompile failed.' + errCode);\r\n        })\r\n      } catch (err) {\r\n        console.error('precompile failed!.' + err.code + err.message);\r\n      }\r\n  }\r\n})\r\n\r\n//省略其它代码，以下是拦截替换\r\nonInterceptWebRequest(request: WebResourceRequest, webTag:string):ESObject {\r\n    // webview内核处理替换\r\n    if(url == \"https://www.tongecn.com/v1.1.1/temp/test3.js\") {\r\n      //替换本地沙箱路径\r\n      SetResourceReplace(webTag, url, \"https://localhost/data/storage/el2/base/files/test.js\");\r\n      //替换本地rawfile文件\r\n      //SetResourceReplace(webTag, url, \"https://www.example.com/test.js\");\r\n    }\r\n    return null;\r\n}\r\n\r\n//省略有其它代码\r\nMainPage({isWebDebug:true, indexPage:\"https://www.tongecn.com\", lifeCycle:data.mainPageCycle, parentPage:this,onInterceptWebRequest:this.onInterceptWebRequest});\r\n```\r\n\r\n## 常见问题\r\n\r\n### 1. 返回键不起作用\r\n\r\nOpenHarmony 返回键不起作用，就是手势事件，从左往右快速滑动，app 不返回上一页面，或者到了顶层页面不退出应用。\r\n\r\n不同的框架有不同的处理方式，如果不管使用的是什么框架，只在 capacitor 层处理的，需要监听返回键事件，代码如下：\r\n\r\n```javascript\r\ndocument.addEventListener(\"deviceready\", onDeviceReady, false);\r\nfunction onDeviceReady() {\r\n    document.addEventListener(\"backbutton\", onBackKeyDown, false);\r\n}\r\n\r\nfunction onBackKeyDown() {\r\n    //自己处理返回\r\n}\r\n```\r\n\r\n如果采用 ionic angularjs 框架，可以采用如下代码：\r\n\r\n```javascript\r\nfunction showConfirm() {\r\n   //处理退出应用的逻辑\r\n}\r\n$ionicPlatform.registerBackButtonAction(function (e) {\r\n// Is there a page to go back to?\r\n  if ($location.path() == '/tab/message') { //到了顶层页面，/tab/message是顶层页面的路由，这里只是举个例子，实际情况根据您的项目设置\r\n      showConfirm();\r\n      return false\r\n  } else if ($ionicHistory.backView()) {\r\n      // Go back in history\r\n      $ionicHistory.goBack(); //自己处理返回\r\n  } else {\r\n      // This is the last page: Show confirmation popup\r\n      showConfirm();\r\n      return false;\r\n  }\r\n  e.preventDefault();\r\n  return false;\r\n}, 101);\r\n```\r\n\r\n说明：无论采用什么框架都可以在 capacitor 层通过监听 backbutton 返回事件自己处理。\r\n\r\n如果加载的页面不包含相关 js，需要传入控制 webview 路由 MainPageOnBackPress 对象控制返回。\r\n\r\n### 2. 如何访问沙箱资源文件\r\n\r\n采用 `cdvfile://` 访问沙箱文件，以 downloadImage.png 为例：\r\n\r\n```\r\ncdvfile:///data/storage/el2/base/files/chuzhitong/downloadedImage.png\r\n```\r\n\r\n如果是 `file://` 作为 MainPage 的入口页，也可以使用 `file://` 协议访问本地文件，沙箱资源文件可以是图片（png, jpg, svg 等）、js、html 等，请参考最后的 file:// 协议说明。\r\n\r\n### 3. HTTP 协议的 cookie 说明\r\n\r\n如果您使用 http 协议非 https 协议，请参考如下 cookie 说明：\r\n\r\n**（1）同源请求：**\r\n\r\n例如您是直接在 MainPage 传入网址例如传入 `http://www.tongecn.com`，capacitor 会自动处理 cookie，无需手动处理。\r\n\r\n**（2）跨域请求：**\r\n\r\n例如您加载的文件在沙箱路径或者 rawfile 目录下的文件，在 html 文件中使用的 http 发送的 GET/POST 请求，此时需要再在 capacitor.config.json 里面配置 http 请求的域名，以便以 capacitor 为 http 处理 cookie，配置如下：\r\n\r\n```json\r\n{\r\n  \"harmony\": {\r\n    \"cordova-protocol-force\": [\"***.***.com\"]\r\n  }\r\n}\r\n```\r\n\r\n在 ArkTS 侧运行态动态设置 http 的 cookie，http 的 GET/POST 自动携带 cookie，capacitor 不处理静态资源，静态资源有 webview 处理：\r\n\r\n```typescript\r\n//在ArkTs侧运行态动态设置http的cookie,http的GET/POST自动携带cookie，capacitor不处理静态资源，静态资源有webview处理\r\naboutToAppear() {\r\n    SetCordovaProtocolUrl(\"***.****.com\");\r\n}\r\n```\r\n\r\n**（3）HTTPS 协议：**\r\n\r\n您发送的请求是 https 协议，非 http 协议，capacitor 会自动处理 cookie，无需手动处理。\r\n\r\n**（4）手动设置 cookie：**\r\n\r\n如果您要在 ArkTS 侧运行态手动设置 cookie，请参考不常用的高级功能部分。\r\n\r\n### 4. 虚拟域名 www.example.com、自定义域名、localhost、file 协议和 cdvfile 协议的详细说明\r\n\r\n加载 rawfile 目录下的页面时，通过 DevTools 工具测试时或者在日志 log 中会看到 `https://www.example.com` 的域名，可能会感到疑虑或者惊慌，接下来详细介绍一下，为什么使用此域名：\r\n\r\n- OpenHarmony 无法使用 file 协议直接加载 rawfile 目录的文件，因此使用 www.example.com 虚拟域名代替 file 协议，因此您看到 `https://www.example.com` 就理解为 `file://` 即可。\r\n- 在 capacitor.config.json 里面 harmony 属性添加 `\"Hostname\": \"app.com\"` 使用自定义域名加载本地文件。\r\n- 使用 `cdvfile://` 协议加载沙箱目录文件。\r\n\r\n说明：如果您的 h5 程序中有使用 file:// 协议，在 MainPage 中就必须使用 file:// 协议进入首页，否则 file 协议无法加载本地文件，capacitor 完全支持 file:// 协议加载文件，无论是从资源文件夹加载还是从沙箱路径加载 OpenHarmony capacitor 完全支持。\r\n\r\n### 5. 屏蔽跟随系统字体大小\r\n\r\n- 在 app.json5 中增加 configuration 选项以屏蔽跟随系统字体大小，具体配置方法参考：[https://docs.openharmony.cn/pages/v6.0/zh-cn/application-dev/quick-start/app-configuration-file.md](https://docs.openharmony.cn/pages/v6.0/zh-cn/application-dev/quick-start/app-configuration-file.md)\r\n- 在 EntryAbility 的 onWindowStageCreate 函数中增加 `windowStage.setDefaultDensityEnabled(true);` 屏蔽跟随显示大小缩放，参考：[https://docs.openharmony.cn/pages/v5.0.3/zh-cn/application-dev/reference/apis-arkui/js-apis-window.md](https://docs.openharmony.cn/pages/v5.0.3/zh-cn/application-dev/reference/apis-arkui/js-apis-window.md)\r\n\r\n### 6. 跨域错误\r\n\r\ncapacitor 已经解决了所有的跨域访问，同时会自动携带 cookie，并兼容所有的自定义 http 头，无需做任何配置，但是前端 withCredentials 设置为 true 时，需要相应的配置解决跨域。\r\n\r\n在前端发送 POST、GET 请求，withCredentials 为 true 时，同时您的服务器依赖于自定义 http 头例如 X-Auth-Token、Test-Type，mainPage 要传入相应的参数如下：\r\n\r\n```typescript\r\nMainPage({customHttpHeaders:\"X-Auth-Token,Test-Type\",isAllowCredentials:true})\r\n```\r\n\r\n**如果没有设置会报跨域错误：**\r\n\r\n```\r\nAccess 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.\r\n```\r\n\r\n**解决方法**：在 mainPage 增加参数 `isAllowCredentials:true`\r\n\r\n```typescript\r\nMainPage({isAllowCredentials:true})\r\n```\r\n\r\n```\r\nAccess 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\r\n```\r\n\r\n**解决方法**：在 mainPage 增加自定义头，例如自定义 http 头 X-Auth-Token、Test-Type\r\n\r\n```typescript\r\nMainPage({customHttpHeaders:\"X-Auth-Token,Test-Type\", isAllowCredentials:true})\r\n```\r\n\r\n### 7. iframe 跨域设置 cookie\r\n\r\n如果您使用的 iframe 加载了第三方页面，第三方页面直接使用 js 设置 cookie，并不是通过 http 头 Set-Cookie 设置 cookie 的，js 设置 cookie，一定要加上 `SameSite=None; Secure`，否则 iframe 会出现页面无法显示问题，因为请求 http 头不会自动携带 cookie，设置 cookie 示例如下：\r\n\r\n```javascript\r\ndocument.cookie = 'token=467d1510-xxxx-xxxx-xxxx-73852620effa1; path=/; SameSite=None; Secure';\r\n```\r\n\r\n如果第三方页面无法更改，请使用内置浏览器打开页面，或者直接使用 a 标签打开页面，a 标签在 OpenHarmony 的 capacitor 中会自动触发内置浏览器，Android 和 iOS 不具备该功能。示例代码如下：\r\n\r\n```javascript\r\n//内置浏览器打开，可以配置相关参数，需集成内置浏览器插件\r\nwindow.open(\"https://www.*****.com/index.html\", \"title=测试标题\");\r\n```\r\n\r\n```html\r\n<!--a标签打开，会自动触发内置浏览器-->\r\n<a href=\"https://www.*****.com/index.html\" target=\"_blank\">打开链接</a>\r\n```\r\n\r\n### 8. capacitor 内部缓存时长设置\r\n\r\n默认请求下使用 capacitor 的协议栈访问网络，静态资源缓存一天，即 `24 * 60 * 60` 秒钟，如果您想自己配置缓存时长在 capacitor.config.json 里面添加如下配置：\r\n\r\n```json\r\n{\r\n  \"harmony\": {\r\n    \"cordova-cache-duration\":60\r\n  }\r\n}\r\n```\r\n\r\n## 目录结构\r\n\r\n```\r\n目录根目录/\r\n└─src\r\n    ├─main\r\n    │  ├─cpp\r\n    │  │  ├─CoreHarmony // openharmony适配层\r\n    │  │  ├─getcapacitor // capacitor适配层\r\n    │  │  ├─HotCodePushPlugin // 热更新\r\n    │  │  ├─TsCordovaPlugin // arkts cordova插件\r\n    │  │  └─types\r\n    │  │      └─libcapacitor\r\n    │  ├─ets\r\n    │  │  └─components\r\n    │  │      ├─AlertDialog // 弹框\r\n    │  │      ├─CoreHarmony // openharmony适配层\r\n    │  │      ├─getcapacitor // capacitor适配层\r\n    │  │      ├─ImageCompress // 图片处理\r\n    │  │      ├─InAppBrowser // 内置浏览器\r\n    │  │      ├─Permission // 权限处理\r\n    │  │      ├─PluginAction // 插件工具类\r\n    │  │      └─SplashScreen // 闪屏\r\n    │  └─resources // 资源目录\r\n    ├─ohosTest // 测试目录\r\n    │  └─ets\r\n    │      └─test\r\n    └─test // 测试目录\r\n```\r\n\r\n## 贡献代码\r\n\r\n使用过程中发现任何问题都可以提 [Issue](https://gitcode.com/CPF-Ionic/openHarmony-capacitor/issues)，当然，也非常欢迎发 [PR](https://gitcode.com/CPF-Ionic/openHarmony-capacitor/pulls) 共建。\r\n\r\n## 许可证\r\n\r\n本插件基于 **MIT License** 开源，详见 [LICENSE](LICENSE) 文件。","readmeFilename":"README.md"}