在 DevEco Studio 里新建 HarmonyOS 工程时,默认给你生成一个 entry 模块,很多开发者就这么一路写下去,页面越来越多、功能越堆越重,等到要上架才发现包体大得离谱、团队协作天天冲突、一个修改就要全量发版。我之前在一个个人记账项目里就踩过这个坑,后来认真梳理了 Feature 模块的玩法,才真正体会到它为什么被叫做动态化开发的“瑞士军刀”——它不是某个单一功能,而是一整套把 App 拆散、按需装配、动态下发的能力组合。这篇文章我就拿这个记账系统的实战过程,把 Feature 模块从设计思路到落地代码完整拆一遍,给正在被“模块过多”“发版困难”“包体膨胀”折磨的 HarmonyOS 开发者一个可以直接抄作业的参考。
1. Feature 模块到底是什么:先搞清楚它的定位
1.1 一个 App,三种模块形态
HarmonyOS NEXT 的应用工程里,模块大致有三种形态:entry、feature、shared。
entry 是一个 App 的门面,也是整个应用启动时默认加载的入口模块,每个应用有且只能有一个 entry。feature 则是在 entry 之外按业务维度拆出来的功能模块,一个应用可以有多个 feature,比如电商 App 里的“购物车”“订单详情”都可以各拆一个 feature。shared 是动态共享包 HSP(HarmonyOS Shared Package),它不独立运行,专门给其他模块提供公共代码、公共资源和公共 UI 组件。
形象一点说,entry 是“总装车间”,负责把各种零件拼起来;feature 是“功能刀头”,切菜有菜刀、开瓶有开瓶器;shared 是“刀柄里的轴”,所有刀头能转动、能弹出的基础机构都靠它。所以我说它是瑞士军刀:一个刀柄(shared)挂多种刀头(feature),平时不用的刀头收在壳里(按需下载),要用的时候“咔哒”一声弹出来(动态加载)。这就是动态化开发的核心结构支撑。
1.2 为什么动态化开发离不开 Feature 模块
很多刚接触 HarmonyOS 的开发者会把“动态化”直接等同于 Web 加载或者解压远程资源包,其实在 HarmonyOS 的生态里,动态化是一整套链路,而 Feature 模块就是这条链路的地基。
我先说一个真实的包体数据。在记账这个项目里,如果把统计分析、预算管理、用户认证这三块都塞进 entry,编译出来的 HAP 大概有 47MB。拆成三个 Feature 模块后,entry 的安装包降到 20MB 左右,统计和预算这两个高频低频不一的模块被标记成按需分发,用户首次安装只需要拿到核心记账功能,等真正点击“统计报表”入口时再去下载对应模块。这个体验的差异非常明显:首次安装变快、内存占用变小、冷启动时间也缩短了大概 300ms。
动态化另外一个层面是“动态发布”。Feature 模块在 AppGallery Connect 上可以单独发版本、单独回退。我不用因为预算模块改了一个图表颜色,就把整个 App 重新提审一遍。团队并行开发时,两个人的代码仓库也可以做到物理隔离,各自维护各自的 module,合代码时只在 entry 和 hsp 层面做极少的集成操作。这些优势在没有模块化结构时是根本做不到的。
1.3 模块划分的决策依据
拆模块不是拆得越碎越好,拆太多会导致工程管理成本上升、依赖关系混乱。我自己在项目里总结了一套判断标准:
- 是否独立使用:这个功能用户可能在主流程外单独访问,比如“预算设置”,可以独立成模块。
- 是否低频重资源:比如统计报表涉及大量图表库,会显著增加包体,放主包里不划算。
- 是否需要独立发版:如果某个功能迭代频率高,或者经常要临时调整,拆出来更好。
- 是否团队隔离:多团队协作时,模块边界就是团队边界,减少互相踩脚。
在实际项目中,我倾向于把“用户认证”“统计分析”“预算管理”拆成独立 Feature,而“账单记录”“分类管理”这种主流程高频功能继续留在 entry 或放到 HSP 里做复用。这样的边界既保证了动态下发的能力,又不至于让工程碎片化。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 模块划分与工程搭建:动手前想清楚
2.1 开发环境要求
在开始创建 Feature 模块之前,先确认环境,这能省掉后面很多莫名其妙的报错。我的环境如下:
- DevEco Studio 5.0.0 及以上版本(建议用最新正式版)
- HarmonyOS NEXT SDK,API 12 及以上
- 本机 Node.js 18 以上,用于 ohpm 依赖管理
- 如果涉及真机验证,需要 HarmonyOS NEXT 5.0 系统的真机设备,以及注册好的 AppGallery Connect 项目
这里提醒一下,Feature 模块对开发工具版本有隐性的要求,老版本的 DevEco Studio 可能不支持某个模块类型,或者无法识别 module.json5 里的新字段。我见过有人用 DevEco Studio 4.x 打开新工程,feature 模块创建不出来,控制台直接报“Unsupported module type”。遇到这类问题,先升级工具再排查别的。
2.2 用 DevEco Studio 创建 Feature 模块
创建一个 Feature 模块在操作上很简单,我在工程根目录右键选择 New > Module,然后在模板列表里选择 Empty Ability,这与创建 entry 时用的是同一个模板。区别在于,创建过程中要手动修改模块类型和名字。
关键点在创建后第一件做的事:打开 module.json5,把 module.type 改成 "feature",对于公共能力层我单独创建一个 HSP 类型的模块,module.type 改成 "shared"。如果不改,默认创建出来的还是一个等价的 entry 类型模块,行为上会有很多奇怪的问题。
举个例子,我把统计分析模块命名为 feature_stats,然后它的 module.json5 大概是这样的:
json5复制{
"module": {
"name": "feature_stats",
"type": "feature",
"description": "$string:module_desc",
"mainElement": "StatsAbility",
"deviceTypes": ["phone", "tablet"],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "StatsAbility",
"srcEntry": "./ets/statsability/StatsAbility.ets",
"description": "$string:StatsAbility_desc",
"icon": "$media:icon",
"label": "$string:StatsAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": ["entity.system.home"]
}
]
}
]
}
}
这里有几个字段值得重点看。deliveryWithInstall 控制模块是否随主包一起安装,true 表示随主包分发,false 表示按需分发,用户使用时再下载。installationFree 表示是否支持免安装运行,对于原子化服务会用到,普通应用保持 false 即可。这些配置直接决定后面的动态化能力。
2.3 模块间的依赖关系怎么搭
在 HarmonyOS 工程里,模块依赖关系有一个很重要的点:entry 可以依赖 feature,feature 之间通常不互相依赖,需要共享的东西下沉到 HSP 里。entry 和所有 feature 都可以依赖 shared 模块。
我在记账项目里的依赖结构是这样:
- hsp_common:网络封装、数据库工具类、通用图表组件、统一颜色和主题资源
- entry:依赖 hsp_common,承载首页、流水列表、记账入口
- feature_stats:依赖 hsp_common,承载统计分析报表
- feature_budget:依赖 hsp_common,承载预算管理
- feature_auth:依赖 hsp_common,承载用户认证和登录
这样的设计有几个直接的好处:第一,公共逻辑只写一份,改一处全局生效;第二,feature 模块保持轻量,只关注自己的业务;第三,entry 不依赖具体 feature 的实现细节,只是按 want 把用户带到对应模块,耦合度极低。
配置依赖的方式是在用到方模块根目录的 oh-package.json5 里增加 dependencies,比如 feature_stats 依赖 hsp_common,写法是:
json5复制{
"dependencies": {
"hsp_common": "file:../hsp_common"
}
}
注意这是本地路径引用,必须用 file: 前缀。如果构建时提示找不到模块,去检查一下路径是否正确,以及 hsp_common 有没有在 settings.json5 的 modules 列表里注册。
3. 核心案例:个人记账系统的 Feature 模块落地
3.1 需求拆解与模块边界
这个记账项目是一个包含前后端的个人账单服务系统,后端用 AppGallery Connect(AGC)提供认证、云数据库、云存储能力。业务流程大概是:用户登录后,在首页快速记一笔,账单数据进入云数据库;可以按分类查看历史流水;可以看统计报表(月度支出、分类占比、趋势曲线);可以设置每个月总预算和各分类预算。
从功能频率看,记一笔和查看流水是最高频的,必须常驻主模块。统计分析一个月可能才打开几次,而且图表组件体积不小。预算管理平时用不上,每个月发工资那天才会去调一次。这两块是天然的 Feature 模块候选。用户认证虽然每次启动都用到,但它涉及独立的升级和异常处理逻辑,拆出来对发版有好处,所以我把它也做成 feature。
模块边界定了之后,一个很有价值的工作是梳理界面跳转图。首页五个入口:记一笔(entry)、账单流水(entry)、统计分析(feature_stats)、预算管理(feature_budget)、个人中心(feature_auth)。入口都在,但底层模块未必都在本地。跳转前要判断目标模块是否已经安装。
3.2 在 Feature 模块中实现“统计分析”页面
统计分析模块是我这边最有代表性的一个 Feature。它自身是一个完整的 UI 流程,有入口页和二级详情页,内部有自己的路由。在 feature_stats 模块中,我新建了两个页面:StatsIndexPage 和 CategoryDetailPage,并在 module.json5 的 main_pages.json 中登记页面路径。
json5复制{
"src": [
"pages/StatsIndexPage",
"pages/CategoryDetailPage"
]
}
模块内的页面跳转不需要跨模块,直接用 Navigation 或者 router.pushUrl 都行。核心业务包括月度收支统计、分类占比环形图、近六个月趋势折线图。图表组件我封装在 hsp_common 中,这样预算模块也能用,不用重复引入图表库。
页面结构上,我在 StatsIndexPage 顶部放月份选择器,中间放汇总卡片,下面放图表。数据从 AGC 云数据库按日期范围查询后,在前端聚合。加载过程中有个骨架屏,这个也在 hsp_common 里做了通用组件。
从代码分工来说,feature_stats 不关心数据是怎么从网络来的,只调用 hsp_common 暴露的数据仓库接口。这样后期如果要把本地数据库换成 CDN 或者其他后端,只需要改 hsp_common 内部实现,对 feature_stats 完全透明。
3.3 用 HSP 沉淀公共能力
HSP 在动态化里的重要性经常被低估。很多人在 entry 和 feature 里重复写网络请求工具,或者把字符串、颜色资源拷来拷去。靠人品维护的副本多了,总有一天会翻车。我建议公共能力一律下沉到 HSP,包括网络封装、登录态管理、数据库工具、通用组件、主题资源。
hsp_common 的 module.json5 里 type 要改成 shared,且没有 mainElement 和 abilities,因为它不是一个可以被直接启动的模块。它的主代码放在 src/main/ets 下,通过 export 暴露接口。比如我导出一个日期工具类:
typescript复制// hsp_common/src/main/ets/utils/DateUtil.ets
export class DateUtil {
static formatMonth(date: Date): string {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
return `${y}-${m}`;
}
}
其它模块使用的时候正常 import 即可。这里有个细节:HSP 对外暴露的接口必须显式用 export 声明,没有导出的函数在模块外是访问不到的,这和普通 HAR 静态共享包的行为一致。
我在这个项目里把图表组件也放在 hsp_common 里,用 @Component 封装了一个简单饼图组件,接收数据数组,然后渲染。这样 statistic 和 budget 模块都可以用,而且图表库只编译一次,不会在两处各打一份。
3.4 动态加载与远程配置的联动
真正让 Feature 模块“动起来”的关键,是把它和 AGC 的按需分发、远程配置联动起来。这套联动我来拆开讲。
第一步,在 AGC 的“分发与发布”里把 feature_stats 和 feature_budget 配置为“按需发布”,deliveryWithInstall 设为 false。这样打 Release 包时,这些模块不会进入主安装包。
第二步,在 AGC Remote Config 里配置功能开关,比如一个 JSON:
json复制{
"show_stats": true,
"show_budget": true,
"budget_version": "1.2.0"
}
App 启动时我从 Remote Config 拉取配置,把首页入口的显隐控制起来。如果 show_budget 为 false,就不显示预算管理入口,等于后台可以远程决定某个功能是否开放,这一步是动态化开发里非常实用的场景。
第三步,关键跳转逻辑。用户点击统计入口时,不能直接 startAbility,要先检查本地模块是否存在。我封装了一个工具方法,用 bundleManager 查询:
typescript复制import { bundleManager } from '@kit.AbilityKit';
function isModuleInstalled(bundleName: string, moduleName: string): boolean {
let result = false;
try {
const bundleInfo = bundleManager.getBundleInfoSync(bundleName,
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_MODULE);
result = bundleInfo.moduleInfos?.some(m => m.moduleName === moduleName) ?? false;
} catch (err) {
result = false;
}
return result;
}
如果模块未安装,我触发 requestInstall 下载安装,装完再跳转。代码结构大致是这样的:
typescript复制async function openStats() {
const moduleName = 'feature_stats';
const installed = isModuleInstalled('com.example.myaccountbook', moduleName);
if (installed) {
gotoFeatureModule(moduleName, 'StatsAbility');
} else {
await requestInstallModule(moduleName);
gotoFeatureModule(moduleName, 'StatsAbility');
}
}
整个链路跑通之后,用户看到的体验就是一个普通按钮,点了之后等一两秒,新功能就打开了。对于开发者来说,后台发布一个新版本模块,用户无感知地就更新了功能,这才是动态化开发的真正价值。
4. 常见问题与排查技巧实录
4.1 常见报错速查表
做 Feature 模块开发的路上,坑确实不少。我整理了一个速查表,都是我实际踩过的,直接对照解决:
| 报错/现象 | 可能原因 | 解决办法 |
|---|---|---|
| 安装应用时提示“模块不兼容” | module.json5 的 deviceTypes 与真机不匹配 | 检查 deviceTypes 是否包含 phone |
| 跳转 Feature 页面无反应但无报错 | Want 中的 moduleName 与模块名不一致 | 确认 module.json5 的 name 字段 |
| 提示“can not find ability” | abilityName 写错或模块未安装 | 用 bundleManager 查询模块状态 |
| HSP 的接口调用不到 | 没有在 oh-package.json5 声明依赖 | 在 dependencies 中加 file: 引用 |
| 远程配置拉取失败 | 未初始化 AGC 或配置 Key 错误 | 检查 agconnect-services.json 是否配置正确 |
| 包体没有变小 | 按需分发的模块在 AGC 中仍标记为随包安装 | 到 AGC 后台把模块发布策略改为按需 |
| 未安装模块运行时崩溃 | 直接跳转未安装模块的 Ability | 先检查安装状态再触发 requestInstall |
4.2 容易踩的隐性坑
第一个隐性坑是页面资源的路径问题。在 Feature 模块中,如果页面资源引用了其他模块的资源名而没有在依赖里声明,编译时会报找不到资源。但更隐蔽的是运行时资源名冲突,两个模块里都定义了同名字符串,互相覆盖。解决方式是资源命名统一加模块前缀,比如 feature_stats 里的字符串都以 stats_ 开头,hsp_common 里的都以 common_ 开头。这个约定一定要从第一天就执行,不然后期排查会非常痛苦。
第二个坑是跨模块跳转时,目标模块可能被系统回收。在 HarmonyOS 的多任务机制下,Feature 模块的 Ability 退出后,数据状态并不会保留。我们之前遇到过一个场景:用户从统计模块切到后台,再回来时发现页面是新的,状态丢了。后来我把页面状态做了持久化,关键筛选条件存到轻量级偏好数据库中,每次进入时恢复。
第三个坑是开发调试时按需分发模块的安装问题。真机上如果 feature 模块没有随主包安装,而你是通过 DevEco Studio 直接 Run 的,有时候会出现模块找不到的情况。我通常会把所有模块临时改成 deliveryWithInstall: true 来跑联调,等整体稳定后再改回按需。这种做法在团队合作时要同步沟通好,不然很容易出现版本混乱。
4.3 性能与包体的平衡心得
在优化包体时,我做了几次收包测试。最理想的结构里,主包只包含 entry 和 hsp_common 涉及的公共库,所有重型第三方依赖都下沉到需要它的 feature 模块里。比如图表库 ohos-charts,我放在 hsp_common 里,因为它有两个模块都要用,但相比引入两份,HSP 只有一份,这一点在包体优化上已经赚到了。
不过有一个误区要提醒,HSP 不是万能的。它的代码会被打进所有依赖它的模块的运行时,但在“安装到设备”这个维度上,它仍然是需要的,放在 HSP 里的代码体积并不会直接消失。真正让包体瘦身的是“按需分发的 Feature 模块”——它压根不出现在首次安装包里,这部分体积才算真正省下来了。
性能层面,Feature 模块的动态加载不是零成本。requestInstall 涉及网络下载,再快的网络也会有一秒以上的延迟。在这段时间里如果不做用户反馈,体验会非常差。我在入口点击后先弹一个 loading 动画,文案是“正在准备功能模块”,下载完成后再自动跳转。实测下来,这种方式比安装完再让用户点一次按钮的转化率高出不少。
5. 动态化开发还能怎么玩:进阶扩展
5.1 从 Feature 模块到服务卡片
Feature 模块能承载的动态化能力,不只有页面。HarmonyOS 的服务卡片(Form)也可以基于 Feature 模块的实例提供数据。比如在记账项目里,我让用户在主屏添加一个“本月预算剩余”卡片,卡片本身不写业务代码,只负责展示;实际数据由预算 Feature 模块提供。
这意味着用户不需要点开 App,就能在桌面看到预算进度,预算一旦超支,卡片红色预警。当用户点击卡片时,再拉起 feature_budget 的详情页。这种“轻展示 + 重功能”的组合,把动态化开发的触角延伸到了系统桌面层面,用户感知非常强。开发量其实不大,卡片代码也就一两百行,但留存和活跃提升非常明显。
这块的开发流程是:在 feature_budget 模块中新增一个 FormExtensionAbility,在 module.json5 中声明 extensionAbilities,资源配置里添加 form_config.json,再写一个卡片渲染入口。卡片的数据通过数据源回调拉取,拉到后更新卡片内容。
typescript复制export default class BudgetFormAbility extends FormExtensionAbility {
onAddForm(want: Want) {
let formData = new Record<string, Object>();
formData['budgetRemain'] = this.loadBudgetRemain();
return {
data: JSON.stringify(formData)
};
}
private loadBudgetRemain(): string {
// 从数据仓库读取预算剩余,返回格式化的字符串
return '剩余 ¥2,580';
}
}
这个能力再往后走,可以结合后台推送,让服务卡片实时更新,真正做到“用户没打开 App,但功能已在身边”。
5.2 团队协作与发布策略
Feature 模块的工程结构,天然适合多团队并行。以前一个仓库多人协作,经常出现 merge 冲突;拆模块之后,前端团队只需要在集成节点合并,日常开发都在自己的模块目录里进行。模块的负责人可以独立提测,A 团队改统计模块,B 团队改预算模块,完全互不影响。
发布策略上,Feature 模块给灰度发布带来了很大的弹性。我可以在 AGC 后台把新版本预算模块只发布给 10% 的用户,观察崩溃率、功能使用率,确认没问题后再放量。如果中途发现严重问题,可以把线上版本回退到上一个版本而不用动主包。
还有一个实战建议:每个 Feature 模块必须要有版本号管理,并在模块内自行处理“版本不匹配”的兼容问题。比如统计模块依赖 hsp_common 里的一个接口,而主包的 hsp_common 是个旧版本没有这个接口,统计模块在启动时就该自己检测并给出提示,而不是等页面渲染时报错。我习惯在 HSP 里放一个版本常量,模块启动时对比版本号,不匹配则提示更新。这个做法虽然简单,但在真实环境中救过我很多次。
5.3 把动态化开发当成长线能力去建设
很多人把 Feature 模块当做一个工程技巧,用完了就扔到一边。但我在维护记账项目大半年之后发现,它是一个需要持续建设的“长线能力”。模块边界、依赖规范、资源命名、构建脚本、发布流程,这些都应该是团队约定,沉淀成文档。新人进来先看规范,再上手写模块,踩坑的概率能减少一多半。
我有两个很直观的体会:第一,模块拆分不是一锤子买卖,每个新功能上线前都要问一句“这功能该放哪个模块”;第二,动态化开发不是只为了一时包体优化,它让产品运营获得了更灵活的手段——可以远程关闭问题功能、可以按用户画像差异化发布、可以进行功能灰度实验。这些能力叠加起来,才是动态化的真正收益。
如果你现在的 App 还把所有代码堆在 entry 里,我建议你找个适合的边界,先拆一个 Feature 模块出来跑通完整链路,感受一下“按需加载”是什么体验。工具链和 AGC 这些配套现在都已经很成熟了,真正需要花时间的,是转变产品设计和模块组织的思维方式。在 HarmonyOS 的生态里,这种动态化、模块化的能力会越来越重要,越早搭好底子,后面越省心。
