# 设置页插槽契约(DSH 0.1.5-rc.1) 本文记录 `dsh-plugin-desk` 浏览器半接入官方「设置 → 插件」页时踩到的契约要求,以及最终跑通的写法。所有结论都基于 DSH 源码 + 本机运行环境的实测。 ## 一句话结论 **注册值必须是「函数组件」,返回 ReactNode。** 返回 DOM 节点或 `{ render() }` 对象都会被 React 拒绝,并把整个设置 section 打挂(页面变成空的 `
`)。 ## 组件形态 源码里的权威定义(`packages/client/ui-slots/src/index.ts`): ```ts /** Registration-position component shape: the bare call signature. */ export type SlotComponent

= (props: P) => ReactNode ``` 渲染器用 `jsx`/`createElement` 实例化它(`packages/client/ui-renderer/src/.../renderEntry`): ```js return jsx(Comp, { ...kit, ...injected, ...slotInjected.props, ...contextual, ...ownerProps }) ``` 所以合法形态只有一种——**函数组件**。 ## React 从哪来 DSH 的 web shell 会把一组平台模块注入「冻结模块表」,client bundle 里 `require()` 拿到的就是这张表(`packages/client/web/src/seed.ts`): ```ts export function getStaticModules(): Record { return { 'react': React, 'react/jsx-runtime': ReactJsxRuntime, 'react-dom': ReactDom, 'react-dom/client': ReactDomClient, '@deepseek-ai/cordis': Cordis, '@deepseek-ai/dsh-client-store': ClientStore, '@deepseek-ai/dsh-client-ui-slots': UiSlots, '@deepseek-ai/dsh-client-ui-primitives': UiPrimitives, '@deepseek-ai/dsh-client-ui-dockkit': UiDockkit, } } ``` 因此 **bundle 里不需要 import 语句**,直接 `require('react')` 即可;`require` 的查找顺序是:种子表 → 已物化模块 → 已注册工厂,未命中就抛错(`packages/client/modules/src/client/system.ts`)。 **关键纪律:`require` 必须在模块作用域解析一次,绝不能放在组件函数体内。** 组件体内抛错会在渲染期炸掉整个 section——这正是排查过程中最耗时的一个假象。 ## 页面本体怎么画 两条路都可行: 1. **纯 React**:组件返回 `react.createElement(...)` 树。 2. **命令式 DOM + 容器**:组件返回一个容器元素,用回调 ref 在 React 提交后把已构建好的 DOM 挂进去。本插件用这条,因为它复用了宿主半已经写好的 `buildPage()`。 ```js function PluginManagerTab() { if (react === null) return null return react.createElement('div', { className: 'dpm-root', ref: mountOnce }) } function mountOnce(node) { if (node === null || node.dataset.dpmMounted === '1') return node.dataset.dpmMounted = '1' node.append(buildPage()) } ``` 回调 ref 在 React **提交之后**才被调用,此时节点已有父节点,可以安全挂载。(曾尝试在组件体内用 `queueMicrotask` 自己做挂载,失败:微任务早于提交执行,拿不到已提交的节点。) ## 注册 ```js ctx.effect(() => ctx.slots.inject('settings.plugins.tab', () => ctx.slots.register({ name: 'settings.plugins.tab', id: 'plugin-manager', order: 20, label: () => '插件管家' }, PluginManagerTab)), 'plugin-manager: settings tab') ``` - 用 `ctx.slots.inject(name, factory)` 等待目标插槽被声明;直接 `slots.register` 到未声明的插槽会报错。 - 组件作为 `register` 的**第二个参数**传入。 - 官方同一插槽的既有标签是 `插件配置`(id `configurable`)与 `插件列表`(id `all`);`order: 20` 排在其后。 ## 实测对照(本机浏览器,逐步读取真实 DOM) | 注册形态 | 结果 | | --- | --- | | 返回 DOM 节点 | `data-slot-error="settings.section"`,面板空白 | | 返回 `{ render() }` 对象 | 同上 | | 组件体内 `require('react')` | 同上(渲染期抛错) | | **函数组件 + `react.createElement` + 回调 ref** | **标签出现,5 张卡片正常渲染,`data-slot-error` 为空** | 最终实测输出: ``` tabs = ["插件配置", "插件列表", "插件管家"] errNodes = [] cardCount = 5 countText = "5 个插件 · 5 个有说明 · 已过滤 154 个非插件依赖" 点击「停用」 → ✅ 停用成功 · fiber 已 dispose · 已记住该选择;徽章变「已停用」,按钮变「启用」 点击「启用」 → ✅ 启用成功 · fiber 重新装配完成;徽章回「运行中」,按钮回「停用」 ``` ## 社区插件里的反例 `dsh-super-injector` 的 client 半用的是 `component: () => ({ render() {...} })`。按上述契约,这种形态不会被 React 正确实例化——它的设置页在同一个宿主上同样是空白的。 ## 复现 1. 安装本插件并重启 `dsh web`。 2. 强制刷新页面(`Ctrl+Shift+R`),打开 设置 → 插件。 3. 观察 `[...document.querySelectorAll('[data-slot-error]')]` 应为空,且存在「插件管家」标签。