HCompass -- 像搭积木一样构建鸿蒙应用

0 阅读7分钟

模块化、可复用、开源的 HarmonyOS 快速开发框架


为什么需要 HCompass?

鸿蒙生态正在高速发展,越来越多的开发者投入到 HarmonyOS 应用开发中。但在实际项目中,我们常常面临这些痛点:

  • 每个新项目都要从零搭建基础架构,重复造轮子
  • 网络请求、路由导航、状态管理等通用能力缺乏统一封装
  • 多人协作时代码风格不一致,模块边界模糊
  • 业务功能难以跨项目复用,积累的经验无法沉淀

HCompass 正是为解决这些问题而生。它不是一个 UI 组件库,而是一套完整的应用开发框架 -- 帮你把地基打好,让你专注于业务本身。


核心理念:功能包即积木

HCompass 的核心思想很简单:把应用拆成一个个独立的"功能包",像搭积木一样组合它们。

每个功能包都是一个自包含的业务模块,拥有自己的页面、ViewModel、服务和数据模型。需要登录功能?放入 auth 功能包。需要用户中心?放入 user 功能包。功能包之间通过契约层解耦,互不依赖,随时可以拆卸和替换。

你的应用 = Entry(入口) + 若干功能包(积木)

四层架构,职责分明

HCompass 采用清晰的四层架构设计:

层级职责关键特征
Entry应用入口初始化框架、注册功能包、配置路由
Packages业务功能包独立开发、独立测试、可跨项目复用
Shared共享契约定义服务接口和类型,功能包之间的"协议"
Core框架核心与业务无关的通用能力,可直接迁移到任何项目

这种分层设计带来的好处是:Core 层可以直接复用到你的下一个项目,Packages 层的功能包可以按需组合,Shared 层确保模块之间的通信有据可依。


开箱即用的核心能力

依赖注入(DI)

轻量级 DI 容器,让功能包之间彻底解耦:

// 在 Shared 层定义契约
interface IUserRepository {
  getUserInfo(id: string): Promise<User>;
}

// 在功能包中实现并注册
register<IUserRepository>(ServiceKeys.USER_REPO, new UserRepositoryImpl());

// 在任何地方解析使用
const userRepo = resolve<IUserRepository>(ServiceKeys.USER_REPO);

不再需要硬编码依赖关系,功能包只依赖契约,不依赖具体实现。

导航系统

统一的路由管理,支持路由守卫:

// 路由跳转
NavigationService.push(UserRoutes.PROFILE, { userId: "123" });

// 路由守卫 -- 未登录自动跳转登录页
GuardManager.addGuard(new LoginGuard());

支持登录守卫、权限守卫、条件守卫,灵活组合,按优先级执行。

网络请求

基于 Axios 封装,告别重复的请求模板代码:

// 继承 BaseNetWorkViewModel,自动管理 loading/error/success 状态
@ObservedV2
class UserProfileViewModel extends BaseNetWorkViewModel<User> {
  async fetchData(): Promise<void> {
    await this.request(() => this.userRepo.getUserInfo(this.userId));
  }
}

内置拦截器链、统一错误处理、分页逻辑,你只需关注业务数据。

设计系统

统一的设计令牌,确保 UI 一致性:

// 百分比常量,告别魔法数字
Row() { ... }.width(P100).height(P50)

// 间距组件,统一视觉节奏
Column() {
  Text("标题")
  SpaceVerticalMedium()  // 12vp 间距
  Text("内容")
}

基础父类

三个 ViewModel 基类覆盖 90% 的页面场景:

基类适用场景
BaseViewModel通用页面,提供生命周期管理
BaseNetWorkViewModel网络请求页面,自动处理 loading/error/success
BaseNetWorkListViewModel分页列表页面,内置下拉刷新和上拉加载

实战案例:聚合登录功能包

光说架构不够直观,我们直接看官方实现的 login 功能包 -- 一个支持华为账号一键登录、微信、支付宝、短信验证码的聚合登录模块,完整展示了功能包从契约定义到页面渲染的全流程。

功能包目录结构

