HarmonyOS Cordova 混合架构与页面容器-Cordova 与 OpenHarmony 混合开发实战

本文对应项目模块:从
EntryAbility→Index.ets→MainPage→ ArkWeb → Cordova →rawfile/www/index.html的整条链路。
欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
目标:搞清楚「鸿蒙原生壳」是如何把前端页面托管起来的,以及 Cordova 在中间扮演什么角色。
1. 整体架构概览(文字版)
在当前这个财务管家项目中,你看到的 HTML/JS 界面,并不是直接由浏览器打开的,而是嵌在 HarmonyOS 的 ArkWeb 组件里面,由一个 ArkTS 写的壳来承载。大致链路可以拆解为几层:
- Ability 层:应用入口的
EntryAbility,负责加载首个 UI 界面。 - ArkTS 页面容器层:
Index.ets作为首页组件,只干两件事:- 建立 Cordova 插件列表(比如
FileManagerPlugin)。 - 渲染承载 Web 内容的
MainPage/CordovaWebView。
- 建立 Cordova 插件列表(比如
- Web 内核层(ArkWeb):ArkWeb 负责真正渲染 HTML/JS,是一个类似浏览器的内核。
- Cordova 桥接层:在 ArkWeb 里面注入
cordova.js,拦截cordova.exec调用,把 JS 的请求转成原生插件调用。 - 前端页面层:
rawfile/www/index.html + js/pages.js + js/db.js等,真正实现财务逻辑和 UI 的部分。
理解这条链路很重要,因为后面 30 篇文章几乎都是在这个架构之上「加模块」。
2. ArkTS 页面容器:Index.ets 中的 Cordova 插件注册
先看一下项目中 entry/src/main/ets/pages/Index.ets 的核心片段,它是整个应用 Web 部分的宿主:
import {
MainPage,
pageBackPress,
pageHideEvent,
pageShowEvent,
PluginEntry,
CordovaWebView,
MainPageCycle
} from '@magongshou/harmony-cordova/Index';
import { FileManagerPlugin } from '../plugins/FileManagerPlugin';
import { webview } from '@kit.ArkWeb';
@Entry
@Component
struct Index {
/**
* ArkTS 侧的自定义插件配置
* 配置插件名称和对象,详见自定义插件开发部分
*/
cordovaPlugs: Array<PluginEntry> = [
{ pluginName: 'FileManager', pluginObject: new FileManagerPlugin() }
];
aboutToAppear() {
console.info('[Index] aboutToAppear: Index页面即将出现,准备初始化Cordova环境');
console.info('[Index] aboutToAppear: Index仅负责承载MainPage与透传生命周期,不直接操控Web内核');
}
build() {
// 这里一般会渲染 MainPage 或 CordovaWebView
// 示例:
// MainPage({
// cordovaPlugs: this.cordovaPlugs
// })
}
}
2.1 这段 ArkTS 代码在做什么?(详细讲解)
这一段 ArkTS 代码可以看成是「原生壳」的首页容器,它有几个关键点:
- 引入 Cordova 核心类型:
PluginEntry:描述一个 Cordova 插件的配置,包含pluginName和pluginObject两个字段。MainPage/CordovaWebView:这是自定义库@magongshou/harmony-cordova暴露出来的页面组件,内部已经帮你把 ArkWeb + Cordova 初始化好了。
- 引入自定义插件
FileManagerPlugin:- 这一点非常关键,它说明当前项目已经不只是用 Web 层逻辑,而是确确实实接了 ArkTS 原生插件,用于数据导出 / 导入。
cordovaPlugs数组:- 这里就是告诉 Cordova 壳:「我这次要挂载哪些原生插件?」
pluginName: 'FileManager'这个名字必须和 JS 侧cordova.exec('FileManager', 'exportData', ...)里的第一个参数完全一致,否则调用会失败。
- 生命周期 aboutToAppear:
- 这里只是打日志,但思路很重要:ArkTS 容器本身不关心业务,只负责承载 Web 内容和初始化 Cordova 环境。
如果把这段 Struct 想象成 Android 里的 Activity,那 MainPage 就像一个 Fragment,专门用来放 WebView;而 cordovaPlugs 就像往一个 Service Locator 里注册可用的 Native 服务。
3. 原生插件入口:FileManagerPlugin.ets 的结构
因为这个模块要讲整体架构,所以只截取 FileManager 插件的头部,让你感受一下 ArkTS 插件的基本形态:
import { CordovaPlugin, CallbackContext } from '@magongshou/harmony-cordova/Index';
import { PluginResult, MessageStatus } from '@magongshou/harmony-cordova/Index';
import { common } from '@kit.AbilityKit';
import { fileIo } from '@kit.CoreFileKit';
import { util } from '@kit.ArkTS';
interface ExportDataArgs {
data: string;
fileName: string;
dirPath?: string;
}
export class FileManagerPlugin extends CordovaPlugin {
/**
* 导出数据到文件
*/
async exportData(callbackContext: CallbackContext, args: string[]): Promise<void> {
try {
const argsObj: ExportDataArgs = JSON.parse(args[0]);
const jsonData: string = argsObj?.data || '';
const fileName: string = argsObj?.fileName || `finance-backup-${new Date().toISOString().slice(0, 10)}.json`;
const dirPath: string = argsObj?.dirPath || '';
console.log('[FileManagerPlugin] 开始导出数据,文件名: ' + fileName);
console.log('[FileManagerPlugin] 导出目录: ' + dirPath);
const context = getContext() as common.UIAbilityContext;
const cacheDir: string = context.cacheDir;
let exportDir: string = cacheDir;
if (dirPath && dirPath.length > 0) {
exportDir = dirPath;
}
const filePath: string = exportDir + '/' + fileName;
try {
await fileIo.mkdir(exportDir);
} catch (e) {
console.log('[FileManagerPlugin] 目录已存在: ' + exportDir);
}
const file = await fileIo.open(filePath, fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE);
await fileIo.write(file.fd, jsonData);
await fileIo.close(file.fd);
console.log('[FileManagerPlugin] 数据导出成功: ' + filePath);
const pluginResult = PluginResult.createByString(MessageStatus.OK, filePath);
callbackContext.sendPluginResult(pluginResult);
} catch (error) {
console.error('[FileManagerPlugin] 导出失败: ' + JSON.stringify(error));
const pluginResult = PluginResult.createByString(MessageStatus.ERROR, (error as Error).message);
callbackContext.sendPluginResult(pluginResult);
}
}
}
3.1 ArkTS 插件在架构中的位置
从这段代码可以看出,FileManagerPlugin 本质上就是:
- 继承自
CordovaPlugin的一个 ArkTS 类; - 暴露若干个
async xxx(callbackContext, args)方法; - 内部可以随意使用 HarmonyOS 提供的各种 Kit(这里用了
AbilityKit、CoreFileKit、ArkTS工具包)。
在整体架构里,它扮演的角色就是:
- JavaScript 的“系统 API 代理”:
- JS 不直接碰底层文件系统,只调用
cordova.exec('FileManager', 'exportData', [...]); FileManagerPlugin负责把这些请求翻译成真实的fileIo.open/write/close调用。
- JS 不直接碰底层文件系统,只调用
- 错误边界:
- ArkTS 插件内部捕获异常,转换为
PluginResult,再通过 Cordova 桥回传给 JS,避免前端直接崩溃。
- ArkTS 插件内部捕获异常,转换为
从模块划分的角度,这个插件和 Index.ets 是一对:一个负责「注册和挂载」,一个负责「提供具体能力」。
4. Web 入口:rawfile/www/index.html 是怎么被加载的?
虽然本模块重点在 ArkTS 和 Cordova 的壳,但理解 Web 端入口也很关键。项目中的 cordova/src/main/resources/rawfile/www/index.html 大致结构如下(示意):
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>财务管家</title>
<!-- 样式省略 -->
</head>
<body>
<div id="app">
<!-- PC 布局容器 -->
</div>
<script src="cordova.js"></script>
<script src="js/db.js"></script>
<script src="js/pages.js"></script>
<script src="js/plugins/FileManager.js"></script>
<script src="js/app.js"></script>
</body>
</html>
4.1 index.html 在架构中的角色
- 对 ArkTS 来说,这只是一个普通的静态资源,打包进
rawfile,由 ArkWeb 加载。 - 对 Cordova 来说,
cordova.js会在 Web 环境里注入全局对象cordova,并建立exec通道。 - 对你写的业务来说,所有页面逻辑都集中在
db.js(数据)+pages.js(页面)+app.js(启动和事件)里。
因此,从架构视角看,可以把 index.html 理解为:
「被 ArkTS 容器托管的单页应用入口,而不是传统浏览器地址栏里输入的 URL」。
5. JS 侧入口:PageManager 的初始化
最后看一个 JS 端的简单片段,来自 pages.js 的开头部分,用来承接 ArkWeb 渲染出来的 DOM:
class PageManager {
constructor() {
this.currentPage = 'dashboard';
this.pageContent = null;
}
init() {
this.pageContent = document.querySelector('.page-content');
}
renderPage(pageName, data = {}) {
this.currentPage = pageName;
const renderer = this.pages[pageName];
if (renderer and this.pageContent) {
this.pageContent.innerHTML = renderer(data);
this.bindPageEvents(pageName);
}
}
}
5.1 JS 在整个架构中的位置
结合前面的 ArkTS 链路,可以这样理解:
- ArkTS 侧的
Index.ets渲染MainPage/CordovaWebView; - ArkWeb 在内部加载
index.html; index.html依次加载db.js、pages.js、app.js;PageManager.init()找到.page-content容器,把每个功能页渲染进去;- 当用户点击「导出数据」按钮时:
- JS 在
pages.js里调用cordova.exec('FileManager', 'exportData', [...]); - Cordova 把这个请求转发给 ArkTS 的
FileManagerPlugin.exportData; - ArkTS 执行文件读写,再通过
PluginResult把结果返回给 JS; - JS 收到回调后,用 Toast 告知用户导出成功。
- JS 在
这就是当前项目里「从原生到 Web,再从 Web 回到原生」的完整闭环,也是后面数据导入导出模块、预算模块、报表模块等所有高级功能的基础。
6. 小结:为什么要先理解这个模块?
在实际开发中,如果只盯着某一段 JS 或某一个 ArkTS 插件,很容易迷失在细节里,遇到问题就「哪里报错修哪里」,修着修着就觉得结构混乱,不敢大改。把这个模块单独拿出来做第一篇文章,有几个现实的好处:
- 后面 29 个模块,无论是 JS 还是 ArkTS,其实都只是往这条架构链路上「挂功能」;
- 当你想新增一个原生能力(比如震动、后台同步、系统日历集成),流程基本固定:
- 在 ArkTS 里写一个
XXXPlugin,继承CordovaPlugin; - 在
Index.ets的cordovaPlugs里注册这个插件; - 在 Web 端写一个 JS 封装,内部用
cordova.exec('XXX', 'someAction', [...]); - 在
pages.js的某个页面事件里去调用这个封装。
- 在 ArkTS 里写一个
- 当你要排查问题时,也有一条清晰的 Debug 路线:
- 先看 ArkTS 容器是否正常渲染(Index 日志);
- 再看 ArkWeb 是否成功加载 index.html;
- 再看
cordova.js是否初始化完成; - 最后看 JS 和 ArkTS 插件之间的数据是否对得上。
用一句话概括本模块的作用:
它不是在教你写某个功能,而是在教你「这个项目是怎么把 Web 和原生串成一条线的」。
理解了这条线,后面 30 个模块你就可以非常有把握地自由扩展,而不是每次改完都提心吊胆担心「会不会把整个壳搞挂了」。
更多推荐


所有评论(0)