枚举与常量管理:RouterMap 的设计模式

在这里插入图片描述

一、引言

在大型应用中,路由路径的管理是一件容易被忽视但影响深远的事情。如果每个页面跳转都直接使用字符串字面量,一旦路径需要修改,将面临全局搜索替换的噩梦——改漏一个就会导致页面跳转失败。

我们英语学习 App 包含 40+ 个页面横跨 4 个 feature 模块,路由管理必须做到集中、类型安全、易于维护RouterMap 枚举 + RouterTable 注册表 + RouterModule 封装的三层架构正是为此而设计。本文重点分析第一层——RouterMap 枚举的设计模式。

二、RouterMap:字符串枚举的路由表

2.1 完整定义

// commons/commonLib/src/main/ets/constants/RouterMap.ets
export enum RouterMap {
  // 主页
  MAIN_PAGE = 'MainPage',
  // 登录
  LOGIN_PAGE = 'LoginPage',

  // -----------------------------------------------------练习
  CHAPTER_PRACTICE_PAGE = 'ChapterPractice',
  MATERIAL_DOWNLOAD_PAGE = 'MaterialDownload',
  FEATURE_COURSE_PAGE = 'FeatureCourses',
  SEARCH_PAGE = 'SearchPage',
  SEARCH_INPUT_PAGE = 'SearchQuestionPage',

  // -----------------------------------------------------课程
  ANSWER_QUESTIONS_PAGE = 'AnswerQuestionsPage',
  ANSWER_QUESTIONS_TWO_PAGE = 'AnswerQuestionsTwoPage',
  ANSWER_REPORT_PAGE = 'TestReportPage',
  EXAM_RESULT_PAGE = 'ExamResultPage',
  GOOD_COURSE_DETAIL_PAGE = 'GoodCourseDetailPage',
  MY_WRONG_PAGE = 'MyWrongPage',
  MY_COLLECTION_PAGE = 'MyCollectionPage',
  MY_NOTE_PAGE = 'MyNotePage',
  View_NOTE_PAGE = 'ViewNotePage',
  Mock_PAGE = 'MockTestPage',
  COURSE_INTRODUCTION_PAGE = 'CourseIntroductionPage',
  Level_TWO_PAGE = 'SecondListPage',
  Level_THREE_PAGE = 'ThirdListPage',
  Level_ONE_PAGE = 'TopicHomePage',

  // -----------------------------------------------------我的
  MESSAGE_CENTER_PAGE = 'MessageCenterPage',
  SET_UP_PAGE = 'SetUpPage',
  FEEDBACK_PAGE = 'FeedbackPage',
  FEEDBACK_RECORD_PAGE = 'FeedbackRecordPage',
  PRIVACY_STATEMENT_PAGE = 'PrivacyStatementPage',
  PRIVACY_AGREEMENT_PAGE = 'PrivacyAgreementPage',
  PRIVACY_USE_PAGE = 'PrivacyUseAlertPage',
  PERSONAL_CENTER_PAGE = 'EditPersonalCenter',
  ABOUT_PAGE = 'AboutPage',
  COURSE_PAGE = 'CoursePage',
  PRACTICE_RECORDS_PAGE = 'PracticeRecordsPage',
  PRACTICE_DETAILS_PAGE = 'PracticeDetailsPage',
  DAY_PRACTICE_DETAILS_PAGE = 'OneDayPracticeRecordsPage',
  COLLECTION_PAGE = 'CollectionPage',
  LEARNING_PLAN_PAGE = 'LearningPlanSettingPage',
  REMINDER_SETTING_PAGE = 'ReminderSettingPage',
  MY_ORDER_PAGE = 'MyOrderPage',
  Order_Detail_PAGE = 'OrderDetailPage',
  BROWSING_HISTORY_PAGE = 'BrowsingHistoryPage',
  DASHBOARD_PAGE = 'DashboardPage',
  DAILY_CHALLENGE_PAGE = 'DailyChallengePage',
  WORD_CARD_PAGE = 'WordCardPage',
  SPEECH_EVAL_PAGE = 'SpeechEvalPage',
}

2.2 关键设计决策

为什么用字符串枚举而非数字枚举?