packages/login/
├── LoginModule.ets                    # 模块生命周期(DI注册 + 路由 + 守卫)
├── view/
│   ├── LoginPage.ets                  # 主登录页(华为一键登录 + 三方登录)
│   └── SmsLoginPage.ets               # 短信验证码登录页
├── viewmodel/
│   ├── LoginViewModel.ets             # 登录业务逻辑
│   └── SmsLoginViewModel.ets          # 短信登录逻辑
├── components/
│   ├── AnimatedAuthPage.ets           # 认证页面基础布局
│   ├── PhoneInputField.ets            # 手机号输入组件
│   ├── VerificationCodeField.ets      # 验证码输入组件
│   ├── UserAgreement.ets              # 用户协议组件
│   └── ...
├── navigation/
│   ├── LoginNav.ets                   # 登录页导航构建器
│   └── SmsLoginNav.ets                # 短信登录页导航构建器
├── services/
│   └── AuthNavSvcImpl.ets             # 导航服务实现
└── models/
    └── Constant.ets                   # 三方 APP_ID 配置

View / ViewModel / Service / Navigation 各司其职,结构一目了然。

第一步:在 Shared 层定义契约

功能包对外暴露的能力,全部通过 Shared 层的接口契约来声明:

// shared/contracts -- 导航服务契约
export const AUTH_NAV_SVC_KEY: string = "authNavService";

export interface IAuthNavSvc {
  toLogin(): void;       // 跳转登录页
  toSmsLogin(): void;    // 跳转短信登录页
}

// shared/contracts -- 路由常量
export class AuthRoutes {
  static Login = "auth/login";
  static SmsLogin = "auth/sms-login";
}

其他功能包只需要依赖这些接口,不需要知道登录页长什么样、用了哪个 SDK。

第二步:注册模块 -- 一个类搞定 DI、路由、守卫

LoginModule 实现 FeatureModule 接口,框架会在启动时自动调用:

export class LoginModule implements FeatureModule {
  readonly moduleId: string = 'auth';
  readonly moduleName: string = '认证模块';
  readonly version: string = '1.0.0';
  readonly dependencies: string[] = [];

  // 注册服务到 DI 容器
  registerServices(container: Container): void {
    container.register<IAuthNavSvc>(AUTH_NAV_SVC_KEY, () => new AuthNavSvcImpl());
  }

  // 注册页面路由
  registerRoutes(registry: RouteRegistry): void {
    registry.register(AuthRoutes.Login, loginNavBuilderWrapper);
    registry.register(AuthRoutes.SmsLogin, smsLoginNavBuilderWrapper);
  }

  // 注册路由守卫 -- 未登录自动拦截
  registerGuards(navigationService: NavigationService): void {
    navigationService.registerGuard(new AuthGuard());
  }
}

三个方法,把服务注入、路由注册、登录守卫全部声明完毕。当用户访问受保护的页面时,AuthGuard 会自动检查登录状态,未登录则跳转到登录页:

class AuthGuard implements RouteGuard {
  readonly name: string = 'AuthGuard';
  readonly priority: number = 100;  // 优先级最高

  canActivate(context: RouteContext): boolean {
    return getUserState().isLoggedIn();  // 已登录放行,未登录拦截
  }

  onReject(context: RouteContext): void {
    // 拦截后自动跳转登录页
    navigation?.navigateTo(AuthRoutes.Login, context.params);
  }
}

第三步:ViewModel 封装业务逻辑

LoginViewModel 继承 BaseViewModel,集中处理多种登录方式:

@ObservedV2
export default class LoginViewModel extends BaseViewModel {
  @Trace anonymousPhone: string = "";  // 华为账号匿名手机号

  // 华为账号一键登录控制器
  huaweiLoginController: LoginWithHuaweiIDButtonController =
    new LoginWithHuaweiIDButtonController()
      .setAgreementStatus(AgreementStatus.ACCEPTED)
      .onClickLoginWithHuaweiIDButton((error, response) => {
        this.handleLoginWithHuaweiIDButton(error, response);
      });

  // 微信 OAuth 登录
  async onWechatLoginClick(): Promise<void> {
    const share: ShareWxSdk = new ShareWxSdk(APP_ID_WX);
    const res = await share.wechatOAuth("snsapi_userinfo", state, () => {});
    if (res instanceof SendAuthResp) {
      const userInfo = await share.getUserInfo(APP_SECRET_WX, res);
      // 处理登录结果...
    }
  }

  // 支付宝授权登录
  onAlipayLoginClick(): void {
    AFServiceCenter.call(AFService.AFServiceAuth, params);
  }
}

View 层不包含任何业务逻辑,只负责绑定 ViewModel 的状态和方法。

