大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~

一、业务组件拆分方法:先拆“边界”,再谈“复用”

很多人一上来就问:“这个页面能不能拆成组件?”
其实更好的问题是:“这个东西以后有没有独立演化的可能?”

1.1 推荐的拆分维度

可以按这三层来想:

  1. 页面级(Page / Scene 级)

    • 对应一个完整场景:订单详情、个人主页、登录注册。
    • 一般保持相对“粗”,不强求通用。
  2. 业务区域级(Section 级)

    • 页面中的“大块”:用户信息区域、订单商品列表、收货地址区。
    • 这层最值得组件化:经常跨多个页面复用。
  3. 通用 UI 级(Atom / Molecule 级)

    • Button、Tag、Card、ListItem、空状态、加载骨架等。
    • 不懂业务,只关心样式 & 交互。

经验法则:

  • 一个 UI 块如果出现在 ≥ 2 个页面,考虑拆组件。
  • 一段逻辑如果出现在 ≥ 2 个组件,考虑抽到 业务 Hook / Service

1.2 典型拆分示例:订单详情页

想象下一个订单详情页,有这些块:

  • 订单状态头部(状态、倒计时、按钮)
  • 收货信息
  • 商品列表
  • 金额汇总
  • 底部操作栏

组件拆分可以这样:

  • OrderStatusHeader
  • AddressPanel
  • OrderGoodsList
  • OrderAmountSummary
  • OrderBottomBar

在页面里就变成:

@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 组件库的核心原则

  1. 无业务逻辑 / 或业务逻辑可插拔

    • 组件只处理 UI、交互,不直接调接口。
    • 如需业务行为,暴露 onConfirmonClick 回调让外部处理。
  2. 可配置 & 可拓展

    • 避免硬编码文案、颜色;
    • 通过 @Prop 传参、slot 插槽等方式定制。
  3. 不“偷用”全局状态

    • 不直接读写全局单例(比如 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 层分层:

  1. app(壳工程)

    • 只负责启动、路由、依赖注入。
    • 依赖:feature-*core-*ui-*
  2. feature-*(业务模块)

    • 订单、用户、支付、消息等。
    • 依赖:core-*ui-*不要互相依赖
  3. ui-*(UI 组件库)

    • 通用 UI 组件。
    • 依赖:尽量只依赖平台基础库。
  4. 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 / 常量归一

不要在每个模块里面自己定义一套 OrderUser
尽量在 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 封装是让“好东西”在多个工程间复用。

但如果一开始就为了“高大上架构”拆得过细,落地开发会非常痛苦——
与其追求一开始就完美,不如保证“能分层、能抽离、能演进”。

如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~

Logo

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

更多推荐