03 解决的是一种非常明确的“临时展示”:
文章
→ 过渡页
→ 全屏 Gallery
→ 返回文章
全屏 14.2 秒以后,article_042 和 612vp 阅读位置都能回到原处。
但真实阅读应用里,还有一类交互完全不同。
例如用户正在读一篇性能文章,右侧想继续:
看图表
记笔记
看讨论
再回图表
这些不是“短暂弹出去看一下”。
它们更像:
同一个文章上下文里的内部工作区。
HarmonyOS 当前官方平行视界示例已经把“路由跳转过程请求横屏显示”和“开启虚拟容器能力”列为典型场景。TwinShelf 04 不去虚构一个系统级 VirtualContainer() ArkUI 组件,而是按照这个场景思路做项目侧实现:
外层 Navigation
继续维护文章级路由;
reader_workspace
组织 Chart / Notes / Discussion 三个内部页面;
横屏只改变 Window Orientation;
内部页面切换不继续污染外层 NavPathStack。
本轮统一数据:
taskId:
parallel_20261003_04
window:
1440 × 900vp
selectedArticleId:
article_042
outerRoute:
ArticleDetail
outerStackDepth:
2
virtualContainerId:
reader_workspace
virtualContainerEnabled:
true
containerPages:
ChartView
NotesView
DiscussionView
containerPageCount:
3
internalSequence:
Chart
→ Notes
→ Discussion
→ Notes
→ Chart
internalSwitches:
4
avgInternalSwitchCost:
2.7ms
orientation:
LANDSCAPE
orientationRequests:
2
orientationSuccess:
2
orientationRestores:
2
notesDraftChars:
86
discussionScrollOffset:
344vp
detailScrollOffset:
612vp
chartStateRetained:
true
leftSelectionRetained:
true
contentRebuildCount:
0
status:
VIRTUAL_CONTAINER_READY

