我是兰瓶Coding,一枚刚踏入鸿蒙领域的转型小白,原是移动开发中级,如下是我学习笔记《零基础学鸿蒙》,若对你所有帮助,还请不吝啬的给个大大的赞~

前言

先来一句挑衅味儿十足的反问:都 2025 年了,ArkTS 还只当“写页面的胶水代码”?为什么不把模块边界、依赖树、Ability 包装、以及“热更新”的可行路径一起撸明白?别被“复杂”两个字吓到——把线理顺、把坑填平、把套路打通,你会发现: HarmonyOS/ArkTS 的模块化,不只是项目结构好看,而是团队协作、发布节奏、以及多设备分工的压舱石。 今天这篇,我就以“模块组织结构 → import/export → 模块热更新”为主线,串起 module.json5、bundle 结构与 Ability 包管理的前前后后。保证不夹生,绝不鸡汤,全是能落地的做法与代码。当然,我个人的吐槽与小情绪也会一路穿插(不然这文章看着多寡淡呀😏)。

前言:为什么 ArkTS 的“模块化”是全栈创作者的主战场?

说句心里话:我早年对“模块化”的理解,就是“把文件分分目录、搞个 index.ts 汇总导出”。现在回头看,这只是入门级的整洁,离“工程化的稳定与可进化”还差十万八千里。真正的模块化,要解决三件现实问题:
  ① 如何切分能力边界:UIAbility、ServiceExtensionAbility、FormExtensionAbility(卡片)、DataShare 等在模块层面如何落位?
  ② 依赖如何可控:业务 HAP、通用 HAR 库、(可选)共享包如何编排?版本如何锁定?循环依赖怎么破?
  ③ 更新如何平滑:开发期热重载只是爽一时,发布期的“准热更新”怎么设计?资源、协议、模块多 HAP 拆分如何支撑灰度策略?
  这三件事,才是“全栈创作者”能不能又快又稳的分水岭。**页面炫不炫,是个人手艺;模块稳不稳,是团队能力。**好了,别鸡血,开搞。

一、概念先织网:Bundle、HAP、HAR、Ability、module.json5 各归其位

在动刀之前,我们把几个高频名词拎清楚(不求百科式面面俱到,但求和工程决策强相关):

  • Bundle(应用包):你的 App 在系统里的“身份”。一个 bundle 里可以包含多个 HAP(Harmony Ability Package)

  • HAP(模块/功能包):可安装、可独立更新的功能单元,常见有 entry.hap(主入口)与若干 feature-*.hap(功能模块)。一个 HAP 内部由 Ability + 资源 + ArkTS 代码组成。

  • HAR(库包)Harmony Archive,非独立运行、专供复用的类库(ArkTS/资源/类型定义),被 HAP 引用,不直接安装

  • Ability(能力):对应运行时能力入口。Stage 模型下最常用:

    • UIAbility:界面与窗口生命周期;
    • ServiceExtensionAbility:驻后台服务;
    • FormExtensionAbility:桌面卡片;
    • 其它 Extension(如 Data/Work 等)按需选用。
  • module.json5HAP 级配置文件,定义模块名、类型(entry/feature)、内部 Abilities、权限、资源打包规则等。

  • ohpm/oh-package.json5:Harmony 的包管理器与依赖清单,负责拉取 HAR 等依赖,生成 lock 文件,保障可重现构建。

一句话概括:HAP 可安装、HAR 可复用、Ability 可运行、module.json5 可声明。这四者关系理顺,你的模块边界基本就“醒目”了。

二、模块组织结构:从“好看”到“好用”的差一个维度

我给一个可扩展中大型应用的推荐骨架。不是唯一解,但确实踩坑之后沉淀下来的“够稳”方案。

