【HarmonyOS 7 平行视界深度实战】08 现有应用迁移、问题排查与降级怎么做
文章目录
前言
一个已有列表与详情的应用,已经能够让用户在单页中完成打开、阅读和返回。团队接入平行视界时,如果同时重排页面、替换路由、修改状态保存方式并开启多项窗口配置,大家就很难判断一次返回异常究竟来自哪处修改。因此,迁移开始前,团队需要保留能够比较的原有路径,让每次新增配置都有明确的观察对象。
大家可以先选择一条从资料列表进入详情的路径,让用户依次选中条目、打开内容、完成阅读并返回列表。这条路径能够呈现页面归属、选择状态和返回操作之间的关系,也方便大家检查窗口变化是否影响原来的任务。两种显示方式都能完成这条路径以后,团队再决定接入哪些业务页面,迁移范围就有可复查的依据。

一、先保留一条能够完整返回的业务路径
团队选择第一批接入页面时,需要判断用户能否通过这些页面独立完成一次任务,以及大家能否准确比较修改前后的结果。资料列表与静态详情依赖较少,可以先用于检查导航关系;支付、草稿编辑或复杂外部入口涉及更多业务状态,排查时还需要区分这些状态的影响。这种范围选择以降低排查成本为依据,不代表复杂页面不能接入平行视界。
大家保留旧路径时,除了保存源代码,还需要记录当前入口、目标页面、返回方式,以及状态由谁维护。如果列表保存选中项、详情保存阅读位置,迁移检查就需要分别确认这两类数据是否发生变化。首页截图只能呈现一个初始画面,无法说明用户打开详情以后还能否回到原来的内容位置,因此它不能单独承担原有行为的比较依据。
原有行为的记录还需要让其他大家能够重新执行。记录除了说明用户完成了阅读,还应写清用户选择了哪个条目、从哪个入口返回,以及返回后应该看到什么。如果应用提供多种详情入口,团队可以先固定其中一条,避免把通知入口与列表入口的结果混在一起。其他入口需要单独列入后续范围,团队在第一条路径稳定以后再逐项接入。
当前应用由 Index 显示列表,LegacyDetailPage 显示详情,两个页面共用 pageStack,migrationNavigation 标识负责它们的导航容器。列表使用初始值为 -1 的 selectedIndex 记录点击位置。应用没有在启动时自动执行列表选择,因此关联页配置不能替代实际选择事件,配置存在也不能证明冷启动已经完成高亮与内容同步。
用户点击条目时,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。配置没有设置 drawableRectHook 或 enableInSplitScreen,系统分屏支持因此保持默认关闭。系统分屏下没有启用平行视界这一现象,不能直接作为迁移失败的依据。项目如果需要增加分屏支持,大家还需要单独开启选项,并检查窗口是否满足对应条件。
大家确认模块引用与页面名称以后,可以根据故障出现的阶段缩小原因。构建失败时,检查应从配置校验和类型信息开始;应用已经启动却没有预期分栏时,大家再核对窗口条件、设备节点与导航归属。如果两栏已经出现但内容错误,排查就转向业务参数与状态。前一步的结果能够排除一部分原因,大家也就不必同时修改多个方向。
现有项目如果包含多个 Navigation,导航归属需要具体到负责当前路径的容器。homeNavigationId 应当指向该容器,配置中的主页与详情也需要属于同一导航结构。当前工程只有一个明确标识的容器,没有提供嵌套导航的验证结果。因此,大家接入多套路由栈时,需要从实际入口找到管理目标路径的那一套,再核对配置指向。
设备专用节点同样会影响实际生效的配置。如果项目已经为某类设备设置专用配置,公共节点中的修改可能没有成为该设备最终使用的设置。大家需要先确认运行设备采用哪份配置,再比较新增字段。团队暂时缩小设备范围时,也需要检查专用节点保留的内容,避免修改了一处显示方式,却遗漏另一份实际生效的配置。
排查记录可以同时写明现象、检查位置和结论限制,方便其他大家复现问题。下表帮助大家选择检查入口,但实际原因仍然需要通过当前设备上的操作确认。
| 当前现象 | 优先检查的位置 | 不能直接作出的判断 |
|---|---|---|
| 配置构建失败 | easyGo 引用、字段组合、Schema 错误位置 | 不能只归因于 ArkTS 代码 |
| 宽窗口仍为普通页面 | 窗口条件、设备覆盖、显示方式、导航 ID | 不能仅凭设备是平板认定应当分栏 |
| 打开了错误内容 | 点击对象、传入参数、目标页读取 | 两栏出现不代表业务数据正确 |
| 一次返回退出多层 | pop、clear 与已有返回拦截 | 不能只修改系统显示配置 |
| 内容拥挤或被截断 | 实际取宽接口、容器选项、局部布局 | 不能先假设比例配置没有生效 |
例如,应用已经构建成功,用户进入详情后却一直看到固定正文,大家就需要检查详情是否实现了按标识读取内容。比例修改不会替详情补充这项数据读取功能。如果配置尚未通过校验,构建日志已经指出更早的失败位置,大家就应先处理配置问题,而无需推测尚未运行的两栏画面。

小批接入也让同一问题能够在普通配置下进行对照。如果两种配置都出现错误,大家需要检查共有的页面与业务逻辑;如果错误只在平行视界中出现,排查就集中到新增配置及尺寸行为。这个比较需要保持相同版本、数据、设备和窗口,其他变化才不会干扰原因判断。
团队一次只增加少量配置,并不表示每批修改只能包含一个字段。具有依赖关系的字段需要一起组成合法配置,例如显示方式与对应选项不能任意拆开。大家可以分阶段加入能够独立观察的容器或窗口选项,再比较各阶段的结果。修改范围需要以能否解释结果为依据,机械追求最少行数可能产生无效的中间配置,使团队误判平台兼容性。
三、回退配置也必须能够独立构建和使用
问题定位以后,团队需要根据失败位置确定回退范围。只有某类设备或窗口不稳定时,应用可以在该范围保留普通页面;通用导航关系还没有理清时,团队则需要暂时恢复原有显示方式。局部回退与整体回退的选择,取决于两种路径能否继续完成业务,配置项的数量不能单独决定范围。
original 表示普通显示方式,大家回退时还需要处理原有的导航选项。当前配置包含专用于平行视界的 navigationSplitOptions;当大家把宽横屏和方形宽屏都改为 original 时,保留这个对象会导致配置校验失败。因此,大家需要同时移除不再适用的选项,准备完整合法的回退配置,再构建对照包。
大家可以对照下表检查回退需要修改的配置位置,避免只完成显示方式替换,却保留不适用的选项。
| 配置位置 | 平行视界包 | 普通页面对照包 |
|---|---|---|
wideWindowMode | navigationSplit | original |
squareWindowMode | navigationSplit | original |
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
}
}
}
}
更多推荐

所有评论(0)