承接上篇《应用级状态管理:AppStorage与PersistentStorage实战》,本文解决电商App最核心的多页面交互问题:从商品列表页跳转到详情页、再跳转到购物车页,如何实现页面跳转、参数传递、页面栈管理?为什么点击商品卡片后没有打开新页面?为什么返回时购物车数据没有更新?这些问题都指向Stage模型的核心——UIAbility的生命周期与路由机制。所有结论均经过API23环境验证,附赶稿专用浏览器模拟方案。


一、前言:从"单页面"到"多页面"的必然跨越

上篇写完应用级状态管理后,我尝试给电商Demo加一个"商品详情页":点击列表页的商品卡片,跳转到详情页展示完整参数,再点击"加入购物车"返回列表页。结果一运行就踩了三个坑:

  1. 点击卡片没反应,报Router page not found错误

  2. 详情页能打开,但接收不到商品ID参数

  3. 返回列表页后,购物车数量没更新

查了官方文档才发现,HarmonyOS 6.1的Stage模型和之前的FA模型完全不同:页面跳转不是靠组件自己,而是靠UIAbility承载的路由机制。UIAbility是应用的"功能单元",一个UIAbility可以包含多个页面,而页面路由是UIAbility内部的导航逻辑。

本文将基于电商场景,把UIAbility的生命周期、页面路由、参数传递彻底拆透,帮你避开90%的新手坑。


二、核心概念辨析(官方没讲透的3组区别)

很多新手会把UIAbility、Page、Window搞混,甚至和Android的Activity划等号,我们先把边界理清楚:

2.1 UIAbility vs Page vs Window

维度

UIAbility(应用组件)

Page(页面)

Window(窗口)

定位

应用功能的独立单元,拥有独立的生命周期

UIAbility内的单个交互界面

UIAbility的显示载体,负责绘制UI

生命周期

独立管理:onCreateonWindowStageCreateonForegroundonBackgroundonDestroy

依附于UIAbility:onPageShowonPageHideonPageDestroy

依附于UIAbility:onWindowStageCreateonWindowStageDestroy

适用场景

电商模块、支付模块、用户中心等独立功能

商品列表页、详情页、购物车页等单个界面

悬浮窗、分屏窗口、全屏窗口等显示形态

启动方式

通过context.startAbility()启动

通过router.pushUrl()在UIAbility内部跳转

由系统自动创建,无需手动管理

2.2 Stage模型路由 vs FA模型路由

维度

Stage模型(API 10+,当前主流)

FA模型(已逐步废弃)

路由API

import { router } from '@kit.ArkUI'

import featureAbility from '@ohos.ability.featureAbility'

页面管理

基于页面栈,支持pushUrl/back/replaceUrl

基于Ability栈,跳转逻辑复杂

参数传递

通过params字段传递,大小限制128KB

通过want传递,限制更严格

适用场景

所有新开发的应用

旧应用兼容

💡 核心认知:我们平时写的Index.etsDetailPage.ets都是Page(页面),它们必须依附于一个UIAbility(默认是EntryAbility)才能运行。页面跳转的本质是在同一个UIAbility内切换不同的Page


三、代码实现(API23 电商多页面实战)

我们基于上篇的电商列表页改造,新增商品详情页,实现完整的多页面跳转逻辑。

3.1 第一步:注册页面路径(新手必踩坑!)

所有页面必须在main_pages.json中注册,否则路由会报Page not found错误。

打开entry/src/main/resources/base/profile/main_pages.json



{
  "src": [
    "pages/Index",       // 商品列表页(已有)
    "pages/DetailPage"   // 新增:商品详情页
  ]
}

3.2 第二步:修改商品列表页(Index.ets,添加跳转逻辑)

沿用上篇的Index.ets,新增点击商品卡片跳转详情页的逻辑:



import { router } from '@kit.ArkUI'
import { GoodsItem } from './GoodsItem'
import { GlobalState } from '../common/GlobalState'
import { GoodsBean } from './GoodsItem'

@Entry
@Component
struct Index {
  @StorageLink('cartCounts') counts: number[] = [0, 0, 0]
  @StorageLink('pausedId') pausedId: number = -1
  @State goodsList: GoodsBean[] = [
    { id: 1, title: '【API23新特性】HarmonyOS 6.1 定制款超长商品标题...', price: 5999 },
    { id: 2, title: '华为Mate 60 Pro 先锋计划 全网通智能手机...', price: 6999 },
    { id: 3, title: '鸿蒙智联认证 智能穿戴手表 长续航蓝牙通话...', price: 1299 }
  ]

