鸿蒙 ArkTS 独立开发者的项目结构 model / dao / viewmodel / page / common 的分层

0 阅读5分钟

做了一堆鸿蒙 app 之后发现,项目第一天的目录结构 决定后 90 天开发顺不顺。走通几种结构后,总结出适合独立开发者的分层:model / dao / viewmodel / page / common / widget。记录一下这套结构 + 哪些文件该放哪里,顺便分享给有需要的朋友。

一、独立 app 不要搞 Clean Architecture

反面教训:

  • 用 DDD 分 domain / application / infrastructure / presentation 四层
  • 单文件嵌套 5 层目录
  • 加一个字段改 7 个文件
  • 独立开发者一个人写不下去

独立开发者的本质:

  • 人少(1 人)
  • 节奏快(1-2 周出 app)
  • 可读性 > 可扩展性(没有团队要适应)

适合的分层:6 层,平的。

二、推荐的目录结构

entry/src/main/ets/
├── entryability/       # 入口 ability
│   └── EntryAbility.ets
├── entrybackupability/ # 备份能力(空壳)
│   └── EntryBackupAbility.ets
├── model/              # 纯数据模型
│   ├── Asset.ets
│   ├── Category.ets
│   └── Settings.ets
├── database/           # DB 相关
│   ├── DbHelper.ets    # rdb 初始化
│   ├── AssetDao.ets
│   └── CategoryDao.ets
├── viewmodel/          # 业务逻辑
│   ├── ListViewModel.ets
│   └── DetailViewModel.ets
├── pages/              # 页面组件
│   ├── Index.ets
│   ├── ListPage.ets
│   └── DetailPage.ets
├── components/         # 可复用 UI 组件
│   ├── AssetRow.ets
│   ├── CategoryPicker.ets
│   └── EmptyState.ets
├── common/             # 工具 / 常量 / 单例
│   ├── Constants.ets
│   ├── DateUtils.ets
│   ├── ToastManager.ets
│   └── EventBus.ets
└── widget/             # 卡片(如果有)
    ├── MyWidgetAbility.ets
    └── pages/

7 个顶层目录。平,不嵌套。

三、每层的职责

model/

纯数据结构。接口 / class。

// model/Asset.ets
export class Asset {
  id: number = 0
  title: string = ''
  value: number = 0
  createdAt: number = 0
  updatedAt: number = 0
}

不要:

  • 业务逻辑(不写 calculateSomething)
  • DB 代码(不 import rdb)
  • UI 代码(不 import ArkUI)

好处:纯粹。可测试,可序列化。

database/

rdb 相关。DbHelper 做初始化,Dao 做增删改查。

// database/AssetDao.ets
export class AssetDao {
  async insert(asset: Asset): Promise<number> { ... }
  async queryAll(): Promise<Asset[]> { ... }
  async delete(id: number): Promise<number> { ... }
}

不要:

  • Dao 里做业务计算(如 "计算总金额")
  • Dao 导入 UI 组件

viewmodel/

业务逻辑 + state。连接 model/dao 和 UI。

// viewmodel/ListViewModel.ets
export class ListViewModel {
  items: Asset[] = []
  private dao: AssetDao = new AssetDao()

  async load(): Promise<void> {
    this.items = await this.dao.queryAll()
  }

  async add(title: string, value: number): Promise<void> {
    const asset = new Asset()
    asset.title = title
    asset.value = value
    asset.createdAt = Date.now()
    await this.dao.insert(asset)
    await this.load()
  }

  totalValue(): number {
    return this.items.reduce((s, a) => s + a.value, 0)
  }
}

ViewModel 的作用:

  • 包装 Dao 调用
  • 做业务计算
  • UI 直接订阅

不要:

  • 直接碰 rdb(走 Dao)
  • 包含 UI 代码

pages/

页面组件。@Entry @Component 的 struct。

// pages/ListPage.ets
@Entry @Component
struct ListPage {
  @State vm: ListViewModel = new ListViewModel()

  aboutToAppear(): void {
    this.vm.load()
  }

  build() {
    Column() {
      Text(`总计:¥${this.vm.totalValue()}`)
      List() {
        ForEach(this.vm.items, (item: Asset) => {
          ListItem() { AssetRow({ asset: item }) }
        })
      }
    }
  }
}

页面只做:

  • UI 布局
  • 事件 → 调用 viewmodel
  • 没业务逻辑

components/

可复用的 UI 组件。@Component 的 struct。

// components/AssetRow.ets
@Component
export struct AssetRow {
  @Prop asset: Asset = new Asset()

  build() {
    Row() {
      Text(this.asset.title)
      Text(`¥${this.asset.value}`)
    }
  }
}

不要:

  • components 里引 viewmodel
  • components 里做数据加载

原则:传 Prop 进来 UI 显示。

common/

工具类 / 常量 / 单例。

