
平行视界把大屏应用拆成左右两栏以后我遇到的第一个麻烦不是布局而是路由右栏打开附件临时全屏再返回时系统看起来回来了业务路由栈却可能已经丢了一层。一、这次我从一条“返回错误”日志开始查Demo 叫MailDesk是一个邮件阅读应用。在大屏设备上我让它使用平行视界的导航类分栏左侧邮件列表保持稳定右侧显示当前邮件详情。当前测试邮件是MAIL-20260930-108分栏模式记为NAVIGATION ratio 1:2正常链路很简单Inbox → MailDetail问题出在附件。用户在MailDetail里点击产品评审材料.pdf我希望附件进入一个临时全屏AttachmentPreview。看完以后返回应该重新回到左右分栏而且右栏仍然是原来的MailDetail。第一版偶尔会出现这种情况AttachmentPreview → Inbox也就是全屏页退出后右栏详情丢了。开始我怀疑是平行视界恢复慢后来对照 HiLog 才发现真正出错的是我们自己的导航状态打开全屏页时把“当前页面”覆盖掉了却没有留下进入全屏前的右栏快照。HarmonyOS 7 的平行视界面向折叠屏、平板等大屏场景官方介绍了左右推挤、固定覆盖等分栏方式也支持 1:1、1:2、2:1 等比例和拖拽调整。能力负责的是分栏体验但应用自己的 Navigation 路由和业务选中态仍然要管理好。所以这篇我不展开讲如何配置整套 EasyGo而只盯住一个具体问题临时全屏前后右栏路由怎么不丢。二、布局状态和路由状态一开始就不能混成一个字符串我最早写过这样的变量State currentPage: string Inbox在单栏页面里勉强够用到了平行视界就不够了。因为此时至少同时存在三种信息左栏是谁右栏是谁有没有临时全屏页面覆盖在上面。如果只用一个currentPage打开AttachmentPreview后它会变成附件页等附件页关闭时代码已经不知道之前右栏是什么。后来我把状态拆成一个快照。这段代码解决什么问题把分栏路由和临时全屏路由拆开记录。export interface RouteSnapshot { routeStack: string[] currentRoute: string selectedMailId: string splitMode: NAVIGATION | SHOPPING splitRatio: 1:1 | 1:2 | 2:1 fullscreenRoute?: string } export class ParallelRouteStore { snapshot: RouteSnapshot { routeStack: [Inbox], currentRoute: Inbox, selectedMailId: , splitMode: NAVIGATION, splitRatio: 1:2 } save(): RouteSnapshot { return JSON.parse(JSON.stringify(this.snapshot)) as RouteSnapshot } }这里splitMode是 MailDesk 自己为了调试定义的业务枚举并不是我在模拟某个官方字段。这样做的目的是把 EasyGo 分栏策略和应用当前业务页面同时记录下来。当右栏打开MailDetail时快照变成routeStack [Inbox, MailDetail] currentRoute MailDetail selectedMailId MAIL-20260930-108 splitMode NAVIGATION splitRatio 1:2到这里页面到底长什么样已经可以从状态推回来。三、临时全屏之前先保存“返回以后要恢复什么”附件预览是一次特殊跳转。它和普通右栏详情跳转不一样用户只是暂时离开分栏场景并没有放弃当前邮件上下文。所以我在打开附件前先保存当前快照。这段代码解决什么问题进入全屏附件前保存右栏路由避免返回时只能猜。export class ParallelRouteController { private stack: string[] [Inbox] private lastSplitSnapshot?: RouteSnapshot openMailDetail(mailId: string): void { this.stack [Inbox, MailDetail] AppStorage.setOrCreate(selectedMailId, mailId) console.info( [MailDesk] routeMailDetail stack${JSON.stringify(this.stack)} ) } openAttachmentPreview(fileId: string): void { this.lastSplitSnapshot this.captureSplitSnapshot() this.stack.push(AttachmentPreview) AppStorage.setOrCreate(fullscreenRoute, AttachmentPreview) AppStorage.setOrCreate(previewFileId, fileId) console.info( [MailDesk] fullscreen AttachmentPreview fileId${fileId} ) } }这里没有把临时全屏理解成“新的主业务页面”而是把它当成覆盖在当前分栏任务上的一个短生命周期页面。这个判断很重要。如果用户从邮件列表主动进入另一个一级模块比如日历那么当然应该重新组织导航栈但附件预览、图片放大、文档阅读这类页面退出后通常应该回到原来的业务上下文。四、返回不是简单 pop一定要看被 pop 的是什么第一版 Bug 就发生在这里。我当时统一调用pop()然后根据栈顶页面重新渲染。问题是全屏页进入期间平行视界自身的布局变化和页面生命周期可能让 UI 重建如果只依赖当前组件里的局部状态恢复时信息不完整。后来我专门为临时全屏做了一条恢复路径。这段代码解决什么问题关闭 AttachmentPreview 时恢复进入全屏前的分栏快照。closeAttachmentPreview(): void { const snapshot this.lastSplitSnapshot if (!snapshot) { this.stack [Inbox] return } this.stack [...snapshot.routeStack] AppStorage.setOrCreate(selectedMailId, snapshot.selectedMailId) AppStorage.setOrCreate(splitMode, snapshot.splitMode) AppStorage.setOrCreate(splitRatio, snapshot.splitRatio) AppStorage.setOrCreate(fullscreenRoute, ) this.lastSplitSnapshot undefined console.info( [MailDesk] restore route${snapshot.currentRoute} stack${JSON.stringify(this.stack)} ratio${snapshot.splitRatio} ) }这段代码的关键不是多存几个字段而是恢复动作有明确来源恢复保存过的状态不重新猜状态。测试里从全屏页返回后的结果应该始终是currentRoute MailDetail stack [Inbox, MailDetail] splitMode NAVIGATION ratio 1:2如果右栏内容是靠“默认打开第一封邮件”重新生成的即使页面看起来也有邮件详情那仍然是错误恢复。DevEco Studio 截图里我保留了这条完整链路。右侧大屏模拟器是 MailDesk 的双栏邮件页底部日志依次记录打开MAIL-20260930-108、进入AttachmentPreview、再恢复到MailDetail。五、平行视界下右栏选中态其实也是路由的一部分这个问题是附件修好以后才暴露出来的。全屏返回正常了但左栏有时没有继续高亮MAIL-20260930-108。右栏显示 A 邮件左栏却选中了 B 邮件功能没崩体验却很奇怪。原因是我只保存了页面名没有把selectedMailId当成导航上下文。大屏双栏和手机单页最大的不同是多个区域同时向用户表达“我现在在哪”。因此右栏路由和左栏选择必须来自同一份状态源。我后来不再让InboxPage自己保存选中邮件而是统一读取StorageLink(selectedMailId) private selectedMailId: string 点击邮件以后先更新selectedMailId再执行右栏跳转。全屏返回时也从快照恢复它。这样“路由”和“选择”就不会各走各的。六、临时全屏不能顺手把分栏比例恢复成默认值平行视界允许灵活分栏。官方介绍中给出了 1:1、1:2、2:1 等常用比例并支持用户调整。这意味着用户可能已经把右栏拉得更宽。如果附件全屏返回后应用把分栏重新初始化成默认 1:1虽然内容没丢用户刚才调整过的工作区却被重置了。MailDesk 测试固定使用1:2所以我把比例也放进快照。正式项目如果允许拖拽还应该保存实际比例或者保存能重新计算比例的状态。布局状态不一定都要永久写数据库但在一次连续任务里至少应该跨过临时全屏。七、我最后把“全屏前、全屏中、全屏后”直接做进调试页为了验收方便我做了一个手机端的路由状态页。运行数据如下邮件 IDMAIL-20260930-108 当前模式NAVIGATION 分栏比例1:2 路由栈[Inbox, MailDetail] 临时全屏AttachmentPreview时间线是10:24 打开 MailDetail 10:25 打开 AttachmentPreview 10:26 关闭附件恢复 MailDetail图三显示的是最终恢复结果。最下方不是一句模糊的“恢复成功”而是把结果写得很具体MailDetail右栏 NAVIGATION1:2 [Inbox, MailDetail]我越来越喜欢这种调试页。它不需要开发者在演示时不停切 DevEco Studio 看日志也能让测试同学快速判断“页面看起来对”之外路由栈到底对不对。八、全局返回和右栏返回要先定义产品语义平行视界还有一个容易被忽略的问题返回键到底退谁。在手机单栏里返回通常就是退出当前页面。在双栏里左栏可能一直停在 Inbox右栏却已经走了多层MailDetail → ContactDetail → MailDetail → AttachmentPreview这时候如果没有明确规则全局返回很容易一会儿退出右栏一会儿退出整个页面。MailDesk 的约定是临时全屏存在时优先关闭全屏右栏有业务子层级时优先回退右栏右栏已经回到首层详情时再由应用决定是保持双栏还是退出当前模块。真正项目当然可以有不同规则但一定要先定义再实现。不能让组件层谁先收到返回事件谁就处理。九、右栏连续点击时还要防止同一详情被重复压栈附件全屏修完以后我又做了一轮连续点击测试。在邮件列表里快速点击同一封邮件两次如果每次都无条件 push栈会变成[Inbox, MailDetail, MailDetail]视觉上仍然是同一封邮件所以这个问题很容易漏过去。真正暴露是在用户按返回时第一次返回仍然停在同一详情看起来像返回键失效。因此我把“打开右栏详情”从单纯 push 改成了业务去重当前右栏已经是MailDetail而且selectedMailId没有变化时只刷新内容不增加新的路由层级。如果用户从 A 邮件切到 B 邮件导航模式下也不一定要把 A、B 都压进栈。是否保留右栏历史要根据产品语义决定。MailDesk 的定位更像桌面邮件客户端左侧列表负责切换选择右侧详情只是当前选择的投影。所以 A → B 更适合替换右栏内容而不是产生Inbox → MailA → MailB → MailC否则用户连续查看十封邮件后按十次返回才能退出详情和大屏“快速浏览”的目标正好相反。这件事让我意识到平行视界不是把手机 Navigation 原样拉到大屏。左右栏同时存在以后页面层级本身就需要重新定义。十、进程恢复时我不直接复用上一次的全屏状态会话内恢复和进程级恢复也不是一回事。如果用户正在AttachmentPreview全屏查看附件然后系统回收进程应用下一次重新打开时是否应该直接回到全屏附件这个答案未必是“是”。MailDesk 目前只持久化稳定业务状态selectedMailId splitMode splitRatio stableRoute临时全屏AttachmentPreview被我当成会话状态不作为冷启动默认恢复点。原因很简单附件文件可能已经失效权限可能变化下载缓存可能被清理。冷启动时强行恢复一个临时页面反而容易进入错误状态。所以重启以后我先恢复到[Inbox, MailDetail]然后确认MAIL-20260930-108仍然存在再渲染右栏。如果产品确实要求恢复附件预览那也应该把附件资源有效性检查放在恢复前而不是看到上次 route 名叫AttachmentPreview就直接 push。这种区分可以让路由状态更稳定稳定业务上下文可以持久化短生命周期覆盖页只服务当前会话。十一、分栏比例的恢复也要有设备边界本文测试一直使用1:2但真实设备上不能把这个比例当成绝对规则。用户可能在大平板上把右栏拉得很宽换到更窄的折叠屏以后原来的像素宽度可能已经不适用。所以保存“比例”比保存“左右各多少像素”更有迁移性。即便保存的是比例恢复时仍然要做约束。例如当前窗口宽度不足以舒适显示 1:2应用可以回落到当前设备更合理的预设值。恢复用户工作区的含义不是机械恢复每一个数字而是在当前环境允许的范围内尽量恢复用户的上下文和偏好。我会把这类恢复分成两步读取历史偏好 → 按当前窗口能力重新校验 → 得到本次实际分栏日志里同时打印“请求比例”和“实际比例”调试时就能看出是历史状态错误还是当前设备做了合理降级。十二、最终回归我不再只测“打开和返回”MailDesk 最后的回归链路是打开 Inbox选择MAIL-20260930-108确认右栏出现 MailDetail调整一次分栏比例打开附件全屏返回切另一封邮件再切回 108再次打开附件返回最后执行全局返回。每一步都记录当前selectedMailId currentRoute fullscreenRoute routeStack splitMode splitRatio只有这些值和 UI 同时正确我才认为这一轮通过。大屏应用特别容易出现“看起来还能用”的状态 Bug。左栏有列表、右栏有内容页面没有崩测试很容易顺手点过去。但只要选中态、返回栈或比例恢复不一致用户连续操作几次以后就会感觉这个应用“不听话”。所以平行视界的调试重点不只是每一帧长什么样而是用户操作五分钟以后路由状态还能不能解释得通。十三、我给这个问题留下的三个工程判断第一个判断分栏是布局能力Navigation 是业务导航两者不能互相替代。EasyGo 能帮助应用获得更自然的大屏分栏体验但应用仍然需要清晰维护当前业务页面和选中态。第二个判断临时全屏是一种覆盖状态不一定是新的主路由上下文。附件预览、图片查看、扫码等场景如果退出后应该回到原任务就值得在进入前保存快照。第三个判断恢复要恢复用户刚才的工作区而不是恢复一个“看起来差不多”的默认页面。当前邮件、右栏层级、分栏比例、左栏选中态这些共同组成了用户的上下文。HarmonyOS 7 的平行视界把大屏里的多任务体验做得更自然官方给出的购物模式、导航模式和可调比例已经提供了很好的系统级基础。应用真正要补上的是业务状态和路由恢复。这次问题修完以后我没有再把“附件能打开”当成验收结束点。我真正关注的是打开之前用户在哪打开之后系统变成什么样关闭以后能不能原封不动回到那个任务里。当这条链路跑顺以后平行视界才不是简单的“一分为二”而是一个可以连续工作的应用空间。参考资料HarmonyOS 7 平行视界能力解读https://developer.huawei.com/consumer/cn/forum/topic/0201221235973021541多设备通用适配指南https://developer.huawei.com/consumer/cn/multidevice/adaptive-apps/HarmonyOS 示例代码中心https://developer.huawei.com/consumer/cn/samples/