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

应用上下文

本文示例统一从包入口导入:

import {  AppOverlayService,  CommandManager,  DisposableBag,  RenderAppContextScope,  RenderStackPanel,  RenderText,  createAppContext,  createAppContextKey,} from 'ds-ui'

应用上下文用于在复杂业务系统中按范围传递对象,例如登录用户、租户、权限服务、页面参数、当前患者、弹窗临时参数和需要随页面关闭释放的资源。

它不是全局 store 的替代品。它解决的是“某个组件、页面、弹窗如何拿到当前范围内的依赖”,并且让这些依赖跟随对应生命周期释放。

如果你想先看完整业务页面中的上下文如何分层,见 常用业务场景组合

核心概念

概念 说明
AppContextKey<T> 类型安全的上下文 key。推荐业务代码优先使用。
AppContextRegistry key-value 注册表,支持父级链、读取、删除、清空和释放。
RenderAppContextScope 把上下文挂到 render tree 某个范围。子组件、命令和浮层可以沿链读取。
应用级上下文 应用全局共享,例如登录用户、租户、权限服务、字典服务。
页面级上下文 当前页面或 tab 私有,例如当前患者、当前文书、页面缓存。
浮层上下文 弹窗或浮层打开时追加的临时参数。

推荐层级

Application appContext
  登录用户、租户、全局服务、权限服务

Workspace / Tab / Page RenderAppContextScope
  当前页面参数、当前业务对象、页面级缓存、页面级服务

Modal / Popover / Floating Window contextValues
  弹窗参数、临时草稿、打开来源

越局部的对象越应该放在越靠近使用者的位置。当前患者、当前文书、当前选中行通常不应该放在应用级上下文。

上下文不是按 key 区分“全局”或“局部”,而是按挂载位置决定作用范围。应用根上的值全局可见;页面、tab 或弹窗 scope 上的值只对该子树可见。读取时从当前 render 节点向上查找,越近的值优先。

如果业务对象随着 tab 生命周期存在,就把它注入 tab 或页面根;如果对象随着弹窗存在,就在打开弹窗时传入本次浮层上下文。不要为了方便读取就把页面私有对象放到应用根。

典型业务分层

业务范围 适合放入的对象 不适合放入的对象
应用根 登录用户、租户、权限服务、字典服务、应用级配置 当前患者、当前文书、页面草稿、大数据结果集
工作区或 tab 当前模块参数、当前打开对象、tab 级缓存 全局权限模型、跨应用单例
页面根 查询条件、选中行、编辑草稿、页面级服务、页面命令依赖 其他 tab 需要共享的数据
弹窗或浮层 弹窗参数、临时草稿、打开来源、被编辑行 长生命周期缓存、全局服务实例

判断方式很简单:对象应该跟谁一起销毁,就放到谁的上下文范围内。

定义 key

业务代码应把 key 定义在稳定模块中,不要在函数内部反复创建。

interface CurrentRecord {  id: string  title: string} const currentRecordKey = createAppContextKey<CurrentRecord>('record.current')const currentUserKey = createAppContextKey<{ id: string; name: string }>('session.user')

description 用于调试显示,应使用稳定、可读、能区分业务域的名称。

应用级注入

应用启动时可以把全局服务和登录态注入根上下文。应用级对象的生命周期通常等同于当前应用实例。

const currentUserKey = createAppContextKey<{ id: string; name: string }>('session.user')const appContext = createAppContext() appContext.set(currentUserKey, { id: 'u001', name: '张医生' })appContext.set('tenant', 'demo-hospital')

全局上下文适合放:

  • 登录用户和租户。
  • 权限服务、字典服务、配置服务。
  • 应用级事件总线或全局 store。
  • 生命周期等同应用实例的缓存。

不适合放:

  • 当前页面选中行。
  • 当前 tab 打开的业务对象。
  • 大文档、大图片、页面级数组。
  • 弹窗临时参数。

页面级注入

页面级上下文用 RenderAppContextScope 包住页面根布局。页面关闭时 scope dispose,默认会释放由它创建的上下文。

const currentRecordKey = createAppContextKey<{ id: string; title: string }>('record.current')const pageContent = new RenderStackPanel({  orientation: 'vertical',  spacing: 8,}) pageContent.addChild(new RenderText('入院记录')) const pageScope = new RenderAppContextScope({  values: [    [currentRecordKey, { id: 'r001', title: '入院记录' }],  ],  child: pageContent,})

页面级上下文适合放:

  • 当前患者、当前就诊、当前文书。
  • 当前页面查询条件和选中对象。
  • 页面级 controller。
  • 页面内可复用但不应外泄的缓存。
  • 页面命令判断需要读取的状态对象。

页面级注入的值可以被页面内部按钮、表格、右键菜单、弹窗打开源和命令执行上下文读取。页面关闭后,scope dispose 会释放当前页面创建的上下文;如果值是 controller、订阅或缓存,应使用 setDisposable() 或带 disposer 的 set()

读取上下文

上下文查找会从当前范围向父级查找。必需依赖使用 require(),可选依赖使用 get()