一、虚拟容器最重要的价值不是“有三个 Tab”,而是隔离路由层级
如果没有内部工作区,最直接的写法是:
ArticleDetail
→ push Chart
→ push Notes
→ push Discussion
→ push Notes
→ push Chart
很快外层 NavPathStack 就会从:
depth=2
涨到:
depth=7
用户按返回键时,会连续退五次内部工作页。
这明显不符合阅读语义。
TwinShelf 的原则是:
文章级导航
属于外层 Navigation;
文章内部工作区
属于 reader_workspace。
所以本轮不管内部怎么切:
outerStackDepth
始终=2。
二、进入 Workspace 前先请求横屏,但横屏不是业务路由
当前官方 Window 能力支持:
setPreferredOrientation(…)
TwinShelf 在进入 Workspace 时:
async enterWorkspace(): Promise<void> {
const win =
await window.getLastWindow(
this.uiContext.getHostContext()
)
await win.setPreferredOrientation(
window.Orientation.LANDSCAPE
)
this.orientationRequests++
this.orientationSuccess++
this.store.open(
'reader_workspace',
this.articleStore
.selectedArticleId
)
}
这里横屏只改变:
Window Orientation
不会:
push 一个 LandscapeRoute。
否则方向变化和业务导航又会重新耦合。
三、为什么 Workspace 继续绑 article_042
reader_workspace 并不是一个全局工具页。
它有明确 Owner:
article_042
里面的 Chart、Notes、Discussion 都围绕当前文章。
所以进入容器时保存:
articleId
detailScrollOffset
内部页面不需要重新问:
“你现在看哪篇文章?”
这也是左栏选中态为什么始终保持。
四、内部页面切换用 Store,不继续 Push 外层 Navigation
第一段代码解决的是:三张内部页面怎么切都不改变外层路由深度。
export enum WorkspacePage {
CHART,
NOTES,
DISCUSSION
}
export class ReaderWorkspaceStore {
articleId: string =
'article_042'
current:
WorkspacePage =
WorkspacePage.CHART
notesDraft:
string = ''
discussionScrollVp:
number = 344
switchTo(
page: WorkspacePage
): void {
this.current = page
}
}
本轮内部序列:
Chart
→ Notes
→ Discussion
→ Notes
→ Chart
internalSwitches=4。
外层仍然是:
ArticleDetail
depth=2
五、虚拟容器内部状态必须独立保存
Notes 写了:
86 字
Discussion 滚到:
344vp
Chart 当前筛选和图表状态也保持。
如果切 Tab 以后组件重建:
Notes 草稿归零;
Discussion 回顶部;
Chart 筛选丢失。
这就失去了“工作区”的意义。
所以内部页面的状态 Owner 是:
ReaderWorkspaceStore
不是某个 TabContent 局部变量。
六、第二段代码把三页作为一个工作区呈现
@Component
struct ReaderWorkspacePage {
private store:
ReaderWorkspaceStore =
ReaderWorkspaceStore
.getInstance()
build() {
Column() {
Tabs({
index:
this.store.current
}) {
TabContent() {
ChartView({
articleId:
this.store.articleId
})
}
.tabBar('图表')
TabContent() {
NotesView({
draft:
this.store.notesDraft
})
}
.tabBar('笔记')
TabContent() {
DiscussionView({
scrollOffset:
this.store
.discussionScrollVp
})
}
.tabBar('讨论')
}
.onChange((index) => {
this.store.switchTo(
index as WorkspacePage
)
})
}
}
}
这里没有:
pathStack.pushPathByName
内部切换只是:
Workspace State
变化。
这就是“路由状态隔离”的核心。
七、官方“虚拟容器”场景和 TwinShelf 项目实现要区分
HarmonyOS 7 官方平行视界示例明确覆盖:
开启虚拟容器能力
但 TwinShelf 文章里的:
reader_workspace
是项目侧工作区 ID 和状态组织方式。
它不等于一个叫:
VirtualContainer()
的 ArkUI 公共组件。
文章只借用官方场景能力的工程思路:
多个内部页面可以组织成一个连续业务容器,不让它们都污染外层分栏路由。
这个边界必须说清楚。
八、横屏请求和容器切换可能同时发生,不能各自重建页面
进入 Workspace 时:
orientation
从当前方向变 LANDSCAPE;
container
从 ArticleDetail 进入 reader_workspace。
两个变化如果分别触发:
页面销毁 / 重建
状态会闪断两次。
TwinShelf 把:
Orientation
Workspace State
都写入稳定 Store。
UI 只根据最新状态重新布局。
当前:
contentRebuildCount=0
九、退出 Workspace 必须先保存内部状态,再恢复方向
第三段代码解决退出顺序:
async exitWorkspace(): Promise<void> {
this.store.persistCurrentState()
const win =
await window.getLastWindow(
this.uiContext.getHostContext()
)
await win.setPreferredOrientation(
window.Orientation
.AUTO_ROTATION
)
this.orientationRestores++
this.store.close()
}
当前:
orientationRequests=2
orientationSuccess=2
orientationRestores=2
方向请求与恢复完全成对。
十、为什么恢复 AUTO_ROTATION,而不是写死 PORTRAIT
TwinShelf 是大屏阅读应用。
退出 Workspace 以后用户可能仍然处在:
平板横屏;
折叠屏展开;
PC 窗口。
如果直接:
PORTRAIT
反而会破坏外层设备姿态。
所以项目恢复:
进入前的 Orientation Snapshot
示例代码里用 AUTO_ROTATION 表达“交还给外层策略”,真正产品会恢复进入 Workspace 前保存的值。
十一、Chart 状态保持比“图表还能显示”更重要
本轮最终回到 ChartView。
要求:
chartStateRetained=true
也就是:
筛选条件
时间范围
高亮数据点
仍然是进入 Notes / Discussion 前的状态。
内部页面切换不能把 Chart 当作“每次重新初始化的数据页”。
十二、Notes 86 字不能因为切去讨论而自动保存或丢失
当前:
notesDraftChars=86
它仍然是草稿。
从 Notes 切到 Discussion:
不自动保存;
不清空 dirty state。
再切回来:
仍然 86 字。
内部页面切换只是展示切换,不应该擅自改变业务事务状态。
十三、Discussion 的 344vp 也属于工作区状态
DiscussionView:
scroll=344vp
切到 Notes,再切回来以后仍然 344vp。
如果每个内部页面都依赖组件是否“恰好还在树上”来保存状态,后续容器实现变化时很容易丢。
所以所有关键状态都明确进入 Store。
十四、DevEco 图重点看 outerDepth 一直是 2
开发图:

