Administrator
发布于 2026-09-02 / 1,317 阅读
0
0

鸿蒙应用架构模式演进:从 MVC 到 MVI 的工程实践

系列文章《鸿蒙开发从入门到专家》第 39 篇 · 专家

架构模式听起来"高大上",落地其实就是"代码怎么组织"。一个 5 万行的应用,没有清晰架构,到第三个人接手就开始乱。

这一篇把移动端架构模式——MVC、MVP、MVVM、MVI——在鸿蒙生态里的实践方式讲一遍,配上代码示例和权衡分析。鸿蒙的开发范式(声明式 UI + ViewModel + 响应状态)天然适合 MVVM/MVI,所以我们重点放后面两种。

一、架构在解决什么

不同架构模式解决的具体问题不一样:

  • 代码组织:文件放哪、模块怎么分。

  • 可测性:能不能脱离 UI 测业务。

  • 可维护性:改一处会不会到处动。

  • 可演进化:业务复杂度增长后能不能扛。

没有"最优架构",只有"匹配当前业务的架构"。一个工具类 App 不用 MVI;一个复杂交易系统用 MVC 会崩。

二、MVC:经典到失效

MVC(Model-View-Controller)在 Web 后端是真理,在鸿蒙里属于"用过但已经不能完整适配"。

1. 鸿蒙里的 MVC

  • View:ArkUI 的 @Component。

  • Controller:@Entry 文件里那段 @Builder。

  • Model:业务数据。

// Controller + View 混在一起,反例
@Entry
@Component
struct UserPage {
  @State name: string = '';

  aboutToAppear() {
    http.get('/user/me').then((resp) => {
      this.name = resp.name; // 直接在 View 里改状态
    });
  }

  build() {
    Column() { Text(this.name) }
  }
}

问题在于 Controller 和 View 没真正分离,网络请求和 UI 状态混在一起没法测试。

三、MVP:用 Presenter 把 UI 摘出来

MVP(Model-View-Presenter)的核心是把 UI 无关的逻辑放进 Presenter:

interface UserContract {
  interface View {
    setName(name: string): void;
  }
  interface Presenter {
    load(): void;
  }
}

class UserPresenter implements UserContract.Presenter {
  constructor(private view: UserContract.View) {}

  load() {
    http.get('/user/me').then((resp) => {
      this.view.setName(resp.name);
    });
  }
}

@Entry
@Component
struct UserPage implements UserContract.View {
  presenter: UserContract.Presenter = new UserPresenter(this);

  setName(name: string): void { /* 设置 @State */ }

  aboutToAppear() { this.presenter.load(); }
}

能写测试了,但 Presenter 写起来繁琐、回调链复杂,渐渐被 MVVM 取代。

四、MVVM:鸿蒙推荐的姿势

MVVM(Model-View-ViewModel)是主流选择。鸿蒙官方示例代码也是 MVVM 风格的:View 绑定 ViewModel 的状态,ViewModel 负责业务。

1. 一个标准 ViewModel

@ObservedV2
export class ProductListVM {
  @Trace products: Product[] = [];
  @Trace isLoading: boolean = false;
  @Trace errorMsg: string = '';

  async load() {
    this.isLoading = true;
    this.errorMsg = '';
    try {
      const resp = await api.get('/products');
      this.products = resp.data;
    } catch (err) {
      this.errorMsg = '加载失败,请重试';
    } finally {
      this.isLoading = false;
    }
  }
}

@ObservedV2 + @Trace 是鸿蒙提供的响应式机制。当 products 变化时,所有依赖它的 UI 自动刷新。

2. View 端

@Entry
@Component
struct ProductListPage {
  vm: ProductListVM = new ProductListVM();

  aboutToAppear() { this.vm.load(); }

  build() {
    Column() {
      if (this.vm.isLoading) LoadingPage()
      else if (this.vm.errorMsg) ErrorView(this.vm.errorMsg)
      else ListView({ products: this.vm.products })
    }
  }
}

UI 只读 ViewModel 状态,不直接发起请求,逻辑清晰。

3. ViewModel 复用

MVVM 的核心价值是 ViewModel 可测、可复用:

// 测试用例
const vm = new ProductListVM(mockApi);
await vm.load();
assert(vm.products.length === 10);

不需要起 UI、不需要模拟器,单元测试秒级跑完。

五、MVI:单向数据流的极致

MVI(Model-View-Intent)在 MVVM 基础上再加两个约束:

  1. 状态是唯一的真相:整个 View 只有一个 State 不可变对象。

  2. 所有修改通过 Intent:组件发意图,ViewModel 处理后产出新 State。

1. 一个标准 MVI

// State:整个页面的所有信息打包
@ObservedV2
export class CounterState {
  @Trace count: number = 0;
  @Trace history: string[] = [];
}

// Intent:所有用户可能的操作
export type CounterIntent =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'reset' };

// ViewModel:处理 Intent,生成新 State
export class CounterVM {
  @Trace state: CounterState = new CounterState();

