一、拆分落地后的职责边界

本轮拆分把人物和地图从巨型页面中迁入独立 Feature:人物模块拥有人物选择、详情页签、原文/译文模式和关联人物投影;地图模块拥有年份、阵营、缩放、偏移、全屏状态及其交互视图。二者都通过统一的模块合同暴露状态快照、命令分发、订阅、事件和生命周期。

听读采用更保守的边界:播放状态、命令、事件、运行时接口和文本分段模型已形成独立契约;TTS、AVSession 与后台任务仍由 MainFrame 统一协调,避免迁移过程中出现两个媒体资源所有者。当前源码导入关系和成功构建共同确认了这条实际模块边界。

MainFrame 阶段拆分实现结构

已拆分职责当前所有者MainFrame 保留
人物选择、页签、内容模式、关联人物与人物视图PeopleFeature收藏和听读事件协调
年份、阵营、缩放、偏移、全屏地图与地图视图MapFeature收藏事件和跨模块路由
播放状态、命令、事件、运行时与文本分段合同AudioFeatureTTS、媒体会话和后台任务协调
模块激活、停用、订阅和页面组合MainFrame主题、导航与顶层生命周期

二、模块边界按状态所有权划分

四个模块共用同一份泛型合同。snapshot 给出只读状态快照,dispatch 是唯一命令入口,subscribe 用于状态变化,onEvent 把收藏、朗读等业务意图交给外层协调器。激活和停用是显式生命周期,页面销毁时能够同步解除订阅。

export interface FeatureModule<State, Command, Event> {
  snapshot(): State;
  dispatch(command: Command): Promise<void>;
  subscribe(listener: (state: State) => void): () => void;
  onEvent(listener: (event: Event) => void): () => void;
  activate(): Promise<void>;
  deactivate(): Promise<void>;
}

export interface FeatureRoute {
  feature: 'audio' | 'map' | 'people';
  targetId?: string;
  source: 'home' | 'search' | 'related' | 'favorite';
}

MainFrame 在出现时分别订阅人物事件和地图事件,然后调用两个 Feature 的 activate;离开时先 deactivate,再执行取消订阅函数。模块内部事件只有在激活状态下才会发出,避免已离开页面的旧回调继续修改收藏或播放状态。

三、听读先完成稳定合同与资源单一所有权

听读链路同时连接 TTS、AVSession、后台任务、进度计时和应用生命周期,直接搬迁平台对象容易造成重复激活或后台播放中断。因此本轮先把可跨页面复用的合同和文本分段模型放入 AudioFeature,平台资源继续保持单一所有者。

export interface AudioState {
  selectedId?: string;
  phase: 'idle' | 'preparing' | 'playing' | 'paused' | 'failed';
  progressSeconds: number;
  durationSeconds: number;
  loopMode: 'list' | 'single' | 'once';
  backgroundActive: boolean;
  errorCode?: string;
}

export interface AudioCommand {
  type: 'select' | 'play' | 'pause' | 'seek' | 'next' | 'lifecycle';
  audioId?: string;
  seconds?: number;
  lifecycleState?: 'foreground' | 'background';
}

export interface AudioRuntime {
  prepare(text: string): Promise<number>;
  play(fromSeconds: number): Promise<void>;
  pause(): Promise<void>;
  release(): Promise<void>;
}
当前层次已落地内容所有权约束
领域合同AudioState、AudioCommand、AudioEvent不依赖页面 Builder
运行时合同prepare、play、pause、release只描述能力,不重复持有平台对象
文本模型AudioTextSegment 的文本与起止秒数分段和进度映射保持一致
平台协调TTS、AVSession、后台任务当前仍由 MainFrame 单一持有

四、地图模块把视口变换与业务选择分开

地图模块已经把业务选择和视口状态放在同一个可观察 Feature 中,但仍保持两个明确子结构:selection 保存年份与阵营,viewport 保存缩放、偏移和全屏状态。页面通过命令改变状态,无法绕过校验直接写入无效阵营。

async dispatch(command: MapCommand): Promise<void> {
  if (command.type === 'select_year') {
    const year: string = command.value ?? '';
    if (this.years().indexOf(year) < 0) return;
    this.selectedYear = year;
    if (!this.factionVisibleInYear(this.selectedFactionId)) {
      this.selectedFactionId = this.firstVisibleFactionId();
    }
    this.resetViewport();
  } else if (command.type === 'select_faction') {
    const factionId: string = command.value ?? '';
    if (!this.factions.some((item: Faction) => item.id === factionId)) return;
    this.selectedFactionId = factionId;
  }
  this.notifyStateChanged();
}

