第30篇|窗口缩放后页面重置:把布局状态和业务状态分开
摘要:窗口缩放后页面重置,常见于把布局断点、选中项、筛选条件、滚动位置都塞进同一个页面状态。自由窗口、横竖屏或分屏变化时,页面重新布局,业务状态也被顺手清掉。更稳的做法是把布局状态和业务状态分开:窗口变化只更新布局模式,课程选中、筛选条件、草稿输入等业务上下文要保留下来。
一个平板页面在全屏时左侧列表、右侧详情显示正常,拖成窄窗口后切成单列;再拖回宽屏,用户刚选中的课程丢了,筛选条件也回到默认。问题不是断点本身,而是断点切换时重新初始化了整页状态。
这篇文章解决四个实际问题:
- 如何区分窗口布局状态和业务上下文。
- 如何用 LayoutPolicy 统一断点判断。
- 如何在结构切换时保留选中项和筛选条件。
- 如何验证分屏、旋转和长内容路径。
先拆出布局状态和业务状态
| 状态 | 示例 | 窗口变化时是否重置 |
|---|---|---|
| 布局状态 | compact / medium / expanded | 可以重算 |
| 业务状态 | selectedCourseId、filterKey | 不应重置 |
| 输入状态 | 搜索词、草稿 | 不应因缩放丢失 |
| 临时视觉状态 | hover、展开动画 | 可按布局重置 |
窗口变化只能影响排布方式,不能代表用户换了业务上下文。
用 LayoutPolicy 集中断点
exporttypeLayoutMode='compact'|'medium'|'expanded'exportclassLayoutPolicy{resolve(widthVp:number):LayoutMode{if(widthVp<600){return'compact'}if(widthVp<840){return'medium'}return'expanded'}}断点不要散在多个组件里。否则列表、详情、工具栏各自判断宽度,缩放时就容易出现结构不同步。
业务上下文单独建模
exportinterfaceCourseWorkspaceState{selectedCourseId:stringfilterKey:stringkeyword:stringscrollAnchorId:string}exportfunctioncreateDefaultWorkspaceState():CourseWorkspaceState{return{selectedCourseId:'',filterKey:'all',keyword:'',scrollAnchorId:''}}这份状态描述用户在做什么。窗口宽度改变时,它不应该被重新创建。
页面只在尺寸变化时更新布局模式
@Entry@Componentstruct CourseWorkspacePage{@StatelayoutMode:LayoutMode='compact'@Stateworkspace:CourseWorkspaceState=createDefaultWorkspaceState()privatelayoutPolicy:LayoutPolicy=newLayoutPolicy()privatehandleWidthChange(widthVp:number):void{constnext=this.layoutPolicy.resolve(widthVp)if(next===this.layoutMode){return}this.layoutMode=next}}handleWidthChange()只更新layoutMode。它不清筛选、不清选中项、不重新请求业务数据。
按布局模式切结构
@BuilderprivateWorkspaceContent(){if(this.layoutMode==='compact'){CourseSingleColumn({workspace:this.workspace,selectCourse:(id:string)=>this.selectCourse(id)})}else{CourseSplitPane({workspace:this.workspace,selectCourse:(id:string)=>this.selectCourse(id)})}}结构可以切,传入的业务状态不变。这样从单列切回双列时,右侧详情还能显示刚才选中的课程。
缩放后恢复锚点
privateselectCourse(courseId:string):void{this.workspace={...this.workspace,selectedCourseId:courseId,scrollAnchorId:courseId}}窗口变化后如果列表重新排布,至少要知道用户刚才关注哪一项。scrollAnchorId可以帮助列表恢复到合理位置。
验证清单
- 宽屏选中课程后拖到窄屏,选中项仍然存在。
- 窄屏编辑搜索词后拖回宽屏,搜索词不丢。
- 分屏宽度来回拖动,页面不重复请求无关数据。
- 横竖屏切换后按钮和长标题不挤压。
- 空态、详情态、编辑态都走一次缩放路径。
宽窄布局不要共用一套临时组件状态
从双栏切单栏时,组件树可能变了。如果把业务状态藏在某个子组件里,结构切换就会丢。
| 状态 | 应放位置 |
|---|---|
| 当前选中的课程 id | 页面或页面级 Store |
| 当前筛选条件 | 页面或 Service 查询条件 |
| 列表项展开动画 | 子组件内部 |
| 详情页编辑草稿 | 草稿服务或页面级状态 |
判断标准很简单:切换布局后用户仍然关心的状态,应放在布局结构之外;只和视觉展开相关的状态,可以跟着组件销毁。
用页面级 Store 保住业务上下文
exportclassCourseWorkspaceStore{privatestate:CourseWorkspaceState=createDefaultWorkspaceState()getState():CourseWorkspaceState{returnthis.state}updateSelectedCourse(courseId:string):CourseWorkspaceState{this.state={...this.state,selectedCourseId:courseId,scrollAnchorId:courseId}returnthis.state}}如果页面结构复杂,可以把业务上下文放到页面级 Store。注意这里仍然不是全局长期状态,它只是帮助同一页面在布局结构变化时保住上下文。
验证时要拖动窗口,不只改模拟宽度
真实自由窗口变化会连续触发多次尺寸变化。验证时要覆盖慢拖、快速拖、分屏、旋转和后台恢复。只在代码里手动传一个宽度值,无法发现连续变化导致的抖动、重复请求和焦点丢失。
验收时至少记录三类现象:
- 断点切换时是否重新请求业务数据。
- 选中项、搜索词和草稿是否保留。
- 长标题、按钮和列表项是否出现挤压。
缩放问题要看连续变化,不看单点宽度
窗口拖动不是一次事件,而是一串连续尺寸变化。页面如果每次宽度变化都重新请求数据、重建列表或清空详情,就会在拖动过程中明显抖动。我的做法是让尺寸变化只更新layoutMode,并且只有跨过断点时才切结构;如果宽度仍在同一个断点范围内,就不要触发业务刷新。
连续拖动时重点看两件事:
- 同一断点范围内不应反复切结构。
- 跨断点时只更新布局,不清业务上下文。
断点变化后的内容恢复规则
| 场景 | 恢复目标 |
|---|---|
| 双栏切单栏 | 打开当前选中课程详情 |
| 单栏切双栏 | 左侧保留列表位置,右侧显示当前课程 |
| 搜索中缩放 | 保留关键词和筛选结果 |
| 编辑中缩放 | 保留草稿,不重新创建编辑页 |
这些规则要在开发前写清楚。否则每个组件都会按自己的理解处理缩放,最后就会出现列表恢复了,详情没恢复;搜索词还在,选中项没了。
发布前验收不要只看模拟器
如果目标设备包含平板、折叠屏或 PC 自由窗口,至少要真机或对应模拟器拖动窗口验证一次。检查时重点看三件事:布局切换有没有闪白、业务选择有没有丢、长标题和按钮有没有被压扁。视觉不稳定通常比代码报错更早暴露结构问题。
验收记录建议保留:
- 当前设备或模拟器型号。
- 横竖屏、分屏、自由窗口三组结果。
- 仍需真机确认的布局风险。
交付前验收表
| 验收项 | 通过标准 |
|---|---|
| 断点切换 | 只切布局,不清业务状态 |
| 搜索与筛选 | 缩放后仍保留当前条件 |
| 详情选中 | 宽窄布局切换后仍指向同一课程 |
| 长文案 | 标题、按钮、空态不挤压 |
窗口类问题要看连续动作。拖动过程里如果看到闪白、重新加载、选中项丢失,就说明布局状态仍然影响了业务状态。
线上回归时重点看状态是否被误初始化
窗口变化后如果页面重新走了“初始化默认状态”,通常会出现三个信号:筛选条件回到全部、当前详情页关闭、列表滚动回到顶部。排查时我会先搜索createDefaultWorkspaceState()的调用位置,确认它只在页面首次创建或用户明确重置时使用,而不是在每次断点变化时使用。
rg"createDefaultWorkspaceState|handleWidthChange|layoutMode|selectedCourseId"entry features common rg"aboutToAppear|onAreaChange|onPageShow"entry features common这两条命令能把布局切换、页面生命周期和业务状态初始化位置放在一起看。只要发现尺寸变化回调里重新创建业务状态,就要先移出去。
小结:窗口变化只应该改变布局
一多适配里,窗口变化是布局事件,不是业务重置事件。断点判断集中在 LayoutPolicy,业务上下文单独保存,视图按布局模式切结构。这样自由窗口、分屏和旋转不会把用户正在做的事情清掉。
ear|onAreaChange|onPageShow" entry features common
这两条命令能把布局切换、页面生命周期和业务状态初始化位置放在一起看。只要发现尺寸变化回调里重新创建业务状态,就要先移出去。 ## 小结:窗口变化只应该改变布局 一多适配里,窗口变化是布局事件,不是业务重置事件。断点判断集中在 LayoutPolicy,业务上下文单独保存,视图按布局模式切结构。这样自由窗口、分屏和旋转不会把用户正在做的事情清掉。