前言

一个已有列表与详情的应用,已经能够让用户在单页中完成打开、阅读和返回。团队接入平行视界时,如果同时重排页面、替换路由、修改状态保存方式并开启多项窗口配置,大家就很难判断一次返回异常究竟来自哪处修改。因此,迁移开始前,团队需要保留能够比较的原有路径,让每次新增配置都有明确的观察对象。

大家可以先选择一条从资料列表进入详情的路径,让用户依次选中条目、打开内容、完成阅读并返回列表。这条路径能够呈现页面归属、选择状态和返回操作之间的关系,也方便大家检查窗口变化是否影响原来的任务。两种显示方式都能完成这条路径以后,团队再决定接入哪些业务页面,迁移范围就有可复查的依据。

一、先保留一条能够完整返回的业务路径

团队选择第一批接入页面时,需要判断用户能否通过这些页面独立完成一次任务,以及大家能否准确比较修改前后的结果。资料列表与静态详情依赖较少,可以先用于检查导航关系;支付、草稿编辑或复杂外部入口涉及更多业务状态,排查时还需要区分这些状态的影响。这种范围选择以降低排查成本为依据,不代表复杂页面不能接入平行视界。

大家保留旧路径时,除了保存源代码,还需要记录当前入口、目标页面、返回方式,以及状态由谁维护。如果列表保存选中项、详情保存阅读位置,迁移检查就需要分别确认这两类数据是否发生变化。首页截图只能呈现一个初始画面,无法说明用户打开详情以后还能否回到原来的内容位置,因此它不能单独承担原有行为的比较依据。

原有行为的记录还需要让其他大家能够重新执行。记录除了说明用户完成了阅读,还应写清用户选择了哪个条目、从哪个入口返回,以及返回后应该看到什么。如果应用提供多种详情入口,团队可以先固定其中一条,避免把通知入口与列表入口的结果混在一起。其他入口需要单独列入后续范围,团队在第一条路径稳定以后再逐项接入。

当前应用由 Index 显示列表,LegacyDetailPage 显示详情,两个页面共用 pageStackmigrationNavigation 标识负责它们的导航容器。列表使用初始值为 -1selectedIndex 记录点击位置。应用没有在启动时自动执行列表选择,因此关联页配置不能替代实际选择事件,配置存在也不能证明冷启动已经完成高亮与内容同步。

用户点击条目时,openDetail 会先更新选择位置,再清除旧目标并进入详情。这次清除让同级选择成为新的内容起点,因此用户返回时不会依次经过此前打开的条目。openDetail 因此将选择位置更新与路由操作放在同一次点击中。

  private openDetail(index: number): void {
    this.selectedIndex = index
    this.pageStack.clear()
    this.pageStack.pushPathByName('LegacyDetailPage', null)
  }

上面说明选择位置更新与导航操作发生在同一事件中,但函数传给详情的参数仍然是 null。详情显示固定正文,没有根据文章标识读取不同数据,因此当前实现不能证明用户选中某个条目以后,详情就加载了对应文章。大家接入实际列表时,还需要实现业务标识传递和详情读取,并单独检查内容是否匹配。

详情的返回操作也不能自动证明列表状态已经恢复。详情按钮回调调用 clear() 后,目标栈回到空状态;列表中的选择变量仍然有自己的生命周期。当前实现没有保存长列表位置、持久化草稿或在进程重建后恢复数据。原项目已经具备这些功能时,大家需要保留原来的保存责任,再检查分栏是否改变了相关触发时机,避免为了迁移复制另一份状态。

状态副本可能带来另一类故障:列表已经更新选择,详情却仍然读取旧副本。两侧内容各自有效,用户却无法确认它们是否属于同一次选择。因此,大家需要明确点击后由谁更新业务对象、目标页从哪里读取数据,再决定界面变量是否仅用于高亮。当前实现只有选择索引与固定正文,还没有完成这套数据读取过程,项目接入时需要按实际业务补齐。

大家可以把现有行为与仍需实现的内容分别记入下表,明确第一批接入需要覆盖哪些功能。表格中的未实现项说明真实业务的接入责任,并不表示当前页面已经具备这些功能。

检查对象当前实现接入真实业务时需要核对
列表选择点击后更新 selectedIndex高亮是否对应实际加载的业务对象
打开详情清除旧目标后进入 LegacyDetailPage同级替换是否符合返回预期
详情数据固定正文,参数为 null标识传递、读取失败与空内容
返回列表详情按钮清空目标栈系统返回与业务按钮是否一致
阅读与输入没有恢复或持久化实现原项目保存位置和恢复时机是否变化

