ArkTS 模块化到底有多香?为什么不把“依赖管理”和“热更新”一并拿下?
本文由鸿蒙开发者兰瓶Coding分享,从工程化角度探讨ArkTS模块化实践。文章分为三部分:首先厘清Bundle、HAP、HAR等核心概念;然后提出可扩展的项目结构方案,强调HAP承载运行态、HAR沉淀复用代码的原则;最后详细解析module.json5配置文件,展示主入口与功能模块的配置差异。作者主张通过合理的模块划分解决能力边界、依赖管理和热更新等实际问题,认为模块化是团队协作和多设备开发的关
我是兰瓶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.json5:HAP 级配置文件,定义模块名、类型(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
设计要点:
- HAP 只承载“运行态”东西:页面、Ability、资源、与少量贴近业务的逻辑。
- 跨 HAP 复用全部沉到 HAR:
core-kit放模型/协议/工具、ui-kit放通用组件,抵御复制粘贴。 - 应用级与模块级依赖分别管理:通用依赖放 apps 级,特定依赖放各自 HAP 的
oh-package.json5。 - 资源分治:每个 HAP 只带自己需要的图片/字符串,避免臃肿与冲突。
- 可插拔能力:把“新功能”做成独立
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-tasksimportfeature-reports的 util; - 正解:把 util 上移到
core-kitHAR,两个 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 “热”的四个层次
- 开发期热重载(HMR):DevEco Studio 的预览/热重载能力,只在开发机。你改 ArkTS,预览面板或真机调试快速刷新——这不等于线上热更新。
- 资源级热替换(推荐):把“可变的业务视图”抽象成 配置/模板/资源,例如服务器下发 JSON schema、远程文案、可替换图片包,客户端按协议渲染。这是真正工程可控的“准热更新”。
- 模块粒度的差分发版:把新功能做成 独立 feature HAP,版本更新时只替换该 HAP(在应用市场/分发渠道)。从用户视角近似“某功能悄悄变了”,但它仍是一次正常安装更新,不越界、不偏门。
- 代码级热修复/热替换:运行时加载新 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 配合,实现“模块已更新但功能对少量人开放”的双轨灰度。
九、把“热更新”变“稳更新”:灰度、回滚与日志闭环
工程落地三板斧:
- 灰度开关:所有“动态能力”的入口都挂开关(用户、版本、设备、地区),并默认关。
- 回滚预案:远端配置“两条轨迹”——主轨与兜底轨。任何异常,毫不犹豫切回兜底。
- 日志与指标:动态渲染、远端配置的每次命中与失败都埋点;对热变内容单独看崩溃率和停留时长。
别忘了,上线只是开始。没有可观测性与回滚能力的“热”,都只是“冒失”。
十、性能与体积:模块化不是“分文件”,而是“分账”
给你几条立刻能省事的清单:
- 入口瘦身: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”
这里给一个我真正在团队推行过的落地流程,你可以直接照抄:
- 切模块:先把当前应用的功能树摊开,划出
entry与 N 个feature-*的边界。 - 提共性:把模型、协议、工具、通用 UI 抽成
core-kit与ui-kit两个 HAR。 - 定规范:约定 import 路径(只从
index.ets导入)、版本管理(统一 bump)、命名与资源前缀。 - 搭脚手架:模板化 HAP/HAR 的目录、
module.json5、oh-package.json5、lint、pre-commit。 - 验证链路:至少做一个“从 entry 导航到 feature,再回到 entry”的完整用户旅程,确保能力切分没漏。
- 灰度开关:引入最小可用的远端配置体系(哪怕是 JSON 静态托管),把动态入口挂上开关。
- 监控与回滚:确定日志方案与回滚开关规则,发布前压测“快速回滚”流程。
- 持续复盘:每次模块发布后开 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 模块化没有神秘滤镜,它只是把“工程纪律”变成生产力。
那么,今天就动手吧——你的应用,不该再是一坨,而该是一支队列。💪
…
(未完待续)
更多推荐


所有评论(0)