项目分包方案:模块拆分、跨模块组件与方法引用规范

项目规模小的时候,所有页面、工具类、组件都写在一个Entry Module里,开发效率很高。但随着业务迭代,页面越堆越多,代码量涨到几十万行,问题就来了:改一个功能要全项目编译,启动速度变慢,多人协作时代码冲突频繁,公共组件散落在各个角落,想复用还得复制粘贴。这时候就需要做分包——把项目拆成多个模块,按业务和职责分层。本文基于最新鸿蒙Stage模型,讲清楚模块怎么拆、跨模块组件怎么复用、跨模块方法怎么调用,附带可直接落地的代码示例。

01 为什么要分包:单Module的问题

先说说单Module开发到底有哪些坑,为什么要花时间拆模块。

**编译速度慢。**项目越大,改一行代码全量编译的时间越长。本来改个文案要等两分钟,拆成模块后只编译当前模块,秒级出结果。

**代码耦合严重。**所有页面互相import,你引用我的工具类,我调用你的方法,最后牵一发而动全身,没人敢动老代码。

**公共组件复用难。**多个页面要用同一个按钮组件,要么复制粘贴到各个页面,要么硬塞到公共目录里,改一处要同步改好几份。

**权限管控不清楚。**所有页面都在一个模块里,谁都能改谁的代码,没有边界,新同学接手项目根本分不清哪些是公共的、哪些是业务的。

分包不是为了显得架构高级,而是为了解决这些实际问题。项目不大的时候不用硬拆,等代码量上来了、团队人多了,再拆也不迟。

02 模块拆分方案:按业务分层

鸿蒙项目拆分模块,一般按三层来分:公共基础层、业务功能层、主入口层。

**第一层:公共基础层(Common)**放所有业务模块都要用到的东西:网络请求封装、数据库工具、通用UI组件、常量定义、基础工具类。这一层只提供能力,不依赖任何业务模块。

**第二层:业务功能层(Feature)**按业务线拆成独立模块,比如用户中心模块、订单模块、支付模块、消息模块。每个业务模块是独立的HAR,只依赖公共基础层,不依赖其他业务模块。

**第三层:主入口层(Entry)**应用的入口Module,负责把各个业务模块组装起来,处理应用启动、全局配置、页面路由跳转。它依赖所有业务模块,是唯一可以看到所有模块的地方。

最终的项目结构大概是这样:

MyProject/
├── entry/                 # 主入口Module
├── common/                # 公共基础层(HAR)
│   ├── src/main/ets/components/   # 通用组件
│   ├── src/main/ets/utils/       # 工具类
│   └── src/main/ets/net/          # 网络封装
├── feature_user/          # 用户中心模块(HAR)
├── feature_order/         # 订单模块(HAR)
├── feature_pay/           # 支付模块(HAR)
└── build-profile.json5    # 全局配置

拆分的原则很简单:依赖方向只能从上往下走,Entry依赖业务模块,业务模块依赖公共模块。不能反过来,公共模块不能依赖业务模块,不然就循环依赖了。

03 跨模块组件引用:HAR静态共享包

公共组件放在common模块里,其他业务模块要用,怎么引用?用HAR(Static Shared Library)静态共享包。

先在common模块里导出组件:

// common/src/main/ets/components/CommonButton.ets
@Component
export struct CommonButton {
  @Prop text: string = '';
  onClick: () => void = () => {};

  build() {
    Button(this.text)
      .width('100%')
      .height(44)
      .backgroundColor('#0066FF')
      .fontColor('#FFFFFF')
      .borderRadius(8)
      .onClick(() => this.onClick())
  }
}

然后在common的Index.ets里统一导出:

// common/Index.ets
export { CommonButton } from './src/main/ets/components/CommonButton';
export { NetworkUtil } from './src/main/ets/utils/NetworkUtil';
export { Constants } from './src/main/ets/constants/Constants';

业务模块要用的时候,先在oh-package.json5里依赖common:

// feature_order/oh-package.json5
{
  "dependencies": {
    "common": "file:../common"
  }
}

然后就可以直接引用了:

// feature_order/src/main/ets/OrderList.ets
import { CommonButton } from 'common';

@Entry
@Component
struct OrderList {
  build() {
    Column() {
      CommonButton({ text: '提交订单', onClick: () => this.submitOrder() })
    }
  }

  submitOrder() {
    console.info('提交订单');
  }
}