这份记录明确以后,团队就可以保持页面与路由操作,集中观察新增显示方式改变了哪些结果。如果某项功能在接入前就没有实现,大家不能把功能缺失归因于平行视界。如果原有功能在迁移后失效,保存下来的原始行为则可以帮助大家追查变化发生的位置。

二、每项配置都让失败有明确入口

已有的 Navigation 可以继续管理路径,平行视界配置补充页面关系和窗口显示方式。当前工程在模块中引用 easyGo 配置,并将宽横屏和方形宽屏的显示方式设为 navigationSplit。配置中的主页与关联详情沿用既有页面名称,因此这次接入不需要同时改变目标页面的构建关系。

当前配置将两类窗口的显示方式都设为 navigationSplit,应用按对应窗口条件使用平行视界。比例与导航选项仍然保留在完整配置中,大家比较显示变化时也需要保持相同的页面入口。

      "wideWindowMode": "navigationSplit",
      "squareWindowMode": "navigationSplit",

当前配置启用虚拟容器,将宽横屏比例设为 1:2、方形宽屏比例设为 1:1,导航模式为 1。配置没有设置 drawableRectHookenableInSplitScreen,系统分屏支持因此保持默认关闭。系统分屏下没有启用平行视界这一现象,不能直接作为迁移失败的依据。项目如果需要增加分屏支持,大家还需要单独开启选项,并检查窗口是否满足对应条件。

大家确认模块引用与页面名称以后,可以根据故障出现的阶段缩小原因。构建失败时,检查应从配置校验和类型信息开始;应用已经启动却没有预期分栏时,大家再核对窗口条件、设备节点与导航归属。如果两栏已经出现但内容错误,排查就转向业务参数与状态。前一步的结果能够排除一部分原因,大家也就不必同时修改多个方向。

现有项目如果包含多个 Navigation,导航归属需要具体到负责当前路径的容器。homeNavigationId 应当指向该容器,配置中的主页与详情也需要属于同一导航结构。当前工程只有一个明确标识的容器,没有提供嵌套导航的验证结果。因此,大家接入多套路由栈时,需要从实际入口找到管理目标路径的那一套,再核对配置指向。

设备专用节点同样会影响实际生效的配置。如果项目已经为某类设备设置专用配置,公共节点中的修改可能没有成为该设备最终使用的设置。大家需要先确认运行设备采用哪份配置,再比较新增字段。团队暂时缩小设备范围时,也需要检查专用节点保留的内容,避免修改了一处显示方式,却遗漏另一份实际生效的配置。

排查记录可以同时写明现象、检查位置和结论限制,方便其他大家复现问题。下表帮助大家选择检查入口,但实际原因仍然需要通过当前设备上的操作确认。

当前现象优先检查的位置不能直接作出的判断
配置构建失败easyGo 引用、字段组合、Schema 错误位置不能只归因于 ArkTS 代码
宽窗口仍为普通页面窗口条件、设备覆盖、显示方式、导航 ID不能仅凭设备是平板认定应当分栏
打开了错误内容点击对象、传入参数、目标页读取两栏出现不代表业务数据正确
一次返回退出多层popclear 与已有返回拦截不能只修改系统显示配置
内容拥挤或被截断实际取宽接口、容器选项、局部布局不能先假设比例配置没有生效

例如,应用已经构建成功,用户进入详情后却一直看到固定正文,大家就需要检查详情是否实现了按标识读取内容。比例修改不会替详情补充这项数据读取功能。如果配置尚未通过校验,构建日志已经指出更早的失败位置,大家就应先处理配置问题,而无需推测尚未运行的两栏画面。

小批接入也让同一问题能够在普通配置下进行对照。如果两种配置都出现错误,大家需要检查共有的页面与业务逻辑;如果错误只在平行视界中出现,排查就集中到新增配置及尺寸行为。这个比较需要保持相同版本、数据、设备和窗口,其他变化才不会干扰原因判断。

团队一次只增加少量配置,并不表示每批修改只能包含一个字段。具有依赖关系的字段需要一起组成合法配置,例如显示方式与对应选项不能任意拆开。大家可以分阶段加入能够独立观察的容器或窗口选项,再比较各阶段的结果。修改范围需要以能否解释结果为依据,机械追求最少行数可能产生无效的中间配置,使团队误判平台兼容性。