project-root/
  apps/
    main-app/
      entry/                      # 主入口 HAP
        src/main/ets/
          entryability/EntryAbility.ets
          pages/...
          components/...
          common/...
          data/...
          service/...
        src/main/resources/...
        module.json5
        oh-package.json5          # 仅模块级有额外依赖时放这里
      feature-tasks/              # 功能 HAP:任务域(示例)
        src/main/ets/
          abilities/TasksAbility.ets
          pages/...
          api/...
        src/main/resources/...
        module.json5
      feature-reports/            # 功能 HAP:报表域(示例)
        src/main/ets/...
        module.json5
      feature-widget/             # 卡片 HAP(FormExtensionAbility)
        src/main/ets/...
        module.json5
      oh-package.json5            # 应用级依赖与脚本(如统一工具、lint)
  libs/
    core-kit/                     # HAR:领域模型与工具函数
      index.ets
      src/...
      oh-package.json5
    ui-kit/                       # HAR:通用 UI 组件(ArkUI 封装)
      index.ets
      src/...
      oh-package.json5
  toolchain/
    scripts/...
    configs/...
  .ohpmrc
  oh-package-lock.json5
  README.md

设计要点

  1. HAP 只承载“运行态”东西:页面、Ability、资源、与少量贴近业务的逻辑。
  2. 跨 HAP 复用全部沉到 HARcore-kit 放模型/协议/工具、ui-kit 放通用组件,抵御复制粘贴
  3. 应用级与模块级依赖分别管理:通用依赖放 apps 级,特定依赖放各自 HAP 的 oh-package.json5
  4. 资源分治:每个 HAP 只带自己需要的图片/字符串,避免臃肿与冲突。
  5. 可插拔能力:把“新功能”做成独立 feature-*.hap发版时可灰度更迭;entry 只做启动与路由调度。

是的,这跟前端“微前端/多入口”思路有几分相似:把风险拆小,把边界拉清。真出问题,修一个 HAP 就够,用户也不至于整包重下。

三、module.json5:给 HAP“立身份证”和“排兵布阵”

来一份清爽但不失要点的 entry 模块示例,注意注释

{
  "module": {
    "name": "entry",
    "type": "entry",                         // 主入口 HAP
    "srcEntry": "./ets/entryability/EntryAbility.ets",
    "deviceTypes": [ "phone", "tablet" ],
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:desc_entry",
        "icon": "$media:app_icon",
        "label": "$string:app_name",
        "type": "page",                      // UIAbility
        "launchType": "standard",
        "skills": [{ "entities": ["entity.system.home"], "actions": ["action.system.home"] }]
      },
      {
        "name": "BgService",
        "type": "service",                   // ServiceExtensionAbility
        "srcEntry": "./ets/service/BgService.ets",
        "exported": false
      }
    ],
    "requestPermissions": [
      { "name": "ohos.permission.INTERNET" },
      { "name": "ohos.permission.VIBRATE" },
      { "name": "ohos.permission.POST_NOTIFICATIONS" }
    ],
    "dependencies": {
      "libs": [                             // 仅示意:编译期引入 HAR(部分工具链会用此段或 ohpm)
        "libs/core-kit.har",
        "libs/ui-kit.har"
      ]
    },
    "metadata": {
      "buildMode": "release"                // 可按需放构建标识、开关等
    }
  }
}

而一个 feature HAP(比如报表)会长这样:

{
  "module": {
    "name": "feature-reports",
    "type": "feature",                       // 功能 HAP
    "srcEntry": "./ets/abilities/ReportsAbility.ets",
    "deviceTypes": [ "phone", "tablet" ],
    "abilities": [
      {
        "name": "ReportsAbility",
        "type": "page",
        "srcEntry": "./ets/abilities/ReportsAbility.ets",
        "launchType": "standard"
      }
    ],
    "requestPermissions": [
      { "name": "ohos.permission.INTERNET" }
    ]
  }
}

两个提醒

  • type=entry 的 HAP 通常承担路由分发职责,把“有无安装某 feature”当成导航可见性的决定因素;
  • 可安装/可更新”发生在 HAP 层,不在 HAR 层(HAR 只是被编进 HAP 里)。这点对“热更新/灰度”的策略很关键。