  // 跳转到商品详情页
  goToDetail(goodsId: number): void {
    router.pushUrl({
      url: 'pages/DetailPage', // 必须和main_pages.json里的路径一致
      params: {
        goodsId: goodsId, // 传递商品ID作为参数
        from: 'Index'     // 传递来源页面,方便埋点/逻辑判断
      }
    }, router.RouterMode.Standard, (err) => {
      if (err) {
        console.error('跳转详情页失败:', err.message)
      }
    })
  }

  get totalPrice(): number {
    return GlobalState.calcTotalPrice(this.goodsList)
  }

  build() {
    Column() {
      Text('电商商品列表页 (UIAbility多页面实战)')
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20, bottom: 20 })

      List() {
        ForEach(this.goodsList, (item: GoodsBean, index: number) => {
          ListItem() {
            // 给商品卡片添加点击事件,跳转到详情页
            GoodsItem({
              goods: item,
              pausedId: $this.pausedId,
              count: $this.counts[index]
            })
              .onClick(() => {
                this.goToDetail(item.id) // 点击卡片触发跳转
              })
          }
        }, (item: GoodsBean) => item.id.toString())
      }
      .width('100%')
      .layoutWeight(1)

      // 底部结算栏(点击跳转到购物车页,后续实现)
      Row() {
        Text(`总价:¥${this.totalPrice}`)
          .fontSize(20)
          .fontColor('#FF0000')
          .fontWeight(FontWeight.Bold)
        Blank()
        Button('去结算')
          .backgroundColor('#0A59F7')
          .fontColor(Color.White)
          .onClick(() => {
            // 后续实现跳转到结算页
            console.log('跳转到结算页')
          })
      }
      .width('100%')
      .padding(20)
      .backgroundColor('#f8f9fa')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f0f0f0')
  }
}

3.3 第三步:新增商品详情页(DetailPage.ets)

创建entry/src/main/ets/pages/DetailPage.ets,实现详情页逻辑:



import { router } from '@kit.ArkUI'
import { GlobalState } from '../common/GlobalState'

@Entry
@Component
struct DetailPage {
  @State goodsId: number = 0 // 商品ID,从列表页传递过来
  @State goodsTitle: string = ''
  @State goodsPrice: number = 0
  @StorageLink('cartCounts') counts: number[] = [0, 0, 0]

  aboutToAppear(): void {
    // 接收从列表页传递过来的参数
    const params = router.getParams()
    if (params) {
      this.goodsId = params['goodsId'] as number || 0
      this.goodsTitle = params['goodsTitle'] as string || ''
      this.goodsPrice = params['goodsPrice'] as number || 0
      console.log('详情页接收到参数:', params)
    }
  }

  // 加入购物车
  addToCart(): void {
    if (this.goodsId > 0) {
      const index = this.goodsId - 1 // 商品ID从1开始,数组索引从0开始
      GlobalState.updateCartCount(index, this.counts[index] + 1)
      // 返回列表页
      router.back()
    }
  }

  build() {
    Column() {
      // 顶部导航栏
      Row() {
        Button('< 返回')
          .backgroundColor('transparent')
          .fontColor('#0A59F7')
          .onClick(() => {
            router.back() // 返回上一页
          })
        Blank()
        Text('商品详情页')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
        Blank()
      }
      .width('100%')
      .padding(15)
      .backgroundColor('#FFFFFF')

      // 商品信息
      Column({ space: 20 }) {
        // 商品图片占位
        Column()
          .width('100%')
          .height(250)
          .backgroundColor('#e8f4ff')
          .borderRadius(12)
          .margin({ top: 20 })

        Text(this.goodsTitle || `商品${this.goodsId}详情`)
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .margin({ top: 20 })

        Text(`¥${this.goodsPrice}`)
          .fontSize(24)
          .fontColor('#FF0000')
          .fontWeight(FontWeight.Bold)

        Text('这是商品详情介绍,包含参数、售后、评价等信息...')
          .fontSize(14)
          .fontColor('#666666')
          .margin({ top: 10 })

        // 加入购物车按钮
        Button('加入购物车')
          .width('90%')
          .height(50)
          .backgroundColor('#0A59F7')
          .fontColor(Color.White)
          .fontSize(18)
          .onClick(() => {
            this.addToCart()
          })
      }
      .padding(20)
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f0f0f0')
  }
}

3.4 第四步:验证UIAbility生命周期(关键调试技巧)

打开entry/src/main/ets/entryability/EntryAbility.ts,添加生命周期日志,验证执行顺序:



import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit'
import { window } from '@kit.ArkUI'
import { PersistentStorage } from '@kit.ArkUI'

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    console.log('EntryAbility onCreate,启动参数:', want.parameters)
    // 初始化PersistentStorage(上篇内容)
    PersistentStorage.persistProp('cartCounts', [0, 0, 0])
    PersistentStorage.persistProp('pausedId', -1)
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    console.log('EntryAbility onWindowStageCreate')
    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        console.error('加载页面失败:', err.message)
        return
      }
      console.log('页面加载成功')
    })
  }

  onNewWant(want: Want): void {
    // 当UIAbility已经启动,再次被调用时触发(比如从通知栏点击进入)
    console.log('EntryAbility onNewWant,新参数:', want.parameters)
  }

  onForeground(): void {
    console.log('EntryAbility onForeground,进入前台')
  }

  onBackground(): void {
    console.log('EntryAbility onBackground,进入后台')
  }

  onDestroy(): void {
    console.log('EntryAbility onDestroy,销毁')
  }
}