年份变化后,当前阵营若不在新年份可见集合中,模块自动选择首个可见阵营并复位视口。打开、关闭全屏和显式重置也统一经过 dispatch。地图面板和全屏浮层都绑定同一个 Feature,因此不会出现两个视图各自维护一份缩放状态。

五、人物模块通过 id 建立跨域链接

人物模块已经同时拥有状态模型和独立 PeopleFeatureView。选择新人物时,模块会把详情页签复位为“生平”、文本模式复位为“译文”,避免上一个人物的局部状态泄漏到新人物;收藏和朗读操作则通过事件交给 MainFrame。

async dispatch(command: PeopleCommand): Promise<void> {
  if (command.type === 'select_person') {
    if (!this.hasPerson(command.value)) return;
    this.selectedPersonId = command.value;
    this.selectedTab = '生平';
    this.textMode = '译文';
  } else if (command.type === 'select_tab') {
    this.selectedTab = command.value;
  } else {
    this.textMode = command.value;
  }
  this.notifyStateChanged();
}

无效人物 id 会被直接拒绝,关联人物优先按数据中的 relatedIds 投影,没有关联数据时才返回其他人物的有限集合。人物模块不持有 TTS 实例;play_person_audio 事件由 MainFrame 转换为现有播放入口,从而维持媒体资源的单一所有权。

六、MainFrame 转为模块装配与跨域协调

主页面创建 PeopleFeature 和 MapFeature,负责把 Feature 事件连接到收藏、朗读和导航能力。人物、地图的内部选择与渲染不再由 MainFrame 重复实现;听读平台运行时仍留在主页面,以保证后台任务、媒体会话和生命周期只有一条协调链。

实际交互事件来源MainFrame 协调结果
人物页点击朗读PeopleEvent现有听读运行时选择并播放对应内容
人物页切换收藏PeopleEvent写入或移除人物收藏
地图切换收藏MapEvent写入或移除阵营地图收藏
收藏中打开人物/地图FeatureRoute校验 id、分发命令并切换主标签
页面出现/离开生命周期激活或停用模块并注册/解除事件订阅

地图入口现在直接组合 MapFeaturePanel,全屏层使用 MapFeatureOverlay;人物入口组合 PeopleFeatureView。手机底部导航和平板侧栏共享同一 Feature 状态,只改变外层布局,不复制人物和地图业务逻辑。

七、实现中的失败边界与防护

模块拆分没有把输入校验留给 UI。人物选择先确认 id 存在;地图先确认年份或阵营合法;年份切换后阵营不可见会回落到首个有效项;未激活模块不会发出业务事件。页面离开时取消订阅,防止重复进入后累积监听器。

听读采用不同的防护:本轮只抽离合同和文本分段,不创建第二套 TTS 或 AVSession 实例。后台任务、播放进度和平台回调仍从原有链路进入,避免结构拆分引发双播放器、重复媒体会话或后台任务争用。

八、源码与构建验证

源码差异显示,MainFrame 删除 1104 行旧实现并加入 210 行模块装配与协调代码,净减少 894 行。新增模块中,人物 Feature 约 510 行,地图 Feature 约 714 行,共享合同 23 行,听读合同与文本分段模型 52 行。行数不是架构质量本身,但能直观看到人物和地图逻辑已经实际迁出主页面。

工程完成完整 HAP 构建,entry、library1 和 library2 的资源处理、ArkTS 编译、HAP 打包与签名阶段均通过。验证重点包括:Feature 泛型合同可被 ArkTS 编译;@Observed 模块可通过 @ObjectLink 注入独立视图;MainFrame 能导入并组合三个领域目录;地图和人物命令、事件及生命周期调用没有类型断裂。

参考:HarmonyOS ArkTS MVVM 开发指导。

九、总结

这次拆分的实际结果是:人物和地图已经形成可观察、可分发命令、可发出事件、可独立渲染的 Feature;听读已经形成稳定合同和文本模型,同时保留平台资源的单一协调链;MainFrame 负责装配、路由、收藏和媒体运行时协调。模块边界由真实调用关系和成功构建共同支撑,不再是待实施规划。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