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

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

一、独立 app 不要搞 Clean Architecture

反面教训:

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

独立开发者的本质:

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

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

二、推荐的目录结构

bash 复制代码
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。

typescript 复制代码
// 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 做增删改查。

typescript 复制代码
// 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。

typescript 复制代码
// 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。

typescript 复制代码
// 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。

typescript 复制代码
// 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/

工具类 / 常量 / 单例。

typescript 复制代码
// 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 :相对路径。

typescript 复制代码
// pages/ListPage.ets
import { ListViewModel } from '../viewmodel/ListViewModel'
import { AssetRow } from '../components/AssetRow'
import { Asset } from '../model/Asset'

鸿蒙 Kit import :@kit.XxxKit。

typescript 复制代码
import { relationalStore } from '@kit.ArkData'
import { promptAction } from '@kit.ArkUI'
import { hilog } from '@kit.PerformanceAnalysisKit'

不要:

typescript 复制代码
// ❌ 老的全路径
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 开销可忽略)。

typescript 复制代码
// 单例: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 的"技术债" 其实来自结构不清 ,不是写错代码 。工程上结构清晰 比算法先进 对独立开发者的长期产出更重要。

相关推荐
阿星AI工作室7 分钟前
WorkBuddy做数字人口播:动效包装+克隆声音+真实照片+对口型全流程
前端
火山引擎开发者社区8 分钟前
给企业配一双 AI 的手:火山引擎 OS Agent 替你操作手机和电脑
前端·人工智能
三年三月8 分钟前
自建https证书SSL
前端
ClouGence1 小时前
Web 自动化测试总是超时?聊聊等待、超时和异步加载
前端·测试
RobinDevNotes2 小时前
从 Electron 到 GPUI,9 个跨平台桌面方案该怎么选
前端·javascript·rust·electron·openresty·前端开发工具
jay神3 小时前
基于 YOLOv8 的轴承缺陷检测(Web 版)
前端·人工智能·yolo
jay神3 小时前
基于 YOLOv8 的森林火灾检测系统(Web 版)
前端·yolo
code2cat4 小时前
HTML基础篇之第一张网页:从index.html到浏览器里的任务清单
前端·html
miss4 小时前
AI 改完不敢上线?OpenSpec 规约驱动开发实战
前端·代码规范·前端工程化