DevTools 开发工具
import { RenderStackPanel, RenderText, type AppHost } from 'ds-ui'
DevTools 是 DirectSurface UI 的开发工具窗口。它把 Layout Inspector、Runtime Diagnostics 和业务侧自定义诊断页统一放在一个可停靠窗口里,适合排查复杂界面的布局、命中、刷新请求、网络请求和现场问题。
DevTools 由 AppOverlayService 管理。默认内置两个 tab:
| Tab | key | 用途 |
|---|---|---|
| Layout | layout-inspector |
查看 render tree、hit path、节点状态、app context 和选中对象。 |
| Runtime | runtime |
查看 paint/layout request、热点聚合、调用栈和诊断快照。 |
业务应用可以注册额外 tab,例如 Network、Logs、State、Feature Flags。框架只负责窗口、停靠、tab 容器和生命周期,不关心业务数据来源。
打开方式
应用入口应保留 Application.mount(...).run(...) 返回的 AppHost,再从 host 获取同一应用实例的服务:
function openDevTools(host: AppHost): void { const uiServices = host.uiServices uiServices.showDevTools() uiServices.showDevTools({ activeTab: 'runtime' }) uiServices.toggleDevTools({ activeTab: 'layout-inspector' }) uiServices.activateDevToolsTab('runtime')}
应用也可以把入口挂到菜单、工具栏或快捷键。Layout API 和 DevTools 的 Layout tab 始终绑定到当前 runtime;传入 enableLayoutInspector: true 时,Application 还会注册 Ctrl/Cmd+Shift+I 开发快捷键。该选项默认是 false,业务基座也可以提供自己的菜单入口。
注册自定义 tab
自定义 tab 使用 registerDevToolsTab() 注册,返回值是注销函数:
const unregister = uiServices.registerDevToolsTab({ key: 'network', label: 'Network', icon: 'server', createContent: () => new RequestLogPanel({ store: requestLogStore }),}) // 模块或 shell 销毁时调用unregister()
RequestLogPanel 和 requestLogStore 是业务自定义示例,不属于 ds-ui。自定义内容只要返回一个公开的 RenderObject 即可。
如果需要一次性替换全部扩展 tab,可以使用 setDevToolsTabs():
uiServices.setDevToolsTabs([ { key: 'network', label: 'Network', icon: 'server', createContent: () => new RequestLogPanel({ store: requestLogStore }), }, { key: 'logs', label: 'Logs', icon: 'file-text', createContent: () => new RenderText('Logs'), },])
key 必须唯一,且不能使用内置 key:layout-inspector、runtime。
内容生命周期
DevTools 会拥有 tab 内容的生命周期:
createContent()在创建 DevTools 窗口时调用。- DevTools 已打开时调用
setDevToolsTabs(),会重新创建扩展 tab 内容。 - 被移除的扩展 tab 内容会被自动
dispose()。 - 关闭 DevTools 时,内置 tab 和所有扩展 tab 内容都会被
dispose()。 - 再次打开 DevTools 会重新调用
createContent()。
因此,扩展 tab 不应复用同一个 RenderBox 实例:
// 推荐:每次打开创建新的内容实例createContent: () => new RequestLogPanel({ store }) // 不推荐:多个窗口或多次打开复用同一个实例const panel = new RequestLogPanel({ store })createContent: () => panel
如果 tab 内容订阅了 store、事件总线或定时器,应在自身 dispose() 中释放:
class RequestLogPanel extends RenderStackPanel { private readonly _unsubscribe: () => void constructor(options: { store: RequestLogStore }) { super({ orientation: 'vertical' }) this._unsubscribe = options.store.subscribe(() => this.refresh()) } refresh(): void { // 从 store 读取最新诊断数据并刷新子组件。 } override dispose(): void { this._unsubscribe() super.dispose() }}
和 Dock Window 的关系
DevTools 默认作为开发者停靠窗口打开,不属于当前业务页面内容。它和主窗口并排显示,避免被业务 AppOverlayHost、弹窗或页面生命周期遮挡。
用户可以在 DevTools 标题栏切换停靠方向或浮动显示。停靠后,和主窗口相邻的边缘可以拖动调整尺寸。
兼容旧入口
旧的 Layout Inspector 和 Runtime Diagnostics 入口仍然可用:
uiServices.showLayoutInspectorPanel()uiServices.showRuntimeDiagnosticsPanel()uiServices.toggleRuntimeDiagnosticsPanel()
这些方法现在会打开 DevTools 并切换到对应 tab。为了兼容旧测试和旧代码:
showLayoutInspectorPanel().debugState()返回 Layout Inspector 的 debug state。showRuntimeDiagnosticsPanel().debugState()返回 Runtime Diagnostics 的 debug state。
新代码建议直接使用 DevTools API:
const window = uiServices.showDevTools({ activeTab: 'runtime' })window.debugDevToolsState()window.debugLayoutInspectorState()window.debugRuntimeDiagnosticsState()
常见接入模式
网络请求
网络请求 tab 通常由应用基座提供:
- HTTP client 统一记录请求开始、结束、耗时、状态和错误。
- 请求记录写入应用级 request log store。
- Network panel 订阅 store 并展示列表和详情。
- Shell 通过
registerDevToolsTab()把 Network panel 挂到 DevTools。
框架不直接采集业务请求,也不要求应用使用某个 HTTP 实现。
日志和状态
Logs、State 等 tab 也建议由应用基座注册,而不是由每个页面单独注册。这样 DevTools 的生命周期稳定,页面切换不会丢失诊断上下文。
页面级诊断可以写入共享 store,再由基座 tab 统一展示。
使用建议
- DevTools 只服务开发、测试和现场排障,不替代业务审计。
- 扩展 tab 的
key使用稳定英文标识,避免和内置 tab 冲突。 - tab 内容不要再放自己的关闭按钮,关闭和停靠由 DevTools 窗口负责。
- 诊断数据要脱敏,不应展示 token、患者隐私、完整病历正文或大体积业务对象。
- 大列表和大对象应分页、虚拟化或摘要展示,避免诊断工具本身造成卡顿。