第四步:View 层 -- 声明式 UI 渲染

@ComponentV2
export struct LoginPage {
  @Local private vm: LoginViewModel = new LoginViewModel();

  build(): void {
    AppNavDestination({ viewModel: this.vm }) {
      // Logo
      LogoIcon()

      // 匿名手机号展示
      Text(this.vm.anonymousPhone)

      // 华为账号一键登录按钮
      LoginWithHuaweiIDButton({
        params: { style: Style.BUTTON_CUSTOM, loginType: LoginType.QUICK_LOGIN },
        controller: this.vm.huaweiLoginController,
      })

      // 短信验证码登录
      IBestButton({
        text: $r("app.string.sms_login"),
        onBtnClick: () => {
          getContainer().tryResolve<IAuthNavSvc>(AUTH_NAV_SVC_KEY)?.toSmsLogin();
        }
      })

      // 第三方登录(微信 / 支付宝 / QQ)
      this.buildThirdPartyLogin()

      // 用户协议
      UserAgreement()
    }
  }
}

注意跳转短信登录页时,通过 DI 容器解析 IAuthNavSvc 服务来导航,而不是直接引用目标页面 -- 这就是契约解耦的威力。

第五步:可复用组件沉淀

login 功能包还沉淀了一组可复用的认证 UI 组件:

// AnimatedAuthPage -- 认证页面通用布局,接收自定义内容
@ComponentV2
export struct AnimatedAuthPage {
  @Param title: ResourceStr = "";
  @BuilderParam content: CustomBuilder;

  build(): void {
    LargePaddingVerticalScroll({ fillMaxSize: true }) {
      Text(this.title).fontSize(28).fontWeight(FontWeight.Medium)
      SpaceVerticalXXLarge()
      if (this.content) { this.content(); }
    }
  }
}

// 短信登录页直接复用这个布局
AnimatedAuthPage({ title: $r("app.string.welcome_login") }) {
  PhoneInputField({ ... })
  VerificationCodeField({ ... })
  UserAgreement()
  IBestButton({ text: $r("app.string.login"), ... })
}

PhoneInputFieldVerificationCodeFieldUserAgreement 这些组件,在你开发注册页、找回密码页时可以直接复用。

小结:一个功能包的完整生命周期

Shared 层定义契约(接口 + 路由 + 类型)
        |
LoginModule 注册服务、路由、守卫
        |
ViewModel 封装业务逻辑(华为/微信/支付宝/短信)
        |
View 层声明式渲染(绑定 ViewModel,零业务代码)
        |
可复用组件沉淀(AnimatedAuthPage / PhoneInputField / ...)

这就是 HCompass 功能包的开发范式。每个功能包都遵循同样的模式,新成员看一个包就能上手所有包。


技术栈

技术用途
HarmonyOS NEXT开发平台
ArkTS / ArkUI开发语言与 UI 框架
@ohos/axiosHTTP 请求
@ibestservices/ibest-ui-v2UI 组件库
@ibestservices/ibest-orm数据库 ORM

谁适合使用 HCompass?

  • 独立开发者:快速搭建应用骨架,把精力放在业务创新上
  • 创业团队:统一技术栈和代码规范,降低协作成本
  • 企业开发团队:沉淀业务功能包,跨项目复用,提升交付效率
  • 鸿蒙生态贡献者:以功能包为单位贡献开源模块,共建生态

完善的文档体系

HCompass 提供了基于 VitePress 构建的完整文档站点,涵盖:

  • 快速开始 -- 从零搭建你的第一个 HCompass 应用
  • 架构设计 -- 深入理解四层架构和 MVVM 模式
  • 核心模块 -- DI、导航、网络、组件等每个模块的详细 API 文档
  • 功能包开发 -- 手把手教你创建和发布功能包
  • 最佳实践 -- 代码规范、性能优化、安全指南、状态管理

文档地址:hcompass.codelably.com


快速开始

# 克隆项目
git clone https://github.com/codelably/HCompass.git

# 使用 DevEco Studio 打开项目
# 等待依赖同步完成
# 运行 entry 模块即可体验

环境要求:

  • DevEco Studio 5.0+
  • HarmonyOS SDK 最新稳定版
  • Node.js 18.0+

开源协议

HCompass 基于 MIT 协议开源,你可以自由使用、修改和分发。

欢迎通过 Issue 和 Pull Request 参与项目建设,一起让鸿蒙开发更简单。