在这里插入图片描述

本文对应项目模块:从 EntryAbilityIndex.etsMainPage → ArkWeb → Cordova → rawfile/www/index.html 的整条链路。
欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。

目标:搞清楚「鸿蒙原生壳」是如何把前端页面托管起来的,以及 Cordova 在中间扮演什么角色。


1. 整体架构概览(文字版)

在当前这个财务管家项目中,你看到的 HTML/JS 界面,并不是直接由浏览器打开的,而是嵌在 HarmonyOS 的 ArkWeb 组件里面,由一个 ArkTS 写的壳来承载。大致链路可以拆解为几层:

  1. Ability 层:应用入口的 EntryAbility,负责加载首个 UI 界面。
  2. ArkTS 页面容器层Index.ets 作为首页组件,只干两件事:
    • 建立 Cordova 插件列表(比如 FileManagerPlugin)。
    • 渲染承载 Web 内容的 MainPage / CordovaWebView
  3. Web 内核层(ArkWeb):ArkWeb 负责真正渲染 HTML/JS,是一个类似浏览器的内核。
  4. Cordova 桥接层:在 ArkWeb 里面注入 cordova.js,拦截 cordova.exec 调用,把 JS 的请求转成原生插件调用。
  5. 前端页面层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 插件的配置,包含 pluginNamepluginObject 两个字段。
    • 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(这里用了 AbilityKitCoreFileKitArkTS 工具包)。

在整体架构里,它扮演的角色就是:

  • JavaScript 的“系统 API 代理”
    • JS 不直接碰底层文件系统,只调用 cordova.exec('FileManager', 'exportData', [...])
    • FileManagerPlugin 负责把这些请求翻译成真实的 fileIo.open/write/close 调用。
  • 错误边界
    • ArkTS 插件内部捕获异常,转换为 PluginResult,再通过 Cordova 桥回传给 JS,避免前端直接崩溃。

从模块划分的角度,这个插件和 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 链路,可以这样理解:

  1. ArkTS 侧的 Index.ets 渲染 MainPage / CordovaWebView
  2. ArkWeb 在内部加载 index.html
  3. index.html 依次加载 db.jspages.jsapp.js
  4. PageManager.init() 找到 .page-content 容器,把每个功能页渲染进去;
  5. 当用户点击「导出数据」按钮时:
    • JS 在 pages.js 里调用 cordova.exec('FileManager', 'exportData', [...])
    • Cordova 把这个请求转发给 ArkTS 的 FileManagerPlugin.exportData
    • ArkTS 执行文件读写,再通过 PluginResult 把结果返回给 JS;
    • JS 收到回调后,用 Toast 告知用户导出成功。

这就是当前项目里「从原生到 Web,再从 Web 回到原生」的完整闭环,也是后面数据导入导出模块、预算模块、报表模块等所有高级功能的基础。


6. 小结:为什么要先理解这个模块?

在实际开发中,如果只盯着某一段 JS 或某一个 ArkTS 插件,很容易迷失在细节里,遇到问题就「哪里报错修哪里」,修着修着就觉得结构混乱,不敢大改。把这个模块单独拿出来做第一篇文章,有几个现实的好处:

  • 后面 29 个模块,无论是 JS 还是 ArkTS,其实都只是往这条架构链路上「挂功能」;
  • 当你想新增一个原生能力(比如震动、后台同步、系统日历集成),流程基本固定:
    1. 在 ArkTS 里写一个 XXXPlugin,继承 CordovaPlugin
    2. Index.etscordovaPlugs 里注册这个插件;
    3. 在 Web 端写一个 JS 封装,内部用 cordova.exec('XXX', 'someAction', [...])
    4. pages.js 的某个页面事件里去调用这个封装。
  • 当你要排查问题时,也有一条清晰的 Debug 路线:
    • 先看 ArkTS 容器是否正常渲染(Index 日志);
    • 再看 ArkWeb 是否成功加载 index.html;
    • 再看 cordova.js 是否初始化完成;
    • 最后看 JS 和 ArkTS 插件之间的数据是否对得上。

用一句话概括本模块的作用:

它不是在教你写某个功能,而是在教你「这个项目是怎么把 Web 和原生串成一条线的」。

理解了这条线,后面 30 个模块你就可以非常有把握地自由扩展,而不是每次改完都提心吊胆担心「会不会把整个壳搞挂了」。

更多推荐