Harmony os 技术实战|拼豆制图48:把个人页字符串路由改成可穷尽的类型协议

个人页通常从几个按钮起步:我的作品、收藏、设置、关于。功能少时,用 string 传递目标看起来最省事;但当字符串同时控制底部页签和个人页面板,任何拼写差异都可能把页面带到一个不存在的状态。

本文以 profilePanelhandleProfileTarget()openProfilePanel() 为例,把松散字符串改造成类型化动作协议,让每次点击只有一条明确路径,并让新增入口时的遗漏在编译阶段暴露出来。

一、字符串路由的问题不只是容易拼错

下面这类签名允许调用方传入任意内容:

ts 复制代码
private handleProfileTarget(target: string): void
private openProfilePanel(panel: string): void

若传入 work 而不是 works,代码仍能编译。更隐蔽的问题是,target 有时表示底部页签,有时表示个人页面板,两个不同概念被塞进了同一字符串通道。最终表现可能是:

  • 未知值被赋给 activeTab,导航没有对应内容;
  • 没有作品时跳到创建页,有作品时却打开面板,规则藏在点击函数里;
  • 面板关闭使用空字符串,空值既表示"关闭"又可能表示"未初始化";
  • 新增菜单后忘记同步某个 if 分支。

类型化改造的第一步,是承认"页签""面板""菜单动作"是三个领域对象。

二、分别定义页签和面板

使用枚举能让状态值集中、可搜索,也不需要用空字符串表示关闭:

ts 复制代码
export enum MainTab {
  Gallery = 'gallery',
  Create = 'create',
  Profile = 'profile'
}

export enum ProfilePanel {
  None = 'none',
  Account = 'account',
  Works = 'works',
  Settings = 'settings',
  About = 'about'
}

@State activeTab: MainTab = MainTab.Gallery
@State profilePanel: ProfilePanel = ProfilePanel.None

None 是一个明确状态,不再依赖 '' 的隐含约定。页面渲染时也可以直接比较枚举成员。

三、菜单传递动作而不是目标字符串

"我的作品"不是简单的导航目标:有记录时打开作品面板,没有记录时进入创建页。它更接近一条命令。

ts 复制代码
export enum ProfileAction {
  OpenAccount = 'open-account',
  OpenWorks = 'open-works',
  OpenFavorites = 'open-favorites',
  OpenSettings = 'open-settings',
  OpenAbout = 'open-about'
}

export interface ProfileMenuItem {
  title: string
  subtitle: string
  action: ProfileAction
}

这样菜单配置只声明用户意图,不必知道最终是切页还是开面板。

四、集中路由并穷尽每个动作

将动作解释集中到一个方法,所有前置条件都能在同一处阅读:

ts 复制代码
private routeProfileAction(action: ProfileAction): void {
  switch (action) {
    case ProfileAction.OpenAccount:
      this.toggleProfilePanel(ProfilePanel.Account)
      return
    case ProfileAction.OpenWorks:
      this.openWorksOrCreate()
      return
    case ProfileAction.OpenFavorites:
      this.profilePanel = ProfilePanel.None
      this.activeTab = MainTab.Gallery
      this.galleryMode = 'favorites'
      return
    case ProfileAction.OpenSettings:
      this.toggleProfilePanel(ProfilePanel.Settings)
      return
    case ProfileAction.OpenAbout:
      this.toggleProfilePanel(ProfilePanel.About)
      return
  }
}

每个 case 都以 return 结束,避免误落入后续分支。新增 ProfileAction 时,路由方法就是唯一需要补齐的入口。

五、把条件跳转提炼成业务方法

作品入口的规则值得单独命名,避免在多处复制"记录数量大于零"的判断:

ts 复制代码
private openWorksOrCreate(): void {
  if (this.generatedRecords.length === 0) {
    this.profilePanel = ProfilePanel.None
    this.activeTab = MainTab.Create
    this.createStatus = '先生成一张作品吧'
    return
  }

  this.activeTab = MainTab.Profile
  this.toggleProfilePanel(ProfilePanel.Works)
}

方法名直接表达产品规则。未来即使"无作品"改成展示空面板,也只需要修改这一处。

六、面板切换只接受 ProfilePanel

切换函数不再接受任意字符串,并显式拒绝打开 None

ts 复制代码
private toggleProfilePanel(panel: ProfilePanel): void {
  if (panel === ProfilePanel.None) {
    this.profilePanel = ProfilePanel.None
    return
  }

  this.profilePanel = this.profilePanel === panel
    ? ProfilePanel.None
    : panel
}

调用方不能误把 MainTab.Create 传进来。这个小约束切断了页签值和面板值混用的可能。

七、渲染层也使用穷尽分支

多个独立 if 容易在状态调整后同时渲染或漏掉回退。可以让面板内容只由一个状态分派:

