App Context API 参考
本文示例所需的公共入口如下;各代码块可以按实际用量缩减:
import { APP_CONTEXT_PROVIDER, AppContextRegistry, Application, CommandManager, DisposableBag, RenderAppContextScope, RenderObject, RenderStackPanel, RenderWindow, createAppContext, createAppContextKey, type AppContextProvider,} from 'ds-ui'
App Context API 用于在应用、页面、弹窗、局部组件和命令之间传递范围依赖。它不是全局 store 的替代品,而是一个按 render tree 和生命周期查找对象的机制。
典型用途:
- 应用级:登录用户、租户、权限服务、字典服务、应用配置。
- 页面级:当前患者、当前文书、页面参数、页面 controller。
- 弹窗级:打开参数、临时草稿、弹窗内服务。
- 组件级:局部编辑器 controller、局部缓存、局部命令依赖。
导出清单
核心导出:
AppContextKeyAppContextRegistryAppContextLookupKeyAppContextInitialValuesAppContextSetOptionsAppContextProviderAPP_CONTEXT_PROVIDERcreateAppContextcreateAppContextKeyisAppContextProviderRenderAppContextScopeRenderAppContextScopeOptions
相关能力:
RenderObject.getAppContext(key)RenderObject.requireAppContext(key)PipelineOwner.appContextApplicationRunOptions.contextAppHost.context
AppContextKey
AppContextKey<T> 是类型安全的上下文 key。
const appContextRecordKey = createAppContextKey<{ id: string; title: string }>('record.current')
| 成员 | 类型 | 说明 |
|---|---|---|
description |
string |
调试用描述。应稳定、可读、能区分业务域。 |
id |
symbol |
内部唯一标识。每次创建 key 都是新实例。 |
同一个上下文值的写入端和读取端必须复用同一个 key 实例。不要在函数内部临时创建 key 再读取,否则读不到之前写入的值。
AppContextLookupKey
AppContextLookupKey<T = unknown> 支持两种形式:
| 类型 | 适用场景 | 类型安全 |
|---|---|---|
AppContextKey<T> |
框架和业务长期使用的依赖。 | 有 |
string |
临时调试、简单 demo、兼容旧代码。 | 无 |
正式业务优先使用 createAppContextKey<T>()。字符串 key 容易命名冲突,也无法在读取时获得类型提示。
创建上下文
const appContext = createAppContext()const childContext = appContext.derive()
createAppContext(initial?):
| 参数 | 类型 | 说明 |
|---|---|---|
initial |
AppContextInitialValues |
初始值。传入已有 AppContextRegistry 时直接返回该实例。 |
AppContextInitialValues 支持:
| 形式 | 示例 | 说明 |
|---|---|---|
AppContextRegistry |
createAppContext(existing) |
直接复用已有 registry。 |
Record<string, unknown> |
createAppContext({ tenant: 'demo' }) |
只能产生字符串 key。 |
Iterable<[AppContextLookupKey, unknown]> |
createAppContext([[userKey, user]]) |
推荐方式,支持 typed key。 |
AppContextRegistry
AppContextRegistry 是实际的 key-value 注册表,支持父级链、读取、删除、派生和释放。
构造函数:
const appContextRecordKeyForRegistry = createAppContextKey<{ id: string; title: string }>('record.current')const appContextRoot = new AppContextRegistry(undefined, [['tenant', 'demo']])const appContextPage = new AppContextRegistry(appContextRoot, [ [appContextRecordKeyForRegistry, { id: 'r001', title: '入院记录' }],])
| 参数 | 类型 | 说明 |
|---|---|---|
parent |
AppContextRegistry | undefined |
父上下文。get() / require() 找不到 own 值时会向父级查找。 |
initial |
Record<string, unknown> | Iterable<[AppContextLookupKey, unknown]> |
初始值。 |
属性:
| 属性 | 类型 | 说明 |
|---|---|---|
disposed |
boolean |
是否已释放。 |
parent |
AppContextRegistry | undefined |
父上下文。 |
revision |
number |
当前 registry 自身版本。set/delete/clear 会递增。 |
方法:
| 方法 | 返回值 | 说明 |
|---|---|---|
set(key, value, options?) |
this |
写入当前 registry。会先释放同 key 的旧值。 |
setDisposable(key, value) |
this |
等价于 set(key, value, { dispose: true }),适合有 dispose() 的对象。 |
get(key) |
T | undefined |
从当前 registry 开始向父级查找。字符串 key 返回 unknown。 |
getOwn(key) |
T | undefined |
只读取当前 registry,不查父级。 |
require(key) |
T |
必需读取。当前和父级都找不到时抛错。 |
has(key) |
boolean |
当前或父级存在即返回 true。 |
hasOwn(key) |
boolean |
只判断当前 registry。 |
keys() |
IterableIterator<AppContextLookupKey> |
当前 registry 自己持有的 key。 |
delete(key) |
boolean |
删除当前 registry 的 key,并释放该值。 |
clear() |
void |
删除并释放当前 registry 的全部 own 值。 |
derive(initial?) |
AppContextRegistry |
创建以当前 registry 为 parent 的子上下文。 |
dispose() |
void |
标记 disposed,并清空当前 registry。不会释放父级。 |
dispose() 后继续 set()、delete()、derive() 会抛错。get()、getOwn()、has() 仍是普通读取行为,但业务不应在页面关闭后继续使用旧 context。
set 释放规则
AppContextSetOptions:
| 字段 | 类型 | 说明 |
|---|---|---|
dispose |
boolean | DisposeFn |
释放策略。 |
释放行为:
dispose不传或为false:只保存值,不自动释放。dispose: true:如果值有dispose()方法,删除、覆盖或 clear 时调用它。dispose: () => void:删除、覆盖或 clear 时调用自定义释放函数。set()写入同 key 新值前,会先释放旧值。delete()只删除当前 registry 的 own 值,不影响父级同 key 值。clear()只释放当前 registry own 值,不释放父级。
const controllerKey = createAppContextKey<{ dispose(): void }>('record.controller')const timerKey = createAppContextKey<number>('page.timer')const contextWithDisposables = createAppContext() contextWithDisposables.setDisposable(controllerKey, { dispose() { // close page controller },}) const timerId = window.setInterval(() => undefined, 1000)contextWithDisposables.set(timerKey, timerId, { dispose: () => window.clearInterval(timerId),}) contextWithDisposables.dispose()
多个定时器、订阅和事件监听应先集中到 DisposableBag,再放入 context:
const pageBagKey = createAppContextKey<DisposableBag>('page.disposables')const pageContextWithBag = createAppContext()const pageBag = new DisposableBag() pageBag.setInterval(() => { reload()}, 30000) pageContextWithBag.setDisposable(pageBagKey, pageBag)pageContextWithBag.dispose()
读取规则
直接读 registry:
const appContextUserKey = createAppContextKey<{ id: string; name: string }>('session.user')const appContextRoot = createAppContext([[appContextUserKey, { id: 'u001', name: '张医生' }]])const appContextPage = appContextRoot.derive() const user = appContextPage.require(appContextUserKey)user.name
从 render 对象读:
const appContextRenderUserKey = createAppContextKey<{ id: string; name: string }>('session.user')const appContextConsumerRender = undefined as unknown as RenderObject const renderUser = appContextConsumerRender.requireAppContext(appContextRenderUserKey)renderUser.name
RenderObject.getAppContext(key) 会从当前 render 节点开始向父级查找 AppContextProvider,然后再查 owner.appContext。越靠近当前节点的值优先。
RenderObject.requireAppContext(key) 和 registry 的 require() 类似,但能正确区分“key 存在且值为 undefined”与“key 不存在”。
AppContextProvider
任何对象只要实现 [APP_CONTEXT_PROVIDER](),就可以作为上下文提供者。
const providerContext = createAppContext({ tenant: 'demo' }) const providerExample: AppContextProvider = { [APP_CONTEXT_PROVIDER](): AppContextRegistry { return providerContext },}
通常业务不需要自己实现 provider,优先使用 RenderAppContextScope、RenderWindow.setAppContext()、RenderModal.setAppContext() 或 workspace/tab 的 context 配置。
RenderAppContextScope
RenderAppContextScope 把上下文挂到 render tree 的某个范围。
构造参数:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
context |
AppContextRegistry |
新建 registry | 外部已有上下文。 |
values |
Record<string, unknown> | Iterable<[AppContextLookupKey, unknown]> |
undefined |
追加到当前 scope 的初始值。 |
child |
RenderBox |
undefined |
被包裹的子树根。 |
disposeContextOnDispose |
boolean |
未传 context 时为 true |
当前 render 释放时是否释放 appContext。 |
属性和方法:
| 成员 | 类型 / 返回值 | 说明 |
|---|---|---|
appContext |
AppContextRegistry |
当前 scope 提供的上下文。 |
child |
RenderBox | undefined |
子节点。 |
setChild(child?) |
void |
替换子节点。 |
dispose() |
void |
按 disposeContextOnDispose 决定是否释放上下文。 |
const appContextScopeRecordKey = createAppContextKey<{ id: string; title: string }>('record.current')const pageContextScope = new RenderAppContextScope({ values: [ [appContextScopeRecordKey, { id: 'r001', title: '入院记录' }], ], child: new RenderStackPanel({ spacing: 8 }),})
如果同时传入 context 和 values,values 会写入这个外部 context。此时默认不会在 scope dispose 时释放外部 context,除非显式设置 disposeContextOnDispose: true。
应用入口上下文
应用启动可注入根上下文:
const appContextHostUserKey = createAppContextKey<{ id: string; name: string }>('session.user')const host = Application.mount('#app').run(new RenderWindow({ title: 'CIS' }), { context: [ [appContextHostUserKey, { id: 'u001', name: '张医生' }], ],}) host.context.require(appContextHostUserKey).id
ApplicationRunOptions.context 会成为 PipelineOwner.appContext。所有 render 节点在本地 provider 链找不到值时,会回退到这个根上下文。
宿主框架销毁应用时应调用 host.dispose()。根上下文跟随 host 释放,适合放应用生命周期对象。
弹窗和窗口上下文
RenderWindow、RenderModal、RenderPromptModal 都实现了 app context provider,并提供 setAppContext(context, { disposeOnDispose? })。
Overlay 服务的弹窗 API 通常支持:
source:从触发源收集页面上下文。context:传入外部上下文。contextValues:追加本次弹窗局部值。
推荐规则:
- 弹窗需要当前页面对象时,传入打开来源
source。 - 弹窗临时参数使用
contextValues。 - 弹窗自己创建且需要随弹窗释放的 context,设置
disposeOnDispose。 - 外部复用的 context 不应由弹窗释放。
命令上下文快照
CommandManager 执行和求值时会根据 source 创建 appContext 快照。快照收集顺序是从 source 向父级,再到应用根;越近的 key 越优先。
const appContextCommandRecordKey = createAppContextKey<{ id: string }>('record.current')const appContextCommandManager = new CommandManager() appContextCommandManager.register({ id: 'record.save', title: '保存', canExecute: context => Boolean(context.appContext.get(appContextCommandRecordKey)), execute: context => { const record = context.appContext.require(appContextCommandRecordKey) record.id },})
命令执行时尽量传入触发源,否则只能解析 root scope 和应用级上下文。
调试与诊断
- Layout Inspector 会显示选中 render 节点可见的 app context。
- Runtime Diagnostics 快照会包含根上下文摘要。
- ObjectInspector 可以展开上下文值。
revision可用于诊断当前 registry 是否发生变更。
上下文调试只应展示对象摘要或脱敏值。不要把敏感业务数据无控制地暴露给生产环境诊断入口。
使用边界
- App Context 解决依赖可见性和生命周期,不负责业务状态同步。
- 页面私有对象不要放应用根,避免 tab 串数据和页面关闭后仍被引用。
- 大文档、大图片、大数组不要无节制放入全局上下文。
- key 定义在稳定模块,读取端和写入端复用同一个 key 实例。
- 页面、tab、弹窗、组件创建的 controller 应使用
setDisposable()或自定义 disposer。 get()可能返回undefined,必需依赖用require()并让错误尽早暴露。