对于有你来说,搭建环境和创建项目看似是肌肉记忆,但在 HarmonyOS NEXT 这个全新的生态里,很多规则已经改变了。如果你依然沿用以前写 Android 或者旧版鸿蒙 FA 模型的思维,很可能会在项目中期陷入重构的泥潭。

今天,我不只是教你点几个按钮,我们要深度探讨一个商业级应用的骨架该如何搭建。这不仅关乎代码写得顺不顺手,更关乎你的应用能不能承载未来复杂的业务增长。

我们要解决的核心问题是:如何在一个全新的原生系统中,构建一个高内聚、低耦合、可扩展的工程结构。

一、 为什么我们坚定选择 Stage 模型

当你打开 DevEco Studio,点击 Create Project 时,系统或许会给你几个模版选择。这时候,请毫不犹豫地选择 Application 并确保 Model 选的是 Stage

可能你会问,为什么不是 FA 模型?你看,Stage 模型是 HarmonyOS NEXT 应用开发的主流模式,它在设计之初就考虑到了复杂的应用场景。相比于 FA 模型,Stage 模型引入了 AbilityStageWindowStage 等概念,将应用的能力生命周期和界面显示生命周期做了分离。这意味着什么?这意味着当你的应用在多设备流转,或者在分屏、悬浮窗这些复杂场景下运行时,Stage 模型能提供更精细的控制力。

对于我们的会议随记 Pro 来说,未来我们可能会做平板适配,甚至手表联动,Stage 模型提供的UIAbility组件分离机制,能让我们更从容地处理这些多窗口逻辑。我们在创建项目时,包名(Bundle Name)建议使用反向域名格式,比如 com.yourname.meetingpro,这个 ID 将是你在鸿蒙生态中的唯一身份证,一旦上架就不能修改,所以请慎重敲下每一个字符。

项目创建完成后,不要急着写代码。我们先来做一件甚至比写代码更重要的事情:规划目录结构

二、商业级工程目录的划分哲学

很多新手开发者的习惯是,把所有页面都扔在 pages 目录下,把所有工具类都扔在 utils 里。这种做法在 Demo 阶段没问题,但一旦业务复杂起来,比如我们的应用涉及录音、数据库、项目管理、联系人统计等多个模块,这种扁平的结构就会变成一场灾难。你会在找一个文件上浪费五分钟,修改一个功能导致另一个功能崩溃。

我们需要引入**模块化(Modularization)**的思维。

在鸿蒙开发中,官方提供了 HAP(Harmony Ability Package)和 HSP(Harmony Shared Package)的概念。HAP 是应用的主入口,可以独立安装;HSP 是动态共享包,用于多个 HAP 之间共享代码。考虑到我们是独立开发,初期为了降低构建复杂度和部署成本,我们暂时不需要搞多个 HAP,我们采用单 HAP 内部模块化的策略。

我们把代码逻辑分为三层:功能层(Features)数据层(Data)公共基础层(Commons)

请打开你的工程,在 entry/src/main/ets 目录下,删掉默认的结构,建立如下的目录树。你看,我们这样做,能让代码的归属权清晰到极致。

entry/src/main/ets/
├── commons/                  # 【公共基础层】底座,与业务无关
│   ├── base/                 # 基类封装
│   ├── constants/            # 全局常量
│   ├── utils/                # 通用工具链
│   └── components/           # 全局通用UI组件
├── data/                     # 【数据层】应用的心脏
│   ├── db/                   # 数据库管理(RdbStore)
│   ├── models/               # 业务实体定义
│   └── preferences/          # 轻量级存储
├── features/                 # 【功能层】按业务垂直切分
│   ├── record/               # 录音模块(核心)
│   ├── project/              # 项目模块
│   ├── contact/              # 人脉模块
│   ├── dashboard/            # 统计看板模块
│   └── settings/             # 设置模块
├── pages/                    # 【路由层】页面的物理入口
│   ├── Index.ets             # App 壳子
│   └── SplashPage.ets        # 启动页
└── entryability/             # Ability 生命周期
    └── EntryAbility.ts

为什么要这么分?