ts 复制代码
@Builder
private ProfilePanelContent() {
  if (this.profilePanel === ProfilePanel.Account) {
    this.AccountPanel()
  } else if (this.profilePanel === ProfilePanel.Works) {
    this.WorksPanel()
  } else if (this.profilePanel === ProfilePanel.Settings) {
    this.SettingsPanel()
  } else if (this.profilePanel === ProfilePanel.About) {
    this.AboutPanel()
  } else {
    this.ProfileOverview()
  }
}

最后的 else 对应 None,页面始终有明确内容,不会出现空白区域。

八、导航状态要一次提交

一次路由往往会同时改变 activeTabprofilePanel 和筛选模式。若更新顺序分散,界面可能短暂进入非法组合。可以用一个导航快照统一提交:

ts 复制代码
export interface NavigationState {
  tab: MainTab
  panel: ProfilePanel
  galleryMode: 'all' | 'favorites'
}

private applyNavigation(next: NavigationState): void {
  this.activeTab = next.tab
  this.profilePanel = next.panel
  this.galleryMode = next.galleryMode
}

例如进入收藏时一次提交 { tab: Gallery, panel: None, galleryMode: favorites },不会保留上一个个人页面板。

九、为非法组合建立不变量

即使字段都有类型,不同字段之间仍可能形成不合理组合。可以把关系写进断言:

ts 复制代码
export function validateNavigation(state: NavigationState): string[] {
  const issues: string[] = []
  if (state.tab !== MainTab.Profile && state.panel !== ProfilePanel.None) {
    issues.push('非个人页不能保持个人页面板')
  }
  if (state.tab !== MainTab.Gallery && state.galleryMode === 'favorites') {
    issues.push('收藏筛选只能属于图库页')
  }
  return issues
}

调试构建或单元测试中运行这些断言,可以更早发现"值合法但组合非法"的状态。

十、测试关注结果状态而不是点击实现

路由测试不需要渲染完整页面,只要给定动作和前置数据,验证最终状态:

前置条件 动作 期望页签 期望面板
无生成记录 我的作品 Create None
有生成记录 我的作品 Profile Works
任意 设置 Profile Settings
任意 再点设置 Profile None
任意 收藏 Gallery None

另外要验证每次离开个人页后,profilePanel 必须回到 None,避免返回时恢复了用户并未重新选择的面板。

十一、落地检查清单

  • 页签、面板和菜单动作使用不同类型;
  • 空面板由 ProfilePanel.None 表示;
  • 菜单项只携带 ProfileAction
  • 条件跳转规则集中在具名方法;
  • 离开个人页时关闭个人页面板;
  • 导航快照不会产生非法组合;
  • 新增动作时同步补充路由与测试。

改造完成后应从每个底部页签进入个人页,再依次点击所有菜单并返回。除了验证目标内容,还要确认旧面板和旧筛选已经清理;否则类型虽然合法,跨页残留仍可能形成不符合产品规则的组合状态。

总结

字符串路由最大的代价不是代码长,而是状态边界不可见。把个人页交互拆成 MainTabProfilePanelProfileAction 后,点击意图、业务规则和最终界面状态各司其职。Harmony os 页面即使继续增长,也能沿着一条可读、可验证的协议演进。

标签:Harmony os、ArkTS、ArkUI、类型建模、页面路由

相关推荐
咏方舟【长江支流】1 小时前
【前端2】单据编辑 -订单单据主子表,万能模板行,自定义添加行
前端·前端框架·状态模式·咏方舟-长江支流
qq_452396231 小时前
第六篇:《CI/CD 流水线:从前端构建到自动化部署》
前端·ci/cd·自动化
小雨青年1 小时前
【HarmonyOS 7 悬浮页签深度实战】03 barFloatingStyle 的宽度、底部间距与遮罩如何配置
华为·harmonyos
咏方舟【长江支流】1 小时前
【前端1】单据编辑 -EasyUI/Vue/React/Bootstrap 主流框架实现订单单据主子表显示和编辑比较,哪个你最易入门?
前端·vue.js·easyui·咏方舟-长江支流·userbaodatagrid
超爱西西鸭1 小时前
鸿蒙ArkTS文件管理:fileIo 文件读写与目录操作
学习·华为·harmonyos·鸿蒙
m0_579146651 小时前
Element UI 表格合并单元格完全指南:从原理到实战
前端·vue.js·elementui
sibylyue1 小时前
# Web端流媒体JS播放器开源库
前端·javascript·开源
超爱西西鸭1 小时前
ArkTS传感器开发:加速度计与数据监听
学习·华为·harmonyos·鸿蒙
码视野1 小时前
基于 Vue3 + Element Plus 的【基于物联网与深度学习的智慧工业园区能耗监测与光储充一体化微电网协同调度系统】设计与实现(附完整源码与PRD)
前端·人工智能·深度学习·物联网·vue3