四、import/export:ArkTS 的“度量单位”与“防身术”

很多人写 ArkTS 还停留在“能导入就行”的阶段。抱歉,这没法支撑中大型协作。我这里给一个“写得舒服又能被工具链读懂”的规范组合拳:

4.1 基本导出与索引聚合(barrel)

// libs/core-kit/src/domain/todo.ts
export interface Todo {
  id: string
  title: string
  note?: string
  estimate: number
  finished: number
  done: boolean
  createdAt: number
  updatedAt: number
}

// libs/core-kit/src/utils/id.ts
export function genId(): string {
  return Math.random().toString(36).slice(2) + Date.now().toString(36)
}

// libs/core-kit/index.ets  —— barrel:集中导出,便于上层树摇优化
export * from './src/domain/todo'
export * from './src/utils/id'
// apps/main-app/entry/src/main/ets/common/Types.ts —— 二次封装(避免上层直接依赖底层路径)
export type { Todo } from 'core-kit'         // 来自 HAR 的类型
export { genId } from 'core-kit'

4.2 命名空间导入与默认导出

import * as Core from 'core-kit'
import { genId } from 'core-kit'

// 当某模块设计成默认导出:
// libs/ui-kit/src/Button.ets
export default struct Button { /* ... */ }

// 使用:默认导入 + 命名导入并存
import Button, { ButtonProps } from 'ui-kit/Button'

4.3 ArkTS 与系统模块的约定

import router from '@ohos.router'
import http from '@ohos.net.http'
import preferences from '@ohos.data.preferences'
import rdb from '@ohos.data.rdb'

做法建议

  • 使用“聚合导出(barrel)”统一出口,减少上层 import 的长路径拼接,更利树摇
  • 给每个 HAR 做 index.ets 作为官方入口,不要让上层 import 私有深层路径;
  • 类型(type/interface)与值分开思考:导入类型不产生运行时依赖,是优化的好帮手。

4.4 资源与 JSON 配置的导入

ArkTS 中常见“资源”导入方式(以字符串资源为例):

import { $r } from '@ohos.resource'          // 具体按 SDK 版本能力,演示思路
Text($r('app.string:app_name'))

对于配置 JSON,建议放在 HAR 中并暴露 类型安全的读取函数,避免上层乱读文件路径:

// libs/core-kit/src/config/appConfig.ts
const defaults = { networkTimeoutMs: 15000, retry: 2 }
export function readAppConfig(): { networkTimeoutMs: number; retry: number } {
  // 可按构建注入或资源方式合并环境配置
  return defaults
}

五、依赖管理:ohpm、版本锁与循环依赖斩断术

5.1 ohpm 的日常姿势

在应用或 HAP/HAR 根目录里有 oh-package.json5,形如:

{
  "name": "@example/main-app",
  "version": "1.2.3",
  "description": "Main application",
  "author": "you",
  "license": "MIT",
  "dependencies": {
    "@example/core-kit": "workspace:libs/core-kit",   // 本地 HAR(workspace 风格)
    "@example/ui-kit": "workspace:libs/ui-kit",
    "@ohos/axios-lite": "^1.0.0"                      // 假设存在:示例依赖
  },
  "scripts": {
    "build": "hvigor build --mode module",
    "dev": "hvigor --watch"
  }
}

两点经验谈

  • workspace: 语义链接本地 HAR,避免发布前就发私服的繁琐;
  • 锁文件 oh-package-lock.json5 必须入库,保证构建可重现

5.2 消灭循环依赖

最容易犯错的,是两个 HAP 互相 import。记住HAP 之间不要直接互引代码,要么走路由,要么共用 HAR。

  • 错例feature-tasks import feature-reports 的 util;
  • 正解:把 util 上移到 core-kit HAR,两个 HAP 各自依赖 core-kit