Commons 层 是我们的军火库。这里存放的是就算换个 App 也能直接用的代码。比如时间格式化工具,无论是做会议 App 还是跑步 App,逻辑都是一样的;再比如全局的 Loading 组件、统一的 Log 打印工具。把它们抽离出来,能保证业务逻辑的纯粹。

Data 层 是数据的唯一真理来源。我们规定:UI 层绝对不能直接操作数据库。所有的增删改查,必须通过 data 层暴露的方法来调用。这样设计的好处是,如果未来有一天,你想把本地数据库换成云端数据库,你只需要修改 data 目录下的代码,而上层的 UI 界面完全不需要感知。这就是架构设计中常说的关注点分离

Features 层 是我们业务的主战场。我们将 App 拆解为录音、项目、人脉等独立的业务域。在 features/record 目录下,我们可以进一步划分 views(视图)、viewmodel(逻辑)、model(该模块独有的数据模型)。这种高内聚的结构,让你在开发录音功能时,视线可以完全聚焦在 record 目录下,不会被其他无关代码干扰。

三、 公共基础层的核心封装

架构搭好了,我们先来填补 Commons 层 的内容,这是整个 App 的地基。

commons/base 目录下,我强烈建议你封装一个 BaseViewModel。在鸿蒙的 ArkUI 范式中,虽然是声明式 UI,但我们依然需要一个地方来处理业务逻辑,将状态(State)和视图(View)解耦。

我们定义一个泛型基类,用来处理所有页面通用的逻辑,比如加载状态的控制、错误信息的提示。

// commons/base/BaseViewModel.ts

import { Observable } from '../utils/Observable'; // 假设你有个简易的观察者工具

export class BaseViewModel {
  /**
   * 页面加载状态
   * true: 正在加载
   * false: 加载完成
   */
  isLoading: boolean = false;

  /**
   * 错误信息
   * 空字符串表示无错误
   */
  errorMessage: string = '';

  /**
   * 统一处理异步任务,自动管理 Loading 状态
   * @param task 异步任务函数
   */
  async launch<T>(task: () => Promise<T>): Promise<T | null> {
    try {
      this.isLoading = true;
      const result = await task();
      return result;
    } catch (error) {
      console.error(`Task failed: ${JSON.stringify(error)}`);
      this.errorMessage = '操作失败,请重试';
      return null;
    } finally {
      this.isLoading = false;
    }
  }
}

你看,有了这个基类,以后我们在写具体的业务逻辑(比如 RecordViewModel)时,只需要继承它。当我们要从数据库读取录音列表时,直接调用 this.launch(async () => { ... }),Loading 状态的切换就会自动完成,不用每次都手写 isLoading = truefalse。这就是封装的价值,它帮我们消灭了重复代码。

此外,在 commons/constants 下,我们要建立一个 AppConfig.tsBreakpoints.ts。鸿蒙提倡一次开发,多端部署,所以我们最好提前定义好断点(Breakpoints)常量,比如 smmdlg,对应手机、折叠屏和平板。虽然初期我们只适配手机,但预留这个配置,体现的是你作为资深开发的远见。

四、 module.json5 深度解读与配置

工程结构搞定后,我们需要把目光移向那个不起眼但至关重要的文件module.json5

在 Stage 模型中,它是整个模块的控制中枢。很多新手遇到的坑,比如“为什么录音没声音”、“为什么切到后台就挂了”、“为什么图标改不过来”,根源都在这个文件里。

请打开 entry/src/main/module.json5,我们要重点配置三个部分:权限(Permissions)能力(Abilities)后台模式(Background Modes)

首先是权限。会议随记 Pro 是一个录音应用,麦克风权限是我们的生命线。不仅如此,为了实现“会议写入系统日历”的功能,我们还需要日历的读写权限。在 requestPermissions 数组中,我们必须显式声明这些需求。这里有一个常识性的细节:从 API 9 开始,系统对隐私权限管控非常严格,你不仅要声明 name,还必须提供 reason(申请理由)和 usedScene(使用场景)。

