DirectSurface UIDirectSurface UI
开始使用
文档/业务指南

页面上下文与生命周期

import {  RenderAppContextScope,  RenderCommandScope,  RenderStackPanel,  RenderText,  createAppContextKey,} from 'ds-ui'

复杂业务系统通常由工作台、tab 页面、浮动窗口、弹窗、侧栏和长生命周期服务共同组成。DirectSurface UI 推荐的基本原则是:页面拥有自己的上下文、命令、状态和资源,页面关闭时统一释放。

这篇文档说明页面应该如何组织,而不是规定业务域对象。具体患者、医嘱、病历、审核流程等业务模型由业务系统自己定义。

页面职责

页面层负责组合 UI 和管理页面范围资源。

页面应该负责:

  • 创建页面根布局。
  • 注入页面上下文。
  • 注册页面命令。
  • 保存页面私有状态。
  • 管理页面级订阅、定时器、controller 和缓存。
  • 在关闭时释放页面资源。

页面不应该负责:

  • 定义框架级通用组件。
  • 把当前页面状态写入应用级全局上下文。
  • 在 render 的 performLayout()performPaint() 中发起业务请求。
  • 页面关闭后继续接收异步回调并更新 UI。

推荐结构

Page Root
  RenderAppContextScope
    RenderCommandScope
      RootLayout
        Header
        QueryArea
        Content
        Footer

如果页面由 TabbedWorkspaceDockWorkspace 管理,可以在打开文档时通过 contextValues 注入页面参数。工作区会在文档内容外创建上下文范围。

最小页面骨架

const currentRecordKey = createAppContextKey<{ id: string; title: string }>('record.current') const pageContent = new RenderStackPanel({  orientation: 'vertical',  spacing: 8,  crossAxisAlignment: 'stretch',}) pageContent.addChild(new RenderText('病历页面')) const commandScope = new RenderCommandScope({  commandManager,  debugLabel: 'record-page',  commands: [    {      id: 'record.refresh',      title: '刷新',      execute: () => reload(),    },  ],  child: pageContent,}) const pageRoot = new RenderAppContextScope({  values: [    [currentRecordKey, { id: 'r001', title: '入院记录' }],  ],  child: commandScope,})

这个结构让页面内的按钮、菜单、浮层和命令都可以读取当前页面上下文,并且页面关闭时释放页面 scope。

生命周期阶段

阶段 应做事项 不应做事项
创建 创建 layout、上下文、命令、controller。 发起依赖尺寸的重计算。
挂载 / attach 进入 render tree,获得 owner、theme、focus、popup 能力。 假设所有异步数据已经返回。
激活 刷新命令状态、恢复焦点、按需刷新可见数据。 重建整页对象树。
隐藏 / 切换 保留页面状态,暂停可见性相关任务。 清空页面上下文或销毁 controller。
关闭 关闭页面浮层,释放订阅、定时器、缓存、上下文、命令 scope。 继续执行旧异步回调。
dispose 清理 render 子树和底层资源。 再访问已释放上下文。

页面状态放在哪里

状态 推荐位置 原因
登录用户、租户、全局权限服务 应用级上下文 应用全局共享。
当前 tab 的业务对象 页面级上下文 跟随页面打开和关闭。
当前选中行、查询条件 页面对象或页面级上下文 页面私有,不应污染全局。
表格内部 hover、滚动、编辑状态 组件内部 组件自己的交互状态。
弹窗草稿 浮层上下文或弹窗 controller 随弹窗关闭释放。
大文档数据、图片缓存 文档/页面 controller 关闭文档后释放。

页面级上下文

页面级上下文应包住页面根布局:

const currentRecordKey = createAppContextKey<{ id: string; title: string }>('record.current')const pageScope = new RenderAppContextScope({  values: [    ['page.kind', 'record'],    [currentRecordKey, { id: 'r001', title: '入院记录' }],  ],  child: content,})

使用规则:

  • 页面打开时注入页面参数。
  • 页面内部组件通过上下文读取当前对象。
  • 页面打开的浮层继承页面上下文。
  • 页面关闭时释放页面上下文。
  • 不要把页面上下文中的对象提升到应用级。

页面级命令

页面命令应放到页面 RenderCommandScope

