鸿蒙组件化与模块化开发实践!
本文摘要:文章介绍了业务组件拆分和NPM模块封装的最佳实践。在组件拆分方面,建议按页面级、业务区域级和通用UI级三个维度进行拆分,并给出了订单详情页的组件拆分示例。在NPM模块封装方面,提出应封装与具体App弱耦合的功能模块,并给出了UI组件库和登录SDK的设计示例。最后强调公共UI组件库应避免业务逻辑,保持可配置性和独立性,通过一个通用弹窗组件示例展示了无业务逻辑的UI组件设计方法。
大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~
本文目录:
一、业务组件拆分方法:先拆“边界”,再谈“复用”
很多人一上来就问:“这个页面能不能拆成组件?”
其实更好的问题是:“这个东西以后有没有独立演化的可能?”
1.1 推荐的拆分维度
可以按这三层来想:
-
页面级(Page / Scene 级)
- 对应一个完整场景:订单详情、个人主页、登录注册。
- 一般保持相对“粗”,不强求通用。
-
业务区域级(Section 级)
- 页面中的“大块”:用户信息区域、订单商品列表、收货地址区。
- 这层最值得组件化:经常跨多个页面复用。
-
通用 UI 级(Atom / Molecule 级)
- Button、Tag、Card、ListItem、空状态、加载骨架等。
- 不懂业务,只关心样式 & 交互。
经验法则:
- 一个 UI 块如果出现在 ≥ 2 个页面,考虑拆组件。
- 一段逻辑如果出现在 ≥ 2 个组件,考虑抽到 业务 Hook / Service。
1.2 典型拆分示例:订单详情页
想象下一个订单详情页,有这些块:
- 订单状态头部(状态、倒计时、按钮)
- 收货信息
- 商品列表
- 金额汇总
- 底部操作栏
组件拆分可以这样:
OrderStatusHeaderAddressPanelOrderGoodsListOrderAmountSummaryOrderBottomBar
在页面里就变成:
@Entry
@Component
struct OrderDetailPage {
@State orderId: string = '';
@State order: OrderModel | null = null;
build() {
Column() {
if (!this.order) {
LoadingSkeleton()
return
}
OrderStatusHeader({ order: this.order })
AddressPanel({ address: this.order.address })
OrderGoodsList({ items: this.order.items })
OrderAmountSummary({ amountInfo: this.order.amountInfo })
Blank().flexGrow(1)
OrderBottomBar({ order: this.order })
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
要点:
页面负责“数据组织 & 生命周期”,业务组件负责“展示 & 交互局部逻辑”。
二、NPM 模块封装:业务能力“打包带走”
在鸿蒙生态里,其实更准确的说法是:用 NPM(ohpm)把业务能力封装成独立模块。
2.1 适合做 NPM 模块的内容
能抽出去的模块,一般有这些特征:
- 和具体 App 弱耦合,别的 App 也能用;
- 有清晰边界:IM SDK、支付 SDK、定位、埋点、权限、通用网络层;
- 更新节奏可以独立发布版本。
示例拆分:
@your-org/core-network:网络请求、异常封装、统一拦截@your-org/core-analytics:埋点 & 上报@your-org/ui-kit:公共 UI 组件库@your-org/feature-login:登录相关页面、逻辑(也可以放在业务仓中,通过子模块)
2.2 基本结构示例
一个简单的 UI 组件库模块结构(纯文本结构,方便你脑补):
ui-kit/
├─ src/
│ ├─ components/
│ │ ├─ Button/
│ │ │ ├─ Button.ets
│ │ │ └─ index.ets
│ │ ├─ Card/
│ │ │ ├─ Card.ets
│ │ │ └─ index.ets
│ │ └─ ...
│ └─ index.ets
├─ oh-package.json5
└─ README.md
src/index.ets 做统一导出:
export * from './components/Button'
export * from './components/Card'
// ...其他组件
使用方工程里:
import { UiButton, UiCard } from '@your-org/ui-kit';
2.3 API 设计原则
封装 NPM 模块时,一个非常重要的原则:只暴露“能力”,不暴露“实现细节”。
- 使用 函数 / 类 / 组件 做边界;
- 不要让业务方直接操作内部
@State; - 不从模块里随意导出内部工具函数(避免之后难以重构)。
比如一个登录 SDK 模块,可以像这样:
// sdk-login/src/index.ets
export async function loginWithPassword(username: string, password: string): Promise<LoginResult> {
// 内部封装:网络请求 + Token 存储 + 错误码转换
}
// 使用方
import { loginWithPassword } from '@your-org/sdk-login';
loginWithPassword(this.username, this.password)
.then(...)
.catch(...)
而不是让使用方自己去调一堆 post('/login')、存 token。
三、公共 UI 组件库设计:少一点“业务味”,多一点“积木感”
公共 UI 组件库是最容易“越写越乱”的地方——
不知不觉就掺进一堆业务逻辑,最后谁也不敢更新。
3.1 组件库的核心原则
-
无业务逻辑 / 或业务逻辑可插拔
- 组件只处理 UI、交互,不直接调接口。
- 如需业务行为,暴露
onConfirm、onClick回调让外部处理。
-
可配置 & 可拓展
- 避免硬编码文案、颜色;
- 通过
@Prop传参、slot 插槽等方式定制。
-
不“偷用”全局状态
- 不直接读写全局单例(比如 userStore);
- 需要的数据由上层传入,或通过独立注入机制。
3.2 一个典型的 UI 组件示例:通用弹窗
@Component
export struct ConfirmDialog {
@Prop title: string = '提示';
@Prop content: string = '';
@Prop confirmText: string = '确定';
@Prop cancelText: string = '取消';
@Prop onConfirm: () => void = () => {};
@Prop onCancel: () => void = () => {};
@State private visible: boolean = true;
build() {
if (!this.visible) {
return;
}
Stack() {
// 半透明背景
Rect()
.width('100%').height('100%')
.backgroundColor(Color.fromARGB(120, 0, 0, 0))
// 中心弹框
Column() {
Text(this.title)
.fontSize(18)
.fontWeight(FontWeight.Medium)
Text(this.content)
.fontSize(14)
.opacity(0.8)
.margin({ top: 8 })
Row() {
Button(this.cancelText)
.layoutWeight(1)
.onClick(() => {
this.visible = false;
this.onCancel();
})
Button(this.confirmText)
.layoutWeight(1)
.onClick(() => {
this.visible = false;
this.onConfirm();
})
}
.margin({ top: 16 })
}
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.width('70%')
.alignSelf(ItemAlign.Center)
}
}
}
业务使用时:
ConfirmDialog({
title: '删除确认',
content: '确定要删除该地址吗?',
confirmText: '删除',
cancelText: '取消',
onConfirm: () => this.deleteAddress(),
})
注意弹窗组件并不知道“删除接口”的存在,这就是干净的 UI 组件。
3.3 组件库的命名 & 分层
推荐按颗粒度分:
-
atoms:最小可复用单位- Button、Icon、Badge、Tag、Avatar
-
molecules:由多个 atom 组合而成- ListItem、FormItem、SearchBar
-
organisms:复杂 UI 模式- FilterPanel、BottomSheet、StepForm
目录示例:
ui-kit/
src/
atoms/
Button/
Tag/
Icon/
molecules/
ListItem/
CardItem/
organisms/
FilterPanel/
ConfirmModal/
index.ets
四、模块依赖管理:防止“互相拉扯”的关键一刀
做模块化最怕的就是:没管好依赖,结果所有模块环环相扣,谁也离不开谁。
4.1 建议的分层结构
可以参考一个常见的 4 层分层:
-
app(壳工程)
- 只负责启动、路由、依赖注入。
- 依赖:
feature-*、core-*、ui-*。
-
feature-*(业务模块)
- 订单、用户、支付、消息等。
- 依赖:
core-*、ui-*,不要互相依赖。
-
ui-*(UI 组件库)
- 通用 UI 组件。
- 依赖:尽量只依赖平台基础库。
-
core-*(底层能力)
- 网络、存储、日志、埋点、权限、配置。
- 不依赖 feature,也尽量不依赖 ui。
用箭头表示依赖关系:
app
↓
feature-*
↓
ui-* core-*
↘ ↓
(平台)
禁止的情况:
feature-order依赖feature-user,反过来feature-user又依赖feature-order(环依赖);ui-kit里 import 某个业务 feature;core-network去调用某个页面组件。
4.2 依赖规则的落地方式
- 命名约定 + code review:
例如约定feature-*不能 import 另一feature-*,在 MR 时人工检查。 - 文档 + 架构图:
把“谁能依赖谁”画成图,贴在仓库 README 里,避免新人乱依赖。
五、工程结构最佳实践:让新人一眼知道东西在哪儿
给一个相对“完整”的工程结构参考,你可以按自己项目规模删减。
app/
entry/
src/
main/ets/
pages/ # 仅页面壳,主要负责组装 feature
app.ets
resources/
build-profile.json5
module.json5
features/
feature-login/
src/...
feature-order/
src/...
feature-user/
src/...
core/
core-network/
core-storage/
core-config/
core-analytics/
ui/
ui-kit/
common/
models/ # 通用数据模型:User, Order, Address...
utils/ # 通用工具:日期格式化、金额格式化等
constants/ # 常量:路由名、事件名、枚举
oh-package.json5
5.1 Page 里只做“组装”,不搞“大杂烩”
一个 OrderDetail 页面在 app 层,大概长这样:
@Entry
@Component
struct OrderDetailPage {
private orderId: string = '';
build() {
FeatureOrderDetail({ orderId: this.orderId })
}
}
真正的业务逻辑和布局在 feature-order 模块里:
// features/feature-order/src/OrderDetailFeature.ets
@Component
export struct FeatureOrderDetail {
@Prop orderId: string = '';
@State order: OrderModel | null = null;
aboutToAppear() {
OrderService.getOrderDetail(this.orderId)
.then(order => this.order = order)
}
build() {
// 前面我们写过类似的布局,就不重复了
OrderDetailLayout({ order: this.order })
}
}
这样:
- app 层只管路由和“装配”,对业务不敏感;
- feature 层可以单独开发、维护,甚至独立发布成 NPM 包。
5.2 公共 Model / 常量归一
不要在每个模块里面自己定义一套 Order、User。
尽量在 common/models 里定义,并通过模块依赖共享。
// common/models/order.ets
export interface OrderModel {
id: string;
status: OrderStatus;
amountInfo: OrderAmountInfo;
// ...
}
业务模块都从这里 import,模型变更时有统一的源头。
5.3 约定优于配置
在多人协作和中长期维护中,约定比百花齐放更重要:
- 组件命名:
模块名 + 功能(如OrderStatusHeader) - 文件命名:每个组件一个目录,
xxx.ets + index.ets - 状态管理:临时局部状态用
@State,跨组件状态用专门的store/service,不要乱挂全局单例
最后一句:模块化是“防腐剂”,不是“银弹”
- 组件化是让 UI 更好复用、结构更清晰;
- 模块化是让业务边界清楚、更新不牵一发动全身;
- NPM 封装是让“好东西”在多个工程间复用。
但如果一开始就为了“高大上架构”拆得过细,落地开发会非常痛苦——
与其追求一开始就完美,不如保证“能分层、能抽离、能演进”。
如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~
更多推荐



所有评论(0)