三、回退配置也必须能够独立构建和使用

问题定位以后,团队需要根据失败位置确定回退范围。只有某类设备或窗口不稳定时,应用可以在该范围保留普通页面;通用导航关系还没有理清时,团队则需要暂时恢复原有显示方式。局部回退与整体回退的选择,取决于两种路径能否继续完成业务,配置项的数量不能单独决定范围。

original 表示普通显示方式,大家回退时还需要处理原有的导航选项。当前配置包含专用于平行视界的 navigationSplitOptions;当大家把宽横屏和方形宽屏都改为 original 时,保留这个对象会导致配置校验失败。因此,大家需要同时移除不再适用的选项,准备完整合法的回退配置,再构建对照包。

大家可以对照下表检查回退需要修改的配置位置,避免只完成显示方式替换,却保留不适用的选项。

配置位置平行视界包普通页面对照包
wideWindowModenavigationSplitoriginal
squareWindowModenavigationSplitoriginal
navigationSplitOptions保留导航模式与比例选项移除整个对象
页面与路由代码原来的列表、详情和共享栈保持同一份实现
本轮结果正式版构建通过修正配置后正式版构建通过

当前两种配置通过独立构建进行对照,没有实现应用内动态开关。大家比较运行结果时,需要分别安装对应产物,并确认当前运行的配置;未签名 HAP 已经生成,也不能证明应用完成了安装。包的配置摘要与版本识别信息需要一同保留,检查人员才能避免把旧包行为误当成新配置结果。

回退包能否使用,还需要通过普通页面中的完整任务判断。用户应该能够找到原入口、打开内容并返回,主要操作也不能依赖只有两栏同时可见时才能访问的位置。如果某个动作仅在左侧提供,而用户进入普通详情页后无法再访问该动作,大家就需要检查页面上的操作安排。详情页成功启动只确认了任务中的一个环节,还不足以证明普通路径能够替代分栏路径。

测试人员需要在两种配置下执行相同的操作链:打开列表、选择条目、进入详情、返回,再改变窗口重复检查。测试人员完成每次操作以后,除了确认页面位置,还需要核对业务对象、选择状态和返回目标。当前详情显示固定正文,数据正确性的检查只能覆盖已有实现。真实项目需要提供可区分的业务内容,大家才能发现参数串用或旧内容残留。

团队是否扩大迁移范围,也需要依据两种配置的对照结果。两种显示方式都能完成核心任务,而且问题可以按设备、窗口或页面定位以后,团队才有依据接入下一批页面。外部入口、深层路由、编辑保存和交易流程还需要各自的检查,静态列表详情的结果不能直接推广到整个应用。

如果用户在普通页面中仍然无法顺利返回,显示配置虽然已经恢复,应用共有的业务问题却仍然存在。大家需要继续修复路由或状态代码,再考虑重新启用分栏。普通路径具备完整的使用功能以后,团队才能在后续遇到设备差异时,将它作为可运行的替代方式。

总结

团队为现有应用接入平行视界时,可以先固定一条列表、详情与返回路径,保留原有路由,再通过小批配置变化观察结果。大家分别检查构建、显示、数据和返回,才能让回退范围与失败位置对应。应用恢复普通显示方式时,同样需要完整合法的配置;宽横屏与方形宽屏都设为 original 的对照中,大家还需要移除 navigationSplitOptions

当前两种配置已经通过正式版构建,完整页面代码保持原样,但静态详情尚未覆盖业务参数、阅读恢复和复杂入口。两种配置都在目标环境中完成同一核心任务以后,团队才有依据扩大迁移范围。

我目前手里的设备还不支持 HarmonyOS 7 ,所以相关内容现阶段主要通过 HarmonyOS 7 模拟器进行验证,真机上的系统表现、设备差异和实际体验,后面有条件再继续补测,最终还是以实际设备运行结果为准。

完整代码

Index.ets

@Entry
@Component
struct Index {
  @Provide('pageStack')
  pageStack: NavPathStack = new NavPathStack()

  @State selectedIndex: number = -1

  @Builder
  pageMap(name: string) {
    if (name === 'LegacyDetailPage') {
      LegacyDetailPage()
    }
  }

  private openDetail(index: number): void {
    this.selectedIndex = index
    this.pageStack.clear()
    this.pageStack.pushPathByName('LegacyDetailPage', null)
  }