ArkTS 的枚举默认是数字枚举(MAIN_PAGE = 0),但 RouterMap 选择的是字符串枚举。原因有三:

  1. 可读性RouterMap.MAIN_PAGE 的值是 'MainPage',在调试、日志和序列化时一目了然。

  2. 序列化友好:路由路径通常需要在配置文件中使用(如 route_map.json),字符串值天然支持 JSON 序列化。

  3. 与 NavPathStack 兼容:HarmonyOS 的 NavPathStack.pushPathByName() 需要字符串类型的路由名,字符串枚举直接满足要求。

// RouterModule.push 的实现
public static push(info: NavRouterInfo, animated?: boolean) {
  RouterModule.stack.pushPathByName(info.url, info.param);  // url 为字符串
}
为什么用注释分组而非嵌套枚举?

你可能注意到代码中使用注释 // --- 练习 --- 等来分组。为什么不使用嵌套枚举或命名空间?因为 ArkTS 不支持枚举嵌套。注释分组是最务实的替代方案——它虽然没有编译期约束,但配合 IDE 的代码折叠功能,阅读体验尚可。

三、RouterTable:Builder 注册表

有了枚举定义的路由名,还需要将它们与实际的页面 Builder 关联起来。这就是 RouterTable 的作用:

// product/entry/src/main/ets/model/RouterTable.ets
import { RouterMap } from 'commonlib';

export class RouterTable {
  static builderMap: Map<string, WrappedBuilder<[object]>> = new Map();

  public static routerInit() {
    // 主页
    RouterTable.builderMap.set(RouterMap.MAIN_PAGE, wrapBuilder(MainEntryBuilder));
    RouterTable.builderMap.set(RouterMap.LOGIN_PAGE, wrapBuilder(LoginPageBuilder));

    // 练习模块(来自 homepage feature)
    RouterTable.builderMap.set(RouterMap.CHAPTER_PRACTICE_PAGE, wrapBuilder(ChapterPracticeBuilder));
    RouterTable.builderMap.set(RouterMap.SEARCH_PAGE, wrapBuilder(SearchPageBuilder));

    // 课程模块(来自 topicpage feature)
    RouterTable.builderMap.set(RouterMap.ANSWER_QUESTIONS_PAGE, wrapBuilder(AnswerQuestionsBuilder));
    RouterTable.builderMap.set(RouterMap.WORD_CARD_PAGE, wrapBuilder(WordCardPageBuilder));
    RouterTable.builderMap.set(RouterMap.SPEECH_EVAL_PAGE, wrapBuilder(SpeechEvalPageBuilder));

    // 我的模块(来自 minepage feature)
    RouterTable.builderMap.set(RouterMap.SET_UP_PAGE, wrapBuilder(SetUpPageBuilder));
    RouterTable.builderMap.set(RouterMap.LEARNING_PLAN_PAGE, wrapBuilder(LearningPlanSettingPageBuilder));
    RouterTable.builderMap.set(RouterMap.DASHBOARD_PAGE, wrapBuilder(DashboardPageBuilder));
    // ... 40+ 路由注册
  }
}

设计要点

  • builderMapRouterMap 枚举值为 key,以 WrappedBuilder 为 value
  • 所有注册集中在 routerInit() 中统一调用,在应用启动时完成
  • wrapBuilder()@Builder 函数包装为可反射调用的形式

四、RouterModule:统一导航封装

有了路由表和枚举,还需要一个统一的导航工具类来消费它们:

// commons/commonLib/src/main/ets/utils/RouterModule.ets
class RouterModule {
  public static stack: NavPathStack = new NavPathStack();

  // 页面跳转
  public static push(info: NavRouterInfo, animated?: boolean) {
    RouterModule.stack.pushPathByName(info.url, info.param, info.onPop, animated);
  }

  // 页面替换
  public static replace(info: NavRouterInfo) {
    RouterModule.stack.replacePathByName(info.url, info.param);
  }

  // 页面回退
  public static pop<T = boolean>(result?: T, animated?: boolean) {
    RouterModule.stack.pop(result, animated);
  }

  // 页面栈清空
  public static clear(animated?: boolean) {
    RouterModule.stack.clear(animated);
  }
}

业务代码中使用时,RouterMap 枚举值 + RouterModule 的组合确保了类型安全:

// MainPage.ets - 页面跳转示例
RouterModule.push({ url: RouterMap.WORD_CARD_PAGE });                        // 无参数跳转
RouterModule.push({ url: RouterMap.LOGIN_PAGE, param: false });              // 带参数跳转
RouterModule.push({ url: RouterMap.ANSWER_QUESTIONS_PAGE, param: '1' });     // 字符串参数
RouterModule.push({ url: RouterMap.WORD_CARD_PAGE, param: { id: 1 } });      // 对象参数

对比直接使用字符串字面量:

// ❌ 不好的做法:硬编码字符串
RouterModule.push({ url: 'WordCardPage' });           // 拼写错误不会报错
RouterModule.push({ url: 'word-card-page' });          // 大小写不一致

// ✅ 好的做法:使用枚举
RouterModule.push({ url: RouterMap.WORD_CARD_PAGE });  // 拼写错误编译报错

五、route_map.json:与系统路由的桥接

除了代码层面的路由管理,HarmonyOS 还要求每个 feature 模块在 route_map.json 中注册 NavDestination 路由:

{
  "routerMap": [
    {
      "name": "HomePage",
      "pageSourceFile": "src/main/ets/pages/MainPage.ets",
      "buildFunction": "HomePageBuilder",
      "data": {
        "description": "首页"
      }
    },
    {
      "name": "FeaturedCourses",
      "pageSourceFile": "src/main/ets/pages/FeaturedCourses.ets",
      "buildFunction": "FeaturedCoursesBuilder",
      "data": {
        "description": "精选课程"
      }
    }
  ]
}

这里的 name 值必须与 RouterMap 枚举的值一致,否则运行时无法匹配。因此 RouterMap 枚举值实际上起着代码层路由名 ↔ 系统层路由配置的桥梁作用。

六、字符串枚举 vs 数字枚举 vs 常量对象

ArkTS 中定义常量有三种主要方式,各有优劣:

方案 示例 运行时值 序列化 IDE 提示 反向映射
字符串枚举 enum E { A = 'a' } 'a' ✅ 友好 ✅ 完整
数字枚举 enum E { A } 0 ❌ 无意义 ✅ 完整
常量对象 const E = { A: 'a' } 'a' ✅ 友好 ⚠️ 有限

对于路由管理,字符串枚举是最佳选择,因为:

  1. 枚举名即文档——RouterMap.WORD_CARD_PAGE'wordCardPage' 更具语义
  2. 重构友好——修改枚举值时,所有引用处自动更新
  3. 不可变——const enum 在编译期内联,无运行时开销
  4. 统一管理——所有路由路径在一处定义,一目了然

七、最佳实践

  1. 统一枚举命名规范模块_功能_PAGE,如 LEARNING_PLAN_PAGEANSWER_QUESTIONS_PAGE。清晰的命名让开发者看到枚举名就能定位到对应模块。

  2. 注释分组:使用 // --- 模块名 --- 注释对枚举成员进行分组,配合 IDE 代码折叠提升可读性。

  3. 枚举值尽量简短:枚举的值用于路由匹配,宜短不宜长。例如 'WordCardPage' 而非 'WordCardManagementPage'

  4. 与 route_map.json 保持同步:新增路由时,确保 RouterMap 枚举、route_map.jsonRouterTable.routerInit() 三者同步更新。

  5. 参数类型关联:对于带参数的路由,建议在跳转处加上注释说明参数类型:

RouterModule.push({
  url: RouterMap.ANSWER_QUESTIONS_PAGE,
  param: '1' as string,  // ⬅️ param: 章节ID(字符串)
});

八、总结

RouterMap 枚举 + RouterTable 注册表 + RouterModule 封装的三层路由架构,是我们项目中的核心基础设施:

  • RouterMap:用字符串枚举集中定义 40+ 路由路径,杜绝硬编码
  • RouterTable:将枚举值映射到具体的页面 Builder,实现声明式路由注册
  • RouterModule:在 NavPathStack 之上提供统一的 push/pop/replace/clear 接口

这套模式的核心价值在于:一处修改,全局生效。当你需要修改某个路由路径时,只需更新 RouterMap 枚举值,所有引用处自动适配。对于大型多模块项目而言,这种"集中管理"的设计模式是保证代码质量的关键。

Logo

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

更多推荐