一句狠话让 HAP 只面向用户、让 HAR 承载共享。把团伙关系变成“共同的上游亲爹”。

5.3 版本策略与破窗效应

给 HAR 设 显式语义化版本,并在应用根维护一份“版本常量表”(或使用工具统一 bump),杜绝“这边 1.2.1,那边 1.2.0”的拉扯。
  宁可升级全量一致,也不要容忍多版本共存——尺寸膨胀与行为差异会把你拖进泥潭。

六、Ability 包管理:页面、服务、卡片与“模块自治”

把 Ability 视作运行态边界,每个 HAP 自治自己的能力集合:

  • entry:含 EntryAbility(主页路由)+ 少量“兜底服务”;
  • feature-xxx:各自的 UIAbility(或只有页面,由 entry 路由到此模块)、必要时的 ServiceExtensionAbility
  • feature-widget:独立 FormExtensionAbility 与卡片布局资源;
  • 共识跨 HAP 不直接访问对方内部 Ability,通过 Want/路由/公共协定 做“松耦合”。

6.1 Ability 之间如何“礼貌沟通”?

推荐用“协议对象”而不是“随手塞参数”:

// libs/core-kit/src/protocols/navigation.ts
export interface NavToReportParams {
  reportId: string
  from?: 'home' | 'task'
}
export const NAV_ACTIONS = {
  toReport: 'app://reports/detail'
}
// apps/main-app/entry/src/main/ets/common/nav.ts
import router from '@ohos.router'
import { NAV_ACTIONS, NavToReportParams } from 'core-kit'

export function gotoReport(p: NavToReportParams) {
  router.pushUrl({ url: NAV_ACTIONS.toReport, params: p as any })
}

关键点:把“路由地址”和“参数类型”纳入 HAR,HAP 只是使用者;这样新旧模块并存时不至于传错参。

七、代码示例:一个“任务域 HAP”与“报表域 HAP”的来回串场

7.1 core-kit(HAR):模型与工具

// libs/core-kit/src/domain/task.ts
export interface Task {
  id: string
  title: string
  done: boolean
  estimate: number
  finished: number
  createdAt: number
  updatedAt: number
}

// libs/core-kit/src/utils/time.ts
export function now(): number {
  return Date.now()
}
export function minutes(n: number) { return n * 60 * 1000 }
// libs/core-kit/index.ets
export * from './src/domain/task'
export * from './src/utils/time'

7.2 feature-tasks(HAP):一个 Ability + 页

// apps/main-app/feature-tasks/src/main/ets/abilities/TasksAbility.ets
import UIAbility from '@ohos.app.ability.UIAbility'
import window from '@ohos.window'

export default class TasksAbility extends UIAbility {
  onWindowStageCreate(stage: window.WindowStage) {
    stage.loadContent('pages/TasksIndex', (err) => {
      if (err && err.code) console.error('Load TasksIndex failed', JSON.stringify(err))
    })
  }
}
// apps/main-app/feature-tasks/src/main/ets/pages/TasksIndex.ets
import { Task, now } from 'core-kit'
import { genId } from 'core-kit'
import rdb from '@ohos.data.rdb'

@Entry
@Component
struct TasksIndex {
  @State list: Task[] = []

  async aboutToAppear() {
    this.list = await this.queryAll()
  }

  build() {
    Column({ space: 12 }) {
      Button('Add Task').onClick(async () => {
        const t: Task = { id: genId(), title: 'New Task', done: false, estimate: 3, finished: 0, createdAt: now(), updatedAt: now() }
        await this.insert(t)
        this.list = [t, ...this.list]
      })
      List() {
        ForEach(this.list, (t: Task) => ListItem() {
          Row() {
            Text(t.title).width('70%')
            Text(`${t.finished}/${t.estimate}`).align(Alignment.End).width('30%')
          }.padding(12)
        }, (t) => t.id)
      }
    }.padding(16)
  }

