From b57a317c823460fcbac867a7df3522b56ce07720 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Thu, 8 Jan 2026 13:56:53 +0800 Subject: [PATCH] feat(docs): add DOMWidget development guide for vanilla JavaScript --- docs/dom_widget_dev_guide.md | 188 +++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 docs/dom_widget_dev_guide.md diff --git a/docs/dom_widget_dev_guide.md b/docs/dom_widget_dev_guide.md new file mode 100644 index 00000000..aab6c5b0 --- /dev/null +++ b/docs/dom_widget_dev_guide.md @@ -0,0 +1,188 @@ +# ComfyUI Custom DOMWidget 开发说明文档 (Vanilla JavaScript) + +本文档旨在说明如何使用 Vanilla JavaScript (纯 JS) 在 ComfyUI 前端中实现自定义 `DOMWidget`。`DOMWidget` 允许你将标准的 HTML 元素(如 `div`, `video`, `canvas`, `input` 等)嵌入到 ComfyUI 的节点中,并享受前端自动布局和缩放管理。 + +--- + +## 1. 核心概念 + +在 ComfyUI 中,`DOMWidget` 是对 LiteGraph 默认 Canvas 渲染逻辑的扩展。它在 Canvas 之上维护一个 HTML 层,使得复杂的交互和媒体显示变得非常容易。 + +### 核心 API +* `app.registerExtension`: 注册扩展的入口。 +* `getCustomWidgets`: 定义新部件类型的钩子。 +* `node.addDOMWidget(name, type, element, options)`: 将 HTML 元素添加到节点的关键方法。 + +--- + +## 2. 基础结构 + +一个标准的自定义 `DOMWidget` 扩展通常遵循以下结构: + +```javascript +import { app } from "../../scripts/app.js"; + +app.registerExtension({ + name: "My.Custom.Extension", + async getCustomWidgets() { + return { + // 定义一个名为 "MY_WIDGET_TYPE" 的新部件类型 + MY_WIDGET_TYPE(node, inputName, inputData, app) { + // 1. 创建 HTML 元素 + const container = document.createElement("div"); + container.innerHTML = "Hello DOMWidget!"; + + // 2. 样式设置 (可选) + container.style.color = "white"; + container.style.padding = "5px"; + + // 3. 调用 addDOMWidget 并返回结果 + const widget = node.addDOMWidget(inputName, "MY_WIDGET_TYPE", container, { + // 配置选项 + getValue() { + return container.innerText; + }, + setValue(v) { + container.innerText = v; + } + }); + + // 4. 固定返回格式 + return { widget }; + } + }; + } +}); +``` + +--- + +## 3. `addDOMWidget` 详细参数 + +```javascript +node.addDOMWidget(name, type, element, options) +``` + +### 参数说明: +1. **`name`**: 部件的内部名称(通常匹配输入名称)。 +2. **`type`**: 部件的类型标识符。 +3. **`element`**: 实际的 HTML 元素。 +4. **`options`**: (Object) 包含生命周期和行为的配置项。 + +### `options` 常用字段: +| 字段 | 类型 | 说明 | +| :--- | :--- | :--- | +| `getValue` | `Function` | 定义部件序列化时的值来源。 | +| `setValue` | `Function` | 定义如何从工作流数据中恢复部件状态。 | +| `getMinHeight` | `Function` | 返回部件的最小像素高度。 | +| `getHeight` | `Function` | 返回部件的期望高度(可返回百分比字符串,如 `"50%"`)。 | +| `hideOnZoom`| `Boolean` | 当 Canvas 缩小到一定程度时是否隐藏 DOM 元素(提高性能,默认 `true`)。 | +| `onDraw` | `Function` | 每一帧绘制时触发,可用于在 Canvas 上做额外标注。 | +| `afterResize` | `Function` | 节点缩放后的回调。 | + +--- + +## 4. 样式与布局管理 + +ComfyUI 前端使用 CSS 变量来协调 Canvas 节点与 DOM 部件的尺寸。你可以在元素的 `style` 或 CSS 中设置这些变量: + +```javascript +container.style.setProperty('--comfy-widget-min-height', '100px'); +container.style.setProperty('--comfy-widget-max-height', '500px'); +``` + +由于 `DOMWidget` 被放置在绝对定位的容器中,建议容器元素的样式设为: +```javascript +container.style.width = "100%"; +container.style.boxSizing = "border-box"; +``` + +--- + +## 5. 生命周期与交互 + +如果你需要访问其他部件或在特定时刻触发逻辑,可以结合 `nodeCreated` 钩子: + +```javascript +app.registerExtension({ + name: "My.Lifecycle.Extension", + nodeCreated(node) { + if (node.comfyClass === "MyCustomNode") { + // 查找刚才创建好的 DOMWidget + const myWidget = node.widgets.find(w => w.name === "my_input"); + + // 可以在这里绑定事件 + myWidget.element.addEventListener("click", () => { + console.log("Widget clicked!", myWidget.value); + }); + } + } +}); +``` + +--- + +## 6. 完整实战示例: 简易文本预览器 + +这个示例实现了一个动态显示文本字数统计的预览器部件。 + +```javascript +import { app } from "../../scripts/app.js"; + +app.registerExtension({ + name: "Comfy.TextCounter", + getCustomWidgets() { + return { + TEXT_COUNTER(node, inputName) { + const el = document.createElement("div"); + el.style.background = "#222"; + el.style.border = "1px solid #444"; + el.style.padding = "8px"; + el.style.borderRadius = "4px"; + el.style.fontSize = "12px"; + + const label = document.createElement("span"); + label.innerText = "Characters: 0"; + el.appendChild(label); + + const widget = node.addDOMWidget(inputName, "TEXT_COUNTER", el, { + getValue() { return node.widgets[0]?.value || ""; }, + setValue(v) { /* 逻辑通常由上游触发 */ }, + getMinHeight() { return 40; } + }); + + // 设置一个自定义更新逻辑 + widget.updateCount = (text) => { + label.innerText = `Characters: ${text.length}`; + }; + + return { widget }; + } + }; + }, + nodeCreated(node) { + if (node.comfyClass === "MyTextNode") { + const counterWidget = node.widgets.find(w => w.type === "TEXT_COUNTER"); + const textWidget = node.widgets.find(w => w.name === "text"); + + if (counterWidget && textWidget) { + // 监听文本部件的回调 + const oldCallback = textWidget.callback; + textWidget.callback = function(v) { + if (oldCallback) oldCallback.apply(this, arguments); + counterWidget.updateCount(v); + }; + } + } + } +}); +``` + +--- + +## 7. 注意事项 + +1. **路径引用**: 引用 `app` 时请根据你的插件目录深度调整路径,通常是 `../../scripts/app.js`。 +2. **清理**: 如果你创建了外部引用(如 `setInterval` 或全局监听),请确在 `node.onRemoved` 中进行清理。 +3. **安全性**: 如果动态设置 `innerHTML`,请确保内容来源可靠,防止 XSS 攻击。 +4. **性能**: 对于高频更新的 DOM 元素,注意不要触发过多的重排(Reflow)。