HiLog:
taskId=
parallel_20261003_04
enter workspace
article=
article_042
outerDepth=
2
orientation=
LANDSCAPE
virtualContainer=
reader_workspace
enabled=
true
pages=
3
switch:
Chart
→ Notes
→ Discussion
→ Notes
→ Chart
count=
4
avgCost=
2.7ms
notesDraft=
86
discussionScroll=
344vp
chartRetained=
true
leftSelectionRetained=
true
orientationRequests=
2
success=
2
restores=
2
outerDepth=
2
contentRebuild=
0
status=
VIRTUAL_CONTAINER_READY
整个过程中外层 Navigation 没被内部工作区污染。
十五、手机运行图展示的是“虚拟容器状态矩阵”
最终运行图:

上半部分:
reader_workspace
3 pages
Chart / Notes / Discussion
下半部分:
article_042
1440×900vp
outerDepth=2
2/2 orientation
internalSwitches=4
avg=2.7ms
contentRebuild=0
连续性检查:
article retained
612vp retained
internal route isolated
chart state retained
orientation restored
最终:
VIRTUAL_CONTAINER_READY
十六、04 最后固定八组场景测试
第一组,article_042 进入 reader_workspace。
第二组,请求 LANDSCAPE 成功。
第三组,Chart → Notes,外层 depth 不变。
第四组,Notes 草稿 86 字保持。
第五组,Discussion 344vp 保持。
第六组,Chart 状态切出去再回来仍保持。
第七组,退出容器后恢复外层方向策略。
第八组,外层 article_042 / 612vp / depth=2 全部不变。
全部通过后:
VIRTUAL_CONTAINER_READY
才成立。
十七、下一篇:容器内部稳定以后,真正做“应用内分屏”
TwinShelf 到 04 已经解决:
主从分栏
分栏比例
右栏连续路由
过渡页
沉浸全屏
横屏请求
虚拟容器
接下来最接近平行视界“多任务”的场景是:
同一个应用里
再触发一次应用内分屏。
例如左侧继续阅读,右侧临时打开“我的收藏 / 比较阅读 / 引用摘录”工作区。
05 会继续:
In-App Split × State Restore
最后 06 再把平行视界的路由、比例、全屏、横屏、容器和应用内分屏做一轮完整回归。
十八、虚拟容器内部应该有自己的返回语义
reader_workspace 里当前有:
Chart
Notes
Discussion
用户在 Discussion 里按返回,不应该立刻退出整个 Workspace。
TwinShelf 的内部返回优先级是:
如果内部页面有历史
→ 回内部上一页;
如果已经回到默认 Chart
→ 才退出 reader_workspace;
这样外层 Navigation 只看到“进入一次 Workspace、退出一次 Workspace”。
不会因为内部切换五次,就让系统返回要按五次才能回文章。
十九、内部页面状态和外层文章状态需要两套 Snapshot
TwinShelf 现在有两层状态:
OuterArticleSnapshot
article_042
detailScroll=612vp
ReaderWorkspaceSnapshot
currentPage
notesDraft=86
discussionScroll=344vp
chartFilter
这两层不能合成一个超级 Store。
原因是生命周期不同。
用户退出 Workspace:
内部状态可以决定保留多久;
外层文章状态必须立即恢复。
以后如果产品决定“下次进入工作区从 Chart 开始”,只需要清 ReaderWorkspaceSnapshot,不会影响 article_042 的阅读位置。
二十、横屏只请求工作区阶段,不应该污染整个应用
进入 Workspace:
request LANDSCAPE
退出以后:
restore previous orientation
而不是把应用全局方向永久改成横屏。
这对 TwinShelf 很重要,因为普通文章阅读仍然可能在竖屏手机上运行。
“某个路由更适合横屏”和“整个应用只支持横屏”是两个完全不同的产品决定。
04 只实现前者。
二十一、横屏请求失败时,Workspace 仍然要能工作
设备策略、窗口模式或系统状态都可能导致:
setPreferredOrientation
失败。
TwinShelf 不会因为横屏请求失败就让工作区彻底不可用。
回退逻辑:
记录 orientation error
→ 保持当前方向
→ 继续打开 reader_workspace
→ 使用响应式布局降级
只是:
orientationSuccess
不会 +1。
本轮 2/2 全成功,所以没有触发降级。
真正产品应当把“横屏增强体验”和“核心功能可用”分开。
二十二、虚拟容器内部页面也要做去重
用户当前已经在 Notes,再次点击 Notes:
不需要重建 NotesView
不需要重新恢复草稿
不需要 internalSwitches++
项目在 switchTo() 里先比较:
target == current
相同就直接返回。
内部页面越多,这种幂等越重要。
否则状态统计会把“重复点击”误算成真实路由切换,页面也会出现不必要刷新。
二十三、图表、笔记、讨论的加载策略应该不同
Chart 可能需要:
读取统计数据
计算图形
Notes 主要是:
本地编辑状态
Discussion 可能:
网络分页
虚拟容器并不意味着三张内部页必须统一生命周期。
TwinShelf 当前策略:
Chart
首次进入加载,后续保留结果;
Notes
一直保留编辑 Store;
Discussion
保留滚动和第一页缓存,长列表允许分页回收。
“内部路由隔离”解决的是外层栈污染,不等于把所有页面强制永久驻留内存。
二十四、容器关闭时要清理临时资源,但不要清业务草稿
退出 Workspace 时可以释放:
图表渲染对象
临时网络请求
订阅监听
但:
notesDraft=86
如果尚未保存,不能因为容器退出就直接丢。
TwinShelf 的 Store 把:
transient runtime resource
和:
business state
分开。
后面 06 做回归时,会分别检查资源是否释放、草稿状态是否按产品规则保留。
二十五、容器内部切换成本 2.7ms 只代表状态提交
本轮:
avgInternalSwitchCost=2.7ms
它不代表:
Discussion 网络加载也只要 2.7ms。
这里统计的是:
currentPage 改变
内部布局提交
已有状态恢复
数据请求和图表计算另有耗时。
性能指标只有口径明确才有比较价值。
二十六、Virtual Container 不是为了“页面更多”,而是为了让外层路由更简单
做完 04 后,外层仍然只有:
文章列表
ArticleDetail
reader_workspace
而不是:
ArticleDetail
Chart
Notes
Discussion
Notes
Chart
…
这就是虚拟容器思路真正的工程收益。
外层路由更容易:
深链恢复
返回收口
跨形态恢复
状态持久化
内部工作区自己处理局部复杂度。
二十七、VIRTUAL_CONTAINER_READY 的完整边界
最终状态表示:
article_042 外层上下文保持;
外层 NavPathStack depth=2;
reader_workspace 内有 3 个内部页面;
4 次内部切换不污染外层栈;
Notes 草稿 86 字保持;
Discussion scroll=344vp 保持;
Chart 状态保持;
横屏请求与恢复成对;
退出 Workspace 后 article scroll=612vp;
contentRebuildCount=0。
做到这一步以后,TwinShelf 才真正拥有了“同一右栏里的连续工作区”,而不只是三个能互相切换的 Tab。
二十八、内部工作区还要考虑深链恢复
如果应用因为系统回收重新启动,用户之前停在:
article_042
reader_workspace
Notes
draft=86
恢复时不能只把外层 Navigation 恢复到 ArticleDetail,然后丢掉内部工作区。
TwinShelf 会把可恢复状态拆成两层持久化键:
outerRouteSnapshot
workspaceSnapshot
先恢复 article_042 与 612vp,再根据 workspaceSnapshot 决定是否重新打开 reader_workspace 以及恢复到 Notes / Discussion / Chart。这样外层路由恢复和内部容器恢复互相独立,也方便在版本升级后对内部状态做兼容迁移。
二十九、横屏工作区还要验证返回时的焦点和手势上下文
横屏进入 Chart/Notes/Discussion 后,焦点、滚动手势和编辑输入法都可能发生变化。退出 Workspace 时除了恢复 Orientation,还要把交互上下文交还给 ArticleDetail,例如恢复正文可滚动、关闭 Notes 的输入法焦点、解除图表手势监听。否则页面视觉上回到了文章,手势却仍被已经隐藏的 ChartView 消费,会出现“文章滑不动”的隐蔽问题。06 的最终回归会把这类资源与交互监听一并纳入收口检查。
最终,04 的重点不是把页面变成三个 Tab,而是证明同一篇文章可以在横屏和内部工作区之间持续工作,外层 Navigation 仍保持简洁,内部状态也有清晰归属。
这套状态隔离也会直接成为下一篇应用内分屏的基础。
后续继续稳定复用。
参考资料
- HarmonyOS 平行视界示例:
https://developer.huawei.com/consumer/cn/samples/ - 窗口旋转与横屏能力:
https://developer.huawei.com/consumer/en/doc/harmonyos-guides/rotation - Navigation 分栏开发:
https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/arkts-navigation-split-mode - Navigation:
https://developer.huawei.com/consumer/cn/doc/doccenter-references/api/ts-basic-components-navigation