HAR是静态打包的,最终会编译进主Module里。好处是简单直接,组件和工具类直接import就能用;缺点是如果多个模块都依赖同一个HAR,最终包里会有一份副本,包体积会稍微大一点。

04 跨模块方法调用:路由与接口注册

业务模块之间不能直接互相import,不然就耦合死了。比如订单模块要跳转到用户中心的个人主页,不能直接import feature_user里的页面。这时候就要用路由来解耦。

鸿蒙官方提供了Navigation路由,跨模块跳转只需要知道页面路径,不需要知道具体实现。

// common/src/main/ets/router/RouterConstants.ets
export class RouterConstants {
  static readonly USER_PROFILE = 'feature_user/ProfilePage';
  static readonly ORDER_DETAIL = 'feature_order/DetailPage';
  static readonly PAY_RESULT = 'feature_pay/ResultPage';
}

各个业务模块在自己的入口里注册页面路由:

// feature_user/src/main/ets/Index.ets
import { RouterConstants } from 'common';

// 页面注册到Navigation
export function registerUserPages(router: Navigation) {
  router.registerPage(RouterConstants.USER_PROFILE, (ctx) => {
    return UserProfilePage();
  });
}

订单模块要跳用户主页,只需要用路由常量,不用依赖用户模块:

// feature_order/src/main/ets/OrderList.ets
import { RouterConstants } from 'common';

@Entry
@Component
struct OrderList {
  @State pagePath: string = '';

  build() {
    Navigation(this.pagePath) {
      Button('查看用户信息')
        .onClick(() => {
          // 直接跳用户主页,不需要import用户模块
          this.pagePath = RouterConstants.USER_PROFILE;
        })
    }
  }
}

这样做的好处是:订单模块根本不知道用户模块的存在,只知道一个路径字符串。以后用户模块改了页面实现,或者拆成单独的HSP,订单模块完全不用改代码。

除了页面跳转,跨模块调用方法也是同理。公共层定义接口,各模块自己实现,主入口在启动时注册实现,其他模块通过接口调用,不直接依赖具体实现。

05 依赖管理与引用规范

分包之后,依赖关系一定要管清楚,不然很快又会回到全耦合的状态。

**第一,依赖方向只能从上往下。**Entry → 业务模块 → 公共模块。公共模块不能依赖业务模块,业务模块之间不能互相依赖。一旦发现两个业务模块要互相调用,说明逻辑抽得不对,应该把公共部分提到公共层去。

**第二,导出要收敛。**每个模块只导出对外暴露的东西,内部实现不要导出。common模块的Index.ets就是出口,只有写在这里的,其他模块才能用。没写在这里的,就算import路径能找到,也不应该用。

**第三,公共组件要设计通用。**放到common里的组件,不能带业务逻辑。CommonButton就是个纯按钮,不能写"提交订单"这种业务文案。业务特有的组件,就放在对应的业务模块里,不要硬塞到公共层。

**第四,常量和枚举统一管理。**接口地址、页面路径、状态码这些,都放到common的常量目录里。不要在业务模块里自己定义一套,不然同一个状态码两个地方写了不一样的值,排查问题都费劲。

06 落地注意事项

分包改造不是一蹴而就的,要逐步推进,别想着一天就拆完。

**先抽公共层。**第一步先把所有页面都在用的工具类、通用组件、常量抽到common模块里。这一步风险最小,改完编译能过,页面功能不变。

**再拆业务模块。**公共层抽完之后,按业务线一个个拆。先拆最独立的模块,比如设置页、关于页,这些和其他业务耦合最少,拆起来最容易。

**最后处理跨模块跳转。**所有模块拆完之后,把原来直接import的跳转改成路由跳转。这一步工作量最大,但是解耦效果最明显。

还有一个常见的坑:HAR静态包最终都会打包进主Entry,如果公共模块太大,包体积会涨。这时候可以考虑用HSP(动态共享包),按需加载,不打包进主包里。但HSP配置更复杂,小项目用HAR就够了,不用上来就追求动态化。

小结

项目分包的核心不是分多少个模块,而是理清依赖关系。公共层放通用能力,业务层按线拆分,入口层负责组装。跨模块用路由和接口通信,不直接互相引用。项目不大的时候不用硬拆,等代码量上来、团队人多了,再按这个思路逐步拆分。先抽公共,再拆业务,最后做路由解耦,每一步都能独立上线,不会一下把项目搞崩。

Logo

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

更多推荐