  private async getStore(): Promise<rdb.RdbStore> {
    const cfg: rdb.StoreConfig = { name: 'task.db', securityLevel: rdb.SecurityLevel.S1 }
    return rdb.getRdbStore(cfg, 1, {
      onCreate: async (db) => {
        await db.executeSql(`CREATE TABLE IF NOT EXISTS tasks(id TEXT PRIMARY KEY, title TEXT, done INTEGER, estimate INTEGER, finished INTEGER, createdAt INTEGER, updatedAt INTEGER)`)
      }
    })
  }

  private async insert(t: Task) {
    const db = await this.getStore()
    await db.insert('tasks', { id: t.id, title: t.title, done: 0, estimate: t.estimate, finished: 0, createdAt: t.createdAt, updatedAt: t.updatedAt })
  }

  private async queryAll(): Promise<Task[]> {
    const db = await this.getStore()
    const res = await db.query(new rdb.RdbPredicates('tasks'), ['id','title','done','estimate','finished','createdAt','updatedAt'])
    const out: Task[] = []
    while (res.goToNextRow()) {
      out.push({
        id: res.getString(0),
        title: res.getString(1),
        done: res.getLong(2) === 1,
        estimate: res.getLong(3),
        finished: res.getLong(4),
        createdAt: res.getLong(5),
        updatedAt: res.getLong(6)
      })
    }
    res.close()
    return out.sort((a,b)=> b.updatedAt - a.updatedAt)
  }
}

7.3 feature-reports(HAP):按任务统计

// apps/main-app/feature-reports/src/main/ets/abilities/ReportsAbility.ets
import UIAbility from '@ohos.app.ability.UIAbility'
import window from '@ohos.window'

export default class ReportsAbility extends UIAbility {
  onWindowStageCreate(ws: window.WindowStage) {
    ws.loadContent('pages/ReportsIndex', ()=>{})
  }
}
// apps/main-app/feature-reports/src/main/ets/pages/ReportsIndex.ets
import rdb from '@ohos.data.rdb'
import { Task } from 'core-kit'

@Entry
@Component
struct ReportsIndex {
  @State stats: { total: number; done: number; pomodoros: number } = { total: 0, done: 0, pomodoros: 0 }

  async aboutToAppear() {
    const list = await this.readTasks()
    this.stats = {
      total: list.length,
      done: list.filter(x=>x.done).length,
      pomodoros: list.reduce((s,x)=> s + x.finished, 0)
    }
  }

  build() {
    Column({ space: 16 }) {
      Text(`Tasks: ${this.stats.total}`).fontSize(22)
      Text(`Completed: ${this.stats.done}`)
      Text(`Pomodoros: ${this.stats.pomodoros}`)
    }.padding(16)
  }

  private async readTasks(): Promise<Task[]> {
    const db = await rdb.getRdbStore({ name: 'task.db', securityLevel: rdb.SecurityLevel.S1 }, 1)
    const res = await db.query(new rdb.RdbPredicates('tasks'), ['id','title','done','estimate','finished','createdAt','updatedAt'])
    const out: Task[] = []
    while (res.goToNextRow()) {
      out.push({
        id: res.getString(0), title: res.getString(1), done: res.getLong(2)===1, estimate: res.getLong(3),
        finished: res.getLong(4), createdAt: res.getLong(5), updatedAt: res.getLong(6)
      })
    }
    res.close()
    return out
  }
}