// common/Constants.ets
export const DB_NAME = 'app.db'
export const PAGE_SIZE = 50

// common/DateUtils.ets
export class DateUtils {
  static todayString(): string { ... }
}

// common/ToastManager.ets
export class ToastManager { ... }

widget/

卡片相关。只在有卡片的 app 存在。

四、文件命名

规范:PascalCase + .ets。

  • Asset.ets(class name)
  • AssetDao.ets(class name + 后缀)
  • ListPage.ets
  • DateUtils.ets

不要:

  • asset.ets(小写)
  • asset_dao.ets(下划线)
  • AssetDAO.ets(全大写)
  • asset-dao.ets(kebab)

一致性 > 其他:项目内统一一种,别混。

五、import 的规范

本项目内 import:相对路径。

// pages/ListPage.ets
import { ListViewModel } from '../viewmodel/ListViewModel'
import { AssetRow } from '../components/AssetRow'
import { Asset } from '../model/Asset'

鸿蒙 Kit import:@kit.XxxKit。

import { relationalStore } from '@kit.ArkData'
import { promptAction } from '@kit.ArkUI'
import { hilog } from '@kit.PerformanceAnalysisKit'

不要:

// ❌ 老的全路径
import relationalStore from '@ohos.data.relationalStore'

// ✓ 推荐
import { relationalStore } from '@kit.ArkData'

独立开发者的建议:全部用 @kit.XxxKit。鸿蒙 5.x 推荐 Kit 聚合写法。

六、单例 vs new 实例

适合单例(common/ 里):

  • ToastManager(全局 toast 不叠加)
  • EventBus(全局事件)
  • CryptoHelper(key 托管)

适合 new 实例(viewmodel/ 里):

  • 每个页面的 viewmodel(页面销毁,vm 销毁)

Dao:可以用 new 也可以单例。本案多用 new(Dao 本身无状态,new 开销可忽略)。

// 单例:ToastManager.getInstance().show('hi')
// new:new AssetDao().insert(...)

七、不要的目录

不要:

  • services/:和 viewmodel 混淆
  • utils/:和 common 混淆
  • types/:和 model 混淆
  • apis/:独立 app 一般不调外网
  • repository/:和 database 混淆
  • providers/:ArkUI 没这个概念

独立开发者的建议:7 个顶层目录够用。多了心智成本高。

八、新加功能的落点

假设要加"导出 CSV":

  • model/:不需要(没新数据结构)
  • database/:可能新加(如果要查询筛选)
  • viewmodel/:新加 ExportViewModel
  • pages/:新加 ExportPage 或在 SettingsPage 加按钮
  • components/:可能新加 FileNamePicker 组件
  • common/:新加 CsvEncoder 工具类

步骤:

  1. 先写 common/CsvEncoder.ets(纯工具)
  2. 再写 viewmodel/ExportViewModel.ets(业务)
  3. 最后写 pages/ 入口(UI)

自底向上:底层稳 上层快。

九、代码量的 benchmark

独立 app 的规模:

  • model/:10-30 文件,每文件 20-80 行
  • database/:3-6 Dao + 1 Helper,每文件 50-150 行
  • viewmodel/:3-10 ViewModel,每文件 50-200 行
  • pages/:5-15 Page,每文件 100-400 行
  • components/:10-30 组件,每文件 30-200 行
  • common/:5-20 工具类,每文件 30-100 行

总代码量:3000-15000 行(1 人 2-4 周的产出)。

超过 20000 行:开始考虑分 module 或 refactor。

踩坑清单

  • 不要 Clean Architecture,用7 层平目录
  • model 纯数据,不碰 DB / UI
  • database 的 Dao 只做 CRUD,不做业务计算
  • viewmodel 包装业务 + state
  • pages 只做 UI + 事件分发
  • components 纯 Prop 的可复用
  • common 工具类 / 单例 / 常量
  • 命名用 PascalCase + .ets
  • import 用 @kit.XxxKit 聚合写法
  • 不要 services / utils / types / apis / providers 等多余目录

尾声

项目结构是独立开发者的 90% 顺畅度。好的结构6 个月后仍然能快速改代码;坏的结构2 周后就不想碰了。

独立开发者的建议:

  • 项目第一天定好 6-7 层结构
  • 开发中不要创建新的顶层目录(抗诱惑)
  • 每周review 一次目录 看是否偏离了

对比:

  • React 项目:pages / components / hooks / utils 的平结构
  • iOS 项目:Models / Views / ViewModels / Services
  • 鸿蒙 ArkUI:本案的 7 层结构

鸿蒙的项目结构和 MVVM / React 接近。独立开发者学一次 pattern 可以复用到多个 app。

一个意外发现:独立 app 的"技术债" 其实来自结构不清,不是写错代码。工程上结构清晰比算法先进 对独立开发者的长期产出更重要。