生命周期执行顺序验证

  1. 首次启动App:onCreateonWindowStageCreateonForeground

  2. 跳转到详情页:无UIAbility生命周期变化(页面跳转是内部行为)

  3. 按Home键回到桌面:onBackground

  4. 从桌面重新打开App:onForeground

  5. 划掉App后台进程:onDestroy


四、原理剖析:为什么这样写性能最优?

4.1 页面栈的最小化刷新

router.pushUrl会将新页面压入页面栈,返回时弹出栈顶页面。ArkUI只会重建当前显示的页面,不会重建整个UIAbility,符合最小刷新原则。比如从列表页跳转到详情页,列表页的组件不会被销毁,返回时直接复用,性能极高。

4.2 单实例模式的合理使用

如果希望购物车页全局只有一个实例(不管从哪个页面跳转,都不会重复创建),可以在module.json5中配置UIAbility的启动模式:



{
  "module": {
    "abilities": [
      {
        "name": "EntryAbility",
        "launchType": "singleton", // 单实例模式
        "srcEntry": "./ets/entryability/EntryAbility.ets"
      }
    ]
  }
}

配置后,多次启动EntryAbility都会复用同一个实例,触发onNewWant回调,而不是重新创建,适合购物车、用户中心等全局唯一的页面。

4.3 参数传递的最优路径

  • 小参数(<128KB):通过router.pushUrlparams传递,简单直接

  • 大参数/共享数据:通过AppStorage中转,避免参数超限(呼应上篇的应用级状态管理)

  • 跨Ability传递:通过want.parameters传递,适合不同UIAbility之间的数据交互


五、踩坑记录(官方文档没写的5个细节)

这些坑我花了3小时才踩明白,新手可以直接跳过:

5.1 页面路径必须和main_pages.json完全一致

❌ 错误:router.pushUrl({ url: 'pages/detail' })(少了Page后缀)

✅ 正确:router.pushUrl({ url: 'pages/DetailPage' })(和main_pages.json里的src完全一致,大小写敏感)

原因:路由系统是根据main_pages.json查找页面的,路径不匹配会直接报Page not found错误。

5.2 参数大小限制128KB

❌ 错误:传递一个10MB的商品详情对象作为params

✅ 正确:只传递商品ID,详情数据从网络/AppStorage加载

原因params的大小限制为128KB,超过会直接抛异常。大对象必须通过AppStorage或数据库中转。

5.3 onNewWant必须重写

当UIAbility配置为singleton模式时,再次启动不会触发onCreate,而是触发onNewWant。如果需要在启动时接收新参数,必须在onNewWant中处理,否则参数会丢失。

5.4 页面返回时的参数传递

❌ 错误:从详情页返回时,用router.pushUrl跳转到列表页

✅ 正确:用router.back()返回,列表页在onPageShow中读取AppStorage里的更新数据

原因pushUrl会创建新的页面实例,导致页面栈无限增长,最终超过32层的限制抛异常。返回时必须用router.back()

5.5 页面栈深度限制32层

HarmonyOS的页面栈默认最多支持32层,超过会抛Stack overflow异常。因此在开发中要避免无意义的跳转,比如不要在循环中调用pushUrl,返回时一定要用back而不是pushUrl

六、总结与延伸

本文实现了从单页面多页面的跨越,核心收获有三个:

  1. UIAbility是Stage模型的核心,承载一组相关页面,拥有独立的生命周期

  2. 页面路由基于页面栈,pushUrl入栈、back出栈,避免页面栈溢出

  3. 参数传递要根据大小选择合适的路径,大对象必须用AppStorage中转

至此,我们已经覆盖了HarmonyOS 6.1开发的环境搭建→组件基础→状态管理→业务实战→应用级存储→多页面路由全流程。下一篇,我们将挑战Stage模型的高级特性——分布式流转,讲解如何实现跨设备(手机、平板、智慧屏)的页面接续、数据同步,让我们的电商App真正具备"万物互联"的能力。

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