"requestPermissions": [
  {
    "name": "ohos.permission.MICROPHONE",
    "reason": "$string:permission_reason_microphone",
    "usedScene": {
      "abilities": ["EntryAbility"],
      "when": "inuse"
    }
  },
  {
    "name": "ohos.permission.READ_CALENDAR",
    "reason": "$string:permission_reason_calendar",
    "usedScene": {
      "abilities": ["EntryAbility"],
      "when": "inuse"
    }
  },
  {
    "name": "ohos.permission.WRITE_CALENDAR",
    "reason": "$string:permission_reason_calendar",
    "usedScene": {
      "abilities": ["EntryAbility"],
      "when": "inuse"
    }
  }
]

注意那个 $string:permission_reason_microphone,千万不要在这里直接写死字符串。你需要在 resources/base/element/string.json 里定义它,支持多语言。如果你的理由写得含糊不清,比如只写“需要权限”,在应用上架审核时是会被直接驳回的。我们要写得诚恳且具体,比如:“需要使用麦克风录制会议音频以便生成笔记”。

接下来是后台模式。这是专业录音应用和玩具应用的分水岭。如果不配置这个,用户在录音时稍微切出去回个微信,或者锁屏放进口袋,系统为了省电很可能会杀掉你的录音进程。为了保证录音的连续性,我们需要申请长时任务。

module 对象下,添加 backgroundModes 字段:

代码段

"module": {
  // ... 其他配置
  "backgroundModes": [
    "audioRecording",
    "audioPlayback"
  ]
}

这行配置向系统宣告:我的应用涉及后台录音后台播放,请给我颁发“免死金牌”。当然,有了这个声明还不够,我们后续在代码中还需要调用后台任务管理的 API 来实时申请,但这行配置是入场券。

最后是 EntryAbility 的配置。在 abilities 数组中,你会看到一个名为 EntryAbility 的对象。它是我们应用的主入口。特别要注意 skills 字段的配置,它决定了你的应用能不能在桌面上被点击图标启动。

"skills": [
  {
    "entities": [
      "entity.system.home"
    ],
    "actions": [
      "action.system.home"
    ]
  }
]

这段配置的意思是,这个 Ability 具备处理“系统桌面”发出的“回家(Home)”动作的能力,换句话说,它就是那个点击图标后第一个跳出来的界面。

五、 资源配置颜色与字符串的规范化

在写 UI 代码前,我们最后要约束的是资源文件

我见过太多只有 3 年经验的开发者,喜欢在代码里硬编码颜色值,比如 #0A59F7。这在项目初期看似快,到了后期要适配深色模式时,就是一场灾难。

我们要利用鸿蒙强大的资源限定词系统。请在 resources/base/element 下确认 color.json 的存在。我们要定义一套语义化的颜色系统。

你看,我们这样做:不要定义 blue_color,而要定义 brand_color(品牌色);不要定义 black_text,而要定义 text_primary(一级文本色)。

{
  "color": [
    {
      "name": "brand_color",
      "value": "#0A59F7"
    },
    {
      "name": "text_primary",
      "value": "#1A1A1A"
    },
    {
      "name": "bg_page",
      "value": "#F1F3F5"
    }
  ]
}

然后,在 resources/dark/element/color.json(如果没有 dark 目录就新建一个)中,重新定义这些名字,但赋予深色的值。

{
  "color": [
    {
      "name": "brand_color",
      "value": "#5C9DFF"
    },
    {
      "name": "text_primary",
      "value": "#E6E6E6"
    },
    {
      "name": "bg_page",
      "value": "#000000"
    }
  ]
}

这样,我们在写代码时只引用 $r('app.color.text_primary')。当用户把手机系统切换到深色模式时,鸿蒙系统会自动帮我们切换颜色值,整个 App 瞬间适配,一行代码都不用改。这就是规范化工程结构的威力。

六、 总结

到这里,我们已经完成了一次完美的起跑。

我们没有急着画界面,而是先修好了路。我们确定了 Stage 模型 的技术路线,搭建了包含 Features、Data、Commons 三层架构的工程目录,封装了通用的 BaseViewModel,并在 module.json5 中拿到了录音和后台运行的关键权限。

现在的工程,虽然运行起来只有一个空白的页面,但它的骨架是强壮的,逻辑是清晰的。它已经做好了承载复杂业务的准备。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