Files
ComfyUI-Lora-Manager/docs/dom_widget_dev_guide.md

189 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 <b>DOMWidget</b>!";
// 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