const commandScope = new RenderCommandScope({  commandManager,  debugLabel: 'orders-page',  commands: [    {      id: 'orders.save',      title: '保存',      canExecute: () => canSave(),      execute: () => save(),    },  ],  child: content,})

页面状态变化时调用 commandManager.invalidate()。页面关闭时,scope dispose 后命令自动失效。

弹窗和浮层

由页面打开的弹窗应继承页面上下文,并追加本次弹窗参数。

Page context
  currentRecord = r001

Modal context
  currentRecord = r001
  dialogMode = edit

这样弹窗按钮、内部表单和命令都能读取当前页面对象。不要让弹窗默认从应用根查找上下文,否则在 tab 浮动、工作区拆分或多页面并存时容易读错对象。

异步请求

页面关闭后,异步请求可能仍然返回。页面应有“是否仍然有效”的判断。

let disposed = false const load = async (): Promise<void> => {  const rows = await service.search({})  if (disposed) return  rows.length} disposed = true

更复杂的业务可以使用页面 controller、请求取消器或请求 token。关键是不要在页面关闭后继续更新已经释放的 render 对象。

关闭流程

推荐关闭顺序:

  1. 阻止新的页面操作进入。
  2. 关闭页面拥有的 popup、modal、floating window。
  3. 取消订阅和定时器。
  4. 取消或忽略异步请求返回。
  5. dispose 页面 controller 和缓存。
  6. dispose 页面 command scope。
  7. dispose 页面 app context。
  8. dispose render 子树。

如果使用 RenderAppContextScopeRenderCommandScope 创建页面范围,正常 dispose render 子树时会自动释放它们内部创建的 context/scope。

工作区页面

TabbedWorkspaceDockWorkspace 适合承载长期打开的业务文档。

打开文档时:

  • 用稳定 document id 管理 tab。
  • 通过 contextValues 注入当前文档参数。
  • 页面内容用 controller 管理业务状态。
  • 关闭前检查脏状态或关闭保护。

切换文档时:

  • 不要销毁非当前页面状态。
  • 可以刷新命令状态,因为当前 active 页面变了。
  • 可以恢复焦点或滚动位置。

关闭文档时:

  • 释放页面上下文和命令 scope。
  • 释放文档 controller、缓存和输入会话。
  • 确认没有 popup 仍持有页面对象。

调试和验证

问题 检查方式
页面读不到当前对象 用 Layout Inspector 查看页面根是否有 RenderAppContextScope
命令状态不刷新 确认业务状态变化后调用了 commandManager.invalidate()
弹窗读错页面数据 确认打开弹窗时传入了 source 或继承了页面上下文。
页面关闭后内存不下降 用 Heap Snapshot 查 page root、controller、context、popup、缓存是否仍被引用。
旧页面命令还能执行 确认命令没有注册到 root scope,页面 scope 是否已 dispose。

常见错误

页面数据放到应用级上下文

多 tab 场景会互相覆盖,页面关闭后还会被全局引用。页面私有状态必须放页面对象或页面级上下文。

页面命令不随页面释放

如果用 manager.register() 注册页面命令,命令会进入 root scope。页面关闭后它仍然存在。页面命令应使用 RenderCommandScope 或页面私有 CommandScope

弹窗失去调用页面上下文

浮层如果挂到应用 overlay 层,视觉上不在页面下面,但逻辑上仍应继承调用页面上下文。打开浮层时要明确 source/contextValues。

dispose 只释放 controller

页面可能还持有 popup、输入会话、事件订阅、图片缓存、定时器和上下文对象。关闭页面要按资源清单逐项释放。

在 layout 中同步页面状态

performLayout() 可能频繁执行。不要在 layout 中调用 setItems()、发请求、注册命令或写上下文。页面状态同步应在事件、数据回调或 controller 层完成。

页面开发清单

创建新业务页面时至少确认:

  • 是否定义了页面级上下文 key。
  • 页面根是否包了 RenderAppContextScope
  • 页面命令是否包了 RenderCommandScope
  • 页面状态变化后是否刷新命令状态。
  • 页面打开的浮层是否继承页面上下文。
  • 页面关闭时是否释放 controller、订阅、定时器、缓存和 popup。
  • 异步请求返回时是否判断页面仍然有效。
  • 大数据组件是否自己管理滚动和虚拟化。

相关文档