const currentRecordKey = createAppContextKey<{ id: string; title: string }>('record.current')const currentUserKey = createAppContextKey<{ id: string; name: string }>('session.user')const context = createAppContext([  [currentRecordKey, { id: 'r001', title: '入院记录' }],]) const record = context.require(currentRecordKey)const user = context.get(currentUserKey)

读取规则:

  • get(key):当前范围找不到时向父级找,找不到返回 undefined
  • getOwn(key):只读当前范围,不查父级。
  • require(key):找不到时抛错,适合页面必须依赖。
  • has(key):当前或父级存在即返回 true
  • hasOwn(key):只判断当前范围。

覆盖和继承

局部上下文可以覆盖父级同名 key。命令执行时会基于 source 从当前 render tree 向上收集上下文,局部值优先。

const currentRecordKey = createAppContextKey<{ id: string; title: string }>('record.current')const currentUserKey = createAppContextKey<{ id: string; name: string }>('session.user')const parent = createAppContext([  [currentUserKey, { id: 'u001', name: '张医生' }],]) const child = parent.derive([  [currentRecordKey, { id: 'r002', title: '病程记录' }],]) child.require(currentUserKey)child.require(currentRecordKey)

这让浮层、tab、局部组件可以继承全局登录态,同时追加自己的页面参数。

浮层上下文

由页面打开的 modal、popover、floating window 应继承调用来源的上下文,再追加本次浮层参数。这样即使页面浮动、tab 脱离原工作区,浮层仍然能读取正确页面级对象。

const overlayReasonKey = createAppContextKey<string>('overlay.reason') // 伪代码:真实打开方式以 AppOverlayService / workspace API 为准。const overlayValues = [  [overlayReasonKey, '从病历页面打开'],] as const

如果浮层需要页面上下文,应传入 source 或使用当前页面提供的上下文根,而不是默认从应用根查找。

浮动 tab 本质上是新的顶级窗口,但它仍然应该持有原 tab/page 的上下文根。这样页面浮出去后,页面内部命令、弹窗和右键菜单仍能读取同一份页面级上下文。

可释放对象

上下文可以管理可释放对象。页面级服务、订阅、定时器包装对象和 controller 应放在页面上下文并注册释放。

const controllerKey = createAppContextKey<{ dispose(): void }>('record.controller')const context = createAppContext() context.setDisposable(controllerKey, {  dispose() {    // 释放页面级资源  },}) context.dispose()

也可以用自定义 disposer:

const timerKey = createAppContextKey<number>('page.timer')const context = createAppContext()const timerId = window.setInterval(() => {}, 1000) context.set(timerKey, timerId, {  dispose: () => window.clearInterval(timerId),})

更推荐把一组页面资源集中放进 DisposableBag,再交给页面上下文托管:

const pageDisposablesKey = createAppContextKey<DisposableBag>('page.disposables')const context = createAppContext()const pageDisposables = new DisposableBag() pageDisposables.setInterval(() => {  reload()}, 30000) context.setDisposable(pageDisposablesKey, pageDisposables)

这样页面 context dispose 时会清理整组资源,包括定时器、事件监听、订阅和动画帧。不要把裸 setInterval() 分散写在按钮回调或服务回调里。

释放规则:

  • delete(key) 会释放当前 key 绑定的旧值。
  • set(key, value) 会先释放当前 key 的旧值。
  • clear() 会释放所有当前范围内的值。
  • dispose() 会标记上下文失效并清空当前范围。
  • 父级上下文不会因为子级 dispose 而释放。

和命令系统配合

命令的 visibleenabledcanExecuteexecute 都可以读取执行上下文里的 appContext

const currentRecordKey = createAppContextKey<{ id: string; title: string }>('record.current')const manager = new CommandManager() manager.register({  id: 'record.save',  title: '保存',  canExecute: context => Boolean(context.appContext.get(currentRecordKey)),  execute: context => {    const record = context.appContext.require(currentRecordKey)    record.title  },})

执行命令时应传入触发命令的 source,这样命令能解析到 source 所在页面的上下文。

调试方式

  • 使用 Layout Inspector 查看某个 render 节点附近的上下文。
  • 使用 Object Inspector 展开上下文值,确认 key 和 value 是否符合预期。
  • 关闭页面后用 Heap Snapshot 检查页面级上下文是否仍被引用。
  • 通过 revision 判断上下文是否发生过变更。

常见错误

把页面状态放到应用级上下文

这会导致 tab 间串数据,也会让页面关闭后对象仍被全局持有。当前患者、当前文书、当前查询结果应放页面级。

在函数里临时创建 key

createAppContextKey() 返回的是带 symbol 的 key。每次调用都是新 key。读取端和写入端必须复用同一个 key 实例。

弹窗没有继承调用页面上下文

弹窗如果从应用根打开,只能看到全局上下文。需要页面上下文时,应从触发按钮、页面根或 workspace 文档传递 source/contextValues。

dispose 后继续使用上下文

AppContextRegistry.dispose() 后继续 set/delete/derive 会抛错。异步回调返回时应先判断页面是否仍然有效。

相关文档