这一篇先不急着写复杂页面,而是把项目从“能打开工程”到“能理解启动链路”跑通。重点看三个问题:工程模块怎么分、Stage 模型从哪里加载首页、发布配置里哪些内容可以讲、哪些内容必须脱敏。

项目落点:

  • 入口 Ability:entry/src/main/ets/entryability/EntryAbility.ets
  • 主页面:entry/src/main/ets/pages/Index.ets
  • 模块配置:entry/src/main/module.json5
  • 构建配置:build-profile.json5entry/build-profile.json5

开篇:为什么选择“离线旅行攻略”这个题目

很多 HarmonyOS Demo 会从待办、计数器、天气页开始,优点是简单,缺点是离真实 App 有距离。《疆域纪行》选择的是一个更接近上架产品的场景:新疆旅行攻略。

这个场景天然包含几个很适合练手的技术点:

  • 内容多:景点、美食、城市、路线、贴士都要结构化管理。
  • 页面多:首页、探索页、路线页、收藏页、我的页、详情页都要完整串起来。
  • 强视觉:旅行 App 需要大图、卡片、标签、沉浸式详情页。
  • 离线可用:不能依赖后端,所有核心内容要打包进应用。
  • 本地状态:收藏、搜索历史、行程清单要在重启后保留。

最终这个项目不是“能跑就行”的页面拼接,而是一套完整的 local-first App 骨架。它特别适合作为 HarmonyOS 初中级开发者的实战项目。

工程结构先看清楚

项目核心文件集中在 entry 模块:

entry/src/main/ets/
├── data/
│   └── AtlasData.ets
├── entryability/
│   └── EntryAbility.ets
├── models/
│   └── AtlasModels.ets
├── pages/
│   └── Index.ets
└── services/
    └── LocalAppStore.ets

这套结构的好处是边界非常清晰:

  • models 放类型定义,先把业务对象说清楚。
  • data 放离线内容库,运行时不需要接口。
  • services 放本地存储读写,页面不直接碰持久化细节。
  • pages 负责页面状态、布局、交互和页面切换。
  • entryability 负责启动 Ability 并加载主页面。

这是做 HarmonyOS 单机应用时很推荐的一种轻量分层。项目还没有复杂到需要一堆模块和路由框架,但也没有把所有逻辑散落在 UI 里。

Stage 模型入口:EntryAbility 做什么

入口 Ability 的核心代码如下:

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
        return;
      }
      hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
    });
  }
}

这里没有做复杂路由,而是直接加载 pages/Index。原因很实际:这个 App 的页面层级主要靠状态驱动,底部 Tab、详情页、搜索页都可以在一个主页面里完成。

这种做法适合 MVP:

  • 启动路径短。
  • 状态集中,调试成本低。
  • 页面跳转不用维护很多路由常量。
  • 单机内容类 App 的交互闭环更快。

后续如果要拆成多页面,可以再把详情页、搜索页、路线编辑页独立出去。MVP 阶段先做“可运行、可浏览、可收藏”,是更划算的选择。

module.json5:设备与入口配置

entry/src/main/module.json5 中声明了设备类型:

"deviceTypes": [
  "phone",
  "tablet",
  "2in1"
]

这说明项目一开始就不是只面向手机竖屏,而是预留了平板和 2in1 设备。旅行攻略类 App 在大屏上的体验很有价值:图片更大、路线信息更清楚、详情页阅读更舒服。

Ability 配置中还设置了启动图标和启动背景:

"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background"

启动体验不是锦上添花。对于一个图片很多的 App,启动时如果有统一的背景色和图标,可以避免冷启动阶段出现突兀白屏。

页面架构:一个 Index 承担主流程

Index.ets 是整个 App 的核心。它的状态定义很直观:

@State activeTab: string = 'home';
@State detailId: string = '';
@State detailKind: string = '';
@State searchText: string = '';
@State exploreType: string = 'spot';
@State exploreRegion: string = 'all';
@State exploreTheme: string = 'all';
@State exploreSort: string = 'recommend';
@State favoriteType: string = 'all';
@State favoriteIds: string[] = [];
@State searchHistory: string[] = [];
@State itineraryIds: string[] = [];
@State showLaunchSplash: boolean = true;

这组状态可以分成四类:

  • 页面导航:activeTabdetailIddetailKind
  • 搜索筛选:searchTextexploreTypeexploreRegionexploreThemeexploreSort
  • 本地数据:favoriteIdssearchHistoryitineraryIds
  • 启动体验:showLaunchSplash

这就是一个单机 App 最小但完整的状态模型。页面显示什么,不靠隐式路由猜,而是由状态直接决定。

build 方法:状态驱动页面壳

主页面的 build() 非常关键:

build() {
  Stack({ alignContent: Alignment.TopStart }) {
    if (this.showLaunchSplash) {
      this.LaunchSplash()
    } else {
      Column() {
        if (this.detailId.length > 0 && this.detailKind === 'route') {
          this.RouteDetail(this.findRouteItem(this.detailId));
        } else if (this.detailId.length > 0) {
          this.AtlasDetail(this.findAtlasItem(this.detailId));
        } else {
          this.MainShell();
        }
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#F6F1E8')
    }
  }
  .width('100%')
  .height('100%')
}

这段代码的亮点是把主流程拆成三层:

  1. 启动页:showLaunchSplash
  2. 详情页:detailIddetailKind
  3. 主 Shell:底部导航和各 Tab 页面

这种结构很适合内容型 App。详情页不一定要真的走系统路由,只要让 detailId 成为页面状态,就能做到“从任何卡片进入详情,从返回键回到原页面”。

返回键处理:让详情页像自然导航

项目里还实现了 onBackPress()

onBackPress(): boolean {
  if (this.detailId.length > 0) {
    this.closeDetail();
    return true;
  }
  return false;
}

当用户处在详情页时,系统返回键会清空详情状态,而不是直接退出 App。这个细节很小,但很影响真实使用体验。

对内容类 App 来说,用户经常从首页、探索页、收藏页反复进入详情。如果返回逻辑不自然,会显得非常粗糙。

为什么 MVP 阶段不急着上后端

《疆域纪行》的第一版定位是完全离线:

  • 景点、美食、路线数据内置在 AtlasData.ets
  • 图片放在 resources/base/media
  • 收藏、历史、行程存入本机 Preferences
  • 无需登录
  • 无需网络

这不是“偷懒”,反而是一种产品策略。新疆旅行场景里,弱网和无网并不少见。攻略信息如果能离线查看,核心价值会更稳。

当然,离线 App 也有边界:门票、开放时间、交通政策、餐厅营业状态可能变化。因此项目在“我的”页面和详情页都加入了免责声明,把动态信息弱化为“旅行参考”。

本篇小结

这篇先完成了整体工程认知:

  • Stage 模型通过 EntryAbility 加载 pages/Index
  • 页面主流程由 @State 驱动
  • 首页、探索、路线、收藏、我的都放在统一 Shell 下
  • 详情页通过 detailId/detailKind 切换
  • 离线内容库和本地状态形成第一版核心闭环

下一篇我们继续看这个 App 最核心的底座:如何用 ArkTS 类型和静态数据,搭建一个可扩展的离线内容库。

Logo

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

更多推荐