  dispatch(intent: CounterIntent) {
    switch (intent.type) {
      case 'increment':
        this.state.count += 1;
        this.state.history.push(`+1 -> ${this.state.count}`);
        break;
      case 'decrement':
        this.state.count -= 1;
        this.state.history.push(`-1 -> ${this.state.count}`);
        break;
      case 'reset':
        this.state.count = 0;
        this.state.history = [];
        break;
    }
  }
}

View 端:

@Entry
@Component
struct CounterPage {
  vm: CounterVM = new CounterVM();

  build() {
    Column() {
      Text(`当前: ${this.vm.state.count}`)
      Button('加一').onClick(() => this.vm.dispatch({ type: 'increment' }))
      Button('减一').onClick(() => this.vm.dispatch({ type: 'decrement' }))
      Button('重置').onClick(() => this.vm.dispatch({ type: 'reset' }))
    }
  }
}

2. MVI 的好处

  • 可回放:所有动作都通过 Intent,调试时可以记录整条 Intent 流回放。

  • 可撤销:Intent 都可序列化,撤销栈实现简单。

  • 状态唯一:不会有"两个状态不一致"的诡异 bug。

3. 代价

  • 写起来啰嗦:每个交互都要定义 Intent 类型。

  • 对简单页面过度设计。

所以"是否采用 MVI"取决于页面复杂度:登录页用 MVVM 够了,下单结算页考虑 MVI。

六、Clean Architecture:六边形分层

Clean Architecture 是更宏观的分层思想,把应用分成几个环:

          实体层 Entities(纯业务)
            ↑
      用例层 Use Cases(业务规则)
            ↑
    接口适配层 Interface Adapters
            ↑
   框架 & 驱动层 Frameworks & Drivers

鸿蒙里的落地映射:

Clean 层

鸿蒙内对应

内容

Entities

model/

数据模型,纯类型

Use Cases

usecase/

业务用例

Interface Adapters

vm/、repository/

ViewModel、仓储

Frameworks

pages/、net/、db/

UI、网络、存储

依赖方向:外向内永远单向。pages/ 依赖 vm/,vm/ 依赖 usecase/,usecase/ 只依赖 model/。

// usecase/GetProductList.ts
export class GetProductList {
  constructor(private repo: ProductRepo) {}

  async execute(params: { keyword: string }): Promise<Product[]> {
    return this.repo.search(params.keyword);
  }
}

// vm/ProductListVM.ts
export class ProductListVM {
  constructor(private getProductList: GetProductList) {}
  async load() {
    this.products = await this.getProductList.execute({ keyword: '' });
  }
}

VM 只通过 Use Case 拿数据,不知道是 HTTP 还是本地缓存。

七、状态机:复杂业务逻辑的最后一招

面对"订单状态""支付状态"这种业务流程,可以用有限状态机(FSM):

type OrderState = 'pending' | 'paid' | 'shipping' | 'completed' | 'cancelled';
type OrderEvent = 'pay' | 'ship' | 'complete' | 'cancel';

const transitions: Record<OrderState, Partial<Record<OrderEvent, OrderState>>> = {
  pending: { pay: 'paid', cancel: 'cancelled' },
  paid: { ship: 'shipping', cancel: 'cancelled' },
  shipping: { complete: 'completed' },
  completed: {},
  cancelled: {},
};

export function nextState(s: OrderState, e: OrderEvent): OrderState {
  return transitions[s][e] ?? s;
}

状态机的好处:状态迁移图视觉化,所有"非法状态"在编译期就能拦截,避免出现"已取消但又发货"的逻辑漏洞。

八、案例:电商 App 架构演进

V1 - 早期(MVC 风格)

所有逻辑直接写在 @Component。30 + 页面,代码 4 万行。已经出现改一处崩一片的问题。

V2 - 引入 MVVM

提炼出 ProductVM、CartVM、UserVM。状态集中在 @ObservedV2。单元测试覆盖率从 0 涨到 35%。

V3 - 引入 UseCase 层

发现 VM 里仍然夹杂业务逻辑。抽离 usecase/,VM 只做"调用 + 状态展示"。业务测试覆盖到 60%。

V4 - 复杂页面切 MVI

支付、订单详情等状态机复杂的页面用 MVI,单纯列表保留 MVVM。

pages/          简单页面:直接 MVVM
                复杂页面:MVI
  ↓
vm/             视图状态
  ↓
usecase/        业务用例
  ↓
repository/     数据抽象
  ↓
net/  db/       具体实现

各取所长,是务实做法。

小结

架构选择没有"最好",只有"合适":

  • MVC:过度时已暴露问题,可以放弃。

  • MVP:可测但啰嗦,很少单独选。

  • MVVM:鸿蒙默认推荐,主流选项。

  • MVI:复杂页面(状态机、可回放)适用。

  • Clean:分层明确,但要看团队接受度。

下一篇是这个 100 篇系列的"中段"总结——前 39 篇都讲了啥,后 60 篇要怎么走,帮读者画一张"接下来读什么"的路线图。

参考阅读

  • 鸿蒙开发者文档《状态管理最佳实践》

  • 《Clean Architecture》(Robert C. Martin)

  • 《Domain Modeling Made Functional》(DDD 实务)

  • Eli Bendersky’s blog(架构对比分析)


评论