  build() {
    Navigation(this.pageStack) {
      Column({ space: 12 }) {
        Text('现有文章列表')
          .fontSize(30)
          .fontWeight(FontWeight.Bold)
          .width('100%')

        Text('页面仍使用原有 Navigation 路由,EasyGo 只接管满足条件的窗口显示。')
          .fontSize(15)
          .fontColor('#5F6678')
          .lineHeight(22)
          .width('100%')

        this.articleItem('API 26 构建记录', 0)
        this.articleItem('平板窗口检查表', 1)
        this.articleItem('折叠屏补测计划', 2)
      }
      .width('100%')
      .height('100%')
      .padding(24)
      .alignItems(HorizontalAlign.Start)
    }
    .id('migrationNavigation')
    .mode(NavigationMode.Stack)
    .title('文章列表')
    .navDestination(this.pageMap)
    .width('100%')
    .height('100%')
  }

  @Builder
  private articleItem(title: string, index: number) {
    Column({ space: 8 }) {
      Text(title)
        .fontSize(18)
        .fontWeight(FontWeight.Medium)
        .width('100%')
      Text(this.selectedIndex === index ? '当前选中' : '点击查看详情')
        .fontSize(14)
        .fontColor(this.selectedIndex === index ? '#5269D8' : '#667085')
        .width('100%')
    }
    .width('100%')
    .padding(18)
    .backgroundColor(this.selectedIndex === index ? '#E9EDFF' : '#F2F4FA')
    .borderRadius(16)
    .alignItems(HorizontalAlign.Start)
    .onClick(() => {
      this.openDetail(index)
    })
  }
}

@Component
struct LegacyDetailPage {
  @Consume('pageStack')
  pageStack: NavPathStack

  build() {
    NavDestination() {
      Scroll() {
        Column({ space: 16 }) {
          Text('现有详情页')
            .fontSize(30)
            .fontWeight(FontWeight.Bold)
            .width('100%')

          Text('这个页面没有手写左右分栏。宽窗口由 EasyGo 组织列表与详情,窄窗口继续使用普通单页跳转。')
            .fontSize(17)
            .lineHeight(28)
            .width('100%')

          this.checkCard('路由', '保留 NavigationMode.Stack 与原有 NavPathStack')
          this.checkCard('布局', '允许内容在页面级容器内收缩和换行')
          this.checkCard('回退', 'EasyGo 切换 original 后仍可单页浏览')

          Button('返回文章列表')
            .width('100%')
            .height(52)
            .onClick(() => {
              this.pageStack.clear()
            })
        }
        .width('100%')
        .padding(24)
        .alignItems(HorizontalAlign.Start)
      }
      .width('100%')
      .height('100%')
    }
    .title('文章详情')
  }

  @Builder
  private checkCard(title: string, detail: string) {
    Column({ space: 6 }) {
      Text(title)
        .fontSize(18)
        .fontWeight(FontWeight.Medium)
        .width('100%')
      Text(detail)
        .fontSize(15)
        .fontColor('#667085')
        .lineHeight(22)
        .width('100%')
    }
    .width('100%')
    .padding(18)
    .backgroundColor('#F2F4FA')
    .borderRadius(16)
    .alignItems(HorizontalAlign.Start)
  }
}

module.json5

{
  "module": {
    "name": "entry",
    "type": "entry",
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": [
      "phone",
      "tablet",
      "2in1"
    ],
    "deliveryWithInstall": true,
    "installationFree": false,
    "easyGo": "$profile:easy_go",
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:layered_image",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "ohos.want.action.home"
            ]
          }
        ]
      }
    ],
    "extensionAbilities": [
      {
        "name": "EntryBackupAbility",
        "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
        "type": "backup",
        "exported": false,
        "metadata": [
          {
            "name": "ohos.extension.backup",
            "resource": "$profile:backup_config"
          }
        ]
      }
    ]
  }
}

easy_go.json

{
  "common": {
    "displayModeOptions": {
      "wideWindowMode": "navigationSplit",
      "squareWindowMode": "navigationSplit",
      "navigationSplitOptions": {
        "homePage": "navBar",
        "relatedPage": "LegacyDetailPage",
        "homeNavigationId": "migrationNavigation",
        "enableReducedContainerSize": true,
        "wideSplit": {
          "ratio": "1 | 2"
        },
        "squareSplit": {
          "ratio": "1 | 1"
        },
        "mode": 1
      }
    }
  }
}
Logo

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

更多推荐