**你看到了吗?**两个 HAP 共享同一份数据结构(来自 core-kit,但各自有独立 Ability 与页面,互不渗透代码。这就叫模块自治

八、模块热更新:别上来就“真热”,先把“准热”铺稳

我先把“热更新”四个字拆开讲清楚,免得一上来就想“代码空中换新脑袋”,最后把自己吓个半死——或者踩到政策红线。

8.1 “热”的四个层次

  1. 开发期热重载(HMR):DevEco Studio 的预览/热重载能力,只在开发机。你改 ArkTS,预览面板或真机调试快速刷新——这不等于线上热更新
  2. 资源级热替换(推荐):把“可变的业务视图”抽象成 配置/模板/资源,例如服务器下发 JSON schema、远程文案、可替换图片包,客户端按协议渲染这是真正工程可控的“准热更新”。
  3. 模块粒度的差分发版:把新功能做成 独立 feature HAP,版本更新时只替换该 HAP(在应用市场/分发渠道)。从用户视角近似“某功能悄悄变了”,但它仍是一次正常安装更新,不越界、不偏门。
  4. 代码级热修复/热替换:运行时加载新 ArkTS/字节码替换。谨慎,不仅涉及平台安全策略,也牵扯签名信任与稳定性边界。能不用就不用,企业内管控场景也要充分评估。

我的建议2 + 3 组合拳,配合良好的模块边界与灰度策略,99% 的需求能满足,还不担心被“一纸政策打回原形”。

8.2 资源级“准热”的落地套路

思路:把“页面结构 + 交互关系”抽象成 Schema,由客户端的 渲染器 负责把 Schema 转换成 ArkUI 组件树。服务端变 Schema,下发后无需换代码即可更新展示。

// libs/core-kit/src/schema/view.ts  —— 仅示意
export type Node =
  | { type: 'text'; text: string; size?: number; color?: string }
  | { type: 'button'; text: string; action: string }
  | { type: 'row'; children: Node[] }
  | { type: 'column'; children: Node[] }

export interface PageSchema { root: Node }
// apps/main-app/entry/src/main/ets/runtime/Renderer.ets
import { PageSchema, Node } from 'core-kit'

@Component
export default struct Renderer {
  schema: PageSchema

  build() {
    this.renderNode(this.schema.root)
  }

  private renderNode(n: Node) {
    if (n.type === 'text') {
      Text(n.text).fontSize(n.size ?? 16).fontColor(n.color ?? '#E6EDF3')
      return
    }
    if (n.type === 'button') {
      Button(n.text).onClick(() => this.fire(n.action))
      return
    }
    if (n.type === 'row') {
      Row({ space: 8 }) { n.children.forEach(c => this.renderNode(c)) }
      return
    }
    if (n.type === 'column') {
      Column({ space: 8 }) { n.children.forEach(c => this.renderNode(c)) }
      return
    }
  }

  private fire(action: string) {
    // 触发行为:路由、打开 Ability、发请求……行为表也可配置化
  }
}
// apps/main-app/entry/src/main/ets/pages/RemotePage.ets
import http from '@ohos.net.http'
import Renderer from '../runtime/Renderer'
import { PageSchema } from 'core-kit'

@Entry
@Component
struct RemotePage {
  @State schema?: PageSchema

  async aboutToAppear() {
    const client = http.createHttp()
    const resp = await client.request('https://example.com/schema/home.json', { method: http.RequestMethod.GET })
    this.schema = JSON.parse(resp.result as string) as PageSchema
  }

  build() {
    if (!this.schema) {
      Text('Loading...')
      return
    }
    Renderer({ schema: this.schema })
  }
}

优点

  • 变化快、不触及可执行代码;
  • 与灰度/AB 测试天然兼容;
  • Schema 校验/回滚也做得更稳。
      缺点:渲染层需投入一次性成本。——但值得,尤其对多入口/多商店/多设备形态的产品。

8.3 模块差分发布(feature HAP)

把“容易变”的子域从 entry.hap 拆到 feature-*.hap把更新半径压缩到单模块
  配套治理

  • entry 里以“能力探测”决定是否展示导航入口(例如通过 Want/查询已安装模块);
  • 对“未安装”或“旧版本”模块,entry 引导用户前往应用市场升级;
  • 特性开关与 Schema 配合,实现“模块已更新但功能对少量人开放”的双轨灰度。

九、把“热更新”变“稳更新”:灰度、回滚与日志闭环

工程落地三板斧

  1. 灰度开关:所有“动态能力”的入口都挂开关(用户、版本、设备、地区),并默认关
  2. 回滚预案:远端配置“两条轨迹”——主轨与兜底轨。任何异常,毫不犹豫切回兜底
  3. 日志与指标:动态渲染、远端配置的每次命中与失败都埋点;对热变内容单独看崩溃率和停留时长。

别忘了,上线只是开始。没有可观测性与回滚能力的“热”,都只是“冒失”。

十、性能与体积:模块化不是“分文件”,而是“分账”

给你几条立刻能省事的清单:

  • 入口瘦身:entry 只保留路由、登录、壳页面;把笨重的详情、统计、可视化搬到 feature HAP。
  • 树摇友好:HAR 的 index.ets 控制导出面(不要一次性 export * 所有子模块),把“副作用”代码隔离。
  • 资源分仓:图片/字体/大资源不要放公共 HAR,按 HAP 各自携带。
  • 状态最小化:跨 HAP 的状态只放“必须共享”的部分(比如账号、全局配置、跨设备 key),其他本地化
  • Ability 即停即走:后台服务(ServiceExtensionAbility)精简生命周期,允许随时被系统回收,启动时靠持久化与 KV 恢复。

十一、常见坑清单:替你先撞一遍

  • 把 HAR 当 HAP 用:错。HAR 不能独立运行与更新,不要往 HAR 里塞“需要生命周期”的逻辑。
  • HAP 互引:几乎 100% 会循环依赖。上移到 HAR 或改走路由。
  • module.json5 滥开权限:最容易被审核运维卡住。按模块最小化申请
  • 资源命名冲突:多模块共用同名资源 ID,运行时谁覆盖谁说不清。命名加前缀,例如 report_chart_title
  • 把“热更新”当银弹:线上问题 8 成来自“没人管的动态配置”。灰度、回滚、可观测一个都不能少。

十二、从 0 到 1 的“模块化落地 Checklist”

这里给一个我真正在团队推行过的落地流程,你可以直接照抄:

  1. 切模块:先把当前应用的功能树摊开,划出 entry 与 N 个 feature-* 的边界。
  2. 提共性:把模型、协议、工具、通用 UI 抽成 core-kitui-kit 两个 HAR。
  3. 定规范:约定 import 路径(只从 index.ets 导入)、版本管理(统一 bump)、命名与资源前缀。
  4. 搭脚手架:模板化 HAP/HAR 的目录、module.json5oh-package.json5、lint、pre-commit。
  5. 验证链路:至少做一个“从 entry 导航到 feature,再回到 entry”的完整用户旅程,确保能力切分没漏。
  6. 灰度开关:引入最小可用的远端配置体系(哪怕是 JSON 静态托管),把动态入口挂上开关。
  7. 监控与回滚:确定日志方案与回滚开关规则,发布前压测“快速回滚”流程。
  8. 持续复盘:每次模块发布后开 30 分钟 Zoom,记录“哪里阻力最大”,不断收敛

十三、附:更多贴身代码片段(随取随用)

13.1 “路由聚合”与“能力探测”

// apps/main-app/entry/src/main/ets/common/featureGate.ts
export async function isFeatureInstalled(name: string): Promise<boolean> {
  // 伪代码:依工具链/SDK 能力,用 Want/BundleManager 查询
  // return bundleManager.hasModule(name)
  return true
}
// apps/main-app/entry/src/main/ets/pages/Home.ets
import { isFeatureInstalled } from '../common/featureGate'

@Entry
@Component
struct Home {
  @State showReports: boolean = false

  async aboutToAppear() {
    this.showReports = await isFeatureInstalled('feature-reports')
  }

  build() {
    Column({ space: 12 }) {
      Button('Tasks').onClick(()=> this.to('feature-tasks'))
      if (this.showReports) {
        Button('Reports').onClick(()=> this.to('feature-reports'))
      } else {
        Button('Reports (Install to use)').onClick(()=> this.toStore('feature-reports'))
      }
    }.padding(16)
  }

  private to(name: string) { /* 跳到对应 Ability */ }
  private toStore(name: string) { /* 引导到市场页 */ }
}

13.2 “类型只导入不落地”的小技巧

// apps/main-app/entry/src/main/ets/types/view.d.ts  —— 纯类型声明文件
export interface CardProps { title: string; subtitle?: string }
// 使用处
import type { CardProps } from '../types/view'   // 仅类型,编译后无运行时代码

13.3 “资源包替换”兜底方案

// 当远端图片不可用时,回落到内置资源
function useRemoteImage(url: string, fallbackRes: string) {
  // 伪代码:预加载失败则返回 $r(fallbackRes)
}

十四、问与答:你可能会追问的几个尖锐问题

Q:既然 feature HAP 能独立更新,为什么不把所有东西都拆成 feature?
  A:拆过头就会演变为“地鼠乐园”:入口处处引导安装、冷启动一地鸡毛、调试复杂度爆表。把“高频变更/强耦合少”的域拆出去,其它留在 entry,更均衡。

Q:资源级“准热”会不会限制交互表达力?
  A:看你 Schema 的抽象能力。先把八成需求(文本、布局、按钮、列表、简单逻辑)覆盖住,剩下两成用 feature 更新承接。不要迷信“无限动态”,那是维护噩梦。

Q:多 HAP 公用数据库,会不会冲突?
  A:关键在 模式与迁移。把表结构定义与迁移脚本放到 core-kit所有 HAP 统一调用,就不会“各写各的 SQL,各撞各的南墙”。

十五、收束:模块化是“可持续开发”的最小秩序

回看全文,我们干了三件实打实的事:
  ① 模块组织结构——用 entry + N 个 feature HAP 承载运行时;用 HAR(core-kit/ui-kit)承载复用;module.json5 正确声明能力与权限。
  ② import/export 体系——聚合导出、类型与值分离、只从公开入口导入,给树摇与巡检让路。
  ③ 模块热更新策略——开发期用热重载提效,发布期用资源级“准热” + feature 差分发版稳步演进,灰度/回滚/监控三件套护体。

写页面会让你变熟练;写模块会让你变可靠。当你把这些“底层秩序”真正握在手里,你就不是在做需求,而是在经营一套可持续的产品系统。这,就是全栈创作者的底气。

十六、彩蛋:一份“拿去即用”的最小样例清单

为了让你今晚就能开工(没开玩笑),我把本文关键文件的“最小版本”再浓缩一下,作为你的第一块地基:

  • apps/main-app/entry/module.json5:声明 EntryAbility + BgService,申请网络、通知权限;
  • apps/main-app/feature-tasks/module.json5:声明 TasksAbility
  • apps/main-app/feature-reports/module.json5:声明 ReportsAbility
  • libs/core-kit/index.ets:导出 Task / genId / now() / minutes()
  • apps/main-app/entry/src/main/ets/pages/Home.ets:入口导航;
  • apps/main-app/feature-tasks/src/main/ets/pages/TasksIndex.ets:增/查 Task;
  • apps/main-app/feature-reports/src/main/ets/pages/ReportsIndex.ets:统计视图;
  • apps/main-app/entry/src/main/ets/runtime/Renderer.ets:资源级“准热”渲染器;
  • oh-package.json5 + oh-package-lock.json5:把依赖锁死,保证每个人构建出来的都是同一个世界。

朋友们,纸上得来终觉浅,绝知此事要开仓。把结构搭起来,把第一个 feature 拆出去,把第一条 Schema 跑起来。你会发现:ArkTS 模块化没有神秘滤镜,它只是把“工程纪律”变成生产力
  那么,今天就动手吧——你的应用,不该再是一坨,而该是一支队列。💪

(未完待续)

Logo

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

更多推荐