编写扩展
扩展可以提供普通 Go 组件、保留状态的原生组件,或异步原生服务。Editor、Markdown 和 Terminal 是可选的原生组件包,Updater 是可选服务。它们使用同一套公开注册接口;第三方扩展无需修改 quickgui-host。
#选择扩展类型
| 你要添加的内容 | 需要交付的内容 |
|---|---|
| 组件或应用逻辑 | 一个 Go 模块 |
| 原生服务或可选后端 | Go 包、清单,以及原生库 |
| 保留状态的原生界面 | 组件 SDK 库和 Go 绑定 |
使用者导入你的 Go 包。CLI 会找到清单并打包匹配的原生产物。
#创建扩展项目
quickgui init-extension my-components --type go
quickgui init-extension my-service --type zig
quickgui init-extension my-service --type rustgo 是默认值,会生成可复用组件。zig 和 rust 会生成带类型化 Go 包装、清单和 npm 产物包的原生服务。每个模板都包含 cmd/demo 和 quickgui.toml:
cd my-service
bun run dev用 --module github.com/acme/my-service 指定 Go 模块,用 --npm-package @acme/extension-my-service 指定产物包。Zig 模板需要 Zig 0.16.x。在本仓库中,在 CLI 发布前运行 bun packages/cli/src/cli.ts init-extension。
#共享组件
从你自己的模块导出组件函数。这个包不需要原生清单。
package notice
import "github.com/egoist/quickgui/go/ui"
func Notice(message string, styles ...ui.StyleBuilder) *ui.Element {
return ui.Text(
message,
).
Padding(12).
BackgroundColor("#eff6ff").
TextColor("#1e40af").
BorderRadius(8).
Style(ui.Style().
Merge(styles...))
}用 Go 版本标签发布。使用者通过 go get 添加。导入 terminal 或 updater 仍会让应用选择这些原生包,因此把可选集成放在单独的包中。
#组织原生扩展
extensions/ 中的每个内置扩展同时是 Go 模块和 npm 包。Rust 放在 src/,TypeScript 绑定放在 js/。自己的组合包也可以采用这一结构:
Cargo.toml
go.mod
badge.go
package.json
quickgui.extension.json
src/
lib.rs
js/
index.ts
lib/
darwin-arm64/libquickgui_acme_badge.dylibC、C++、Rust 以及其他支持 C ABI 的语言都可以实现扩展。Rust 组件库依赖 quickgui-extension-sdk,不依赖渲染器或 quickgui-host。实现 Component 和 ComponentFactory,由 ComponentRuntime 提供生命周期函数表。扩展自行定义包名、组件名、属性和发布版本。
#声明依赖
把这份清单放在被导入的 Go 包源码旁边:
{
"schema": 1,
"name": "acme-echo",
"abi": 1,
"package": "@acme/extension-echo",
"version": "1.0.0",
"library": "quickgui_acme_echo"
}名称以小写字母开头,最多包含 64 个小写字母、数字或连字符。host 是保留名。library 字段是 quickgui_ 加上把连字符替换为下划线后的名称。
Go 包记录该要求并暴露类型化调用:
package echo
import (
"encoding/json"
"github.com/egoist/quickgui/go/host"
"github.com/egoist/quickgui/go/native"
)
func init() {
host.RequireExtension("acme-echo", "1.0.0")
}
func Send(message string, done func(string, error)) {
native.InvokeExtension("acme-echo", "echo", message, func(raw string, err error) {
var reply string
if err == nil {
err = json.Unmarshal([]byte(raw), &reply)
}
done(reply, err)
})
}在 native.Run 就绪之后再调用操作。持久服务用 native.OpenExtension(name, options, changed, ready) 返回会话。所有者销毁时,在 UI 协程上关闭它。
CLI 通过 go list -deps 发现清单。清单和 RequireExtension 都需要:一个控制打包,另一个控制加载。
#实现原生接口
以 C 链接导出 quickgui_extension_v1。描述符包含扩展名称、独立版本、ABI 版本 1 和通用函数表。组件表包含 create、update、render、event 和 destroy;服务表包含 invoke 和 shutdown。
组件保留不透明实例并返回 SDK 的基础节点树。稳定的节点键保留焦点、选择和滚动状态;虚拟列表只请求可见行及预加载行。输入返回所属扩展,扩展发出的事件传给 Go 绑定。一个实例的调用在原生 UI 线程串行执行。耗时任务放到工作线程,数据变化时只唤醒该实例。属性相同时返回未变化,销毁时释放状态和唤醒句柄。
Go 包装可以挂载任意已注册组件:
package badge
import (
"github.com/egoist/quickgui/go/host"
"github.com/egoist/quickgui/go/ui"
)
func init() { host.RequireExtension("acme-badge", "1.0.0") }
func View(label string) *ui.Element {
return ui.ExtensionComponent("acme-badge", "badge", map[string]any{
"label": label,
}, ui.Props{})
}独立编译的 C 计数器 包含完整的组件描述符和生命周期实现。组件帧限制为 16 MiB,前端属性 JSON 限制为 12 MiB。
对于服务,保留借用的请求字节前先复制,尽快返回,稍后回复。
| 请求 | 扩展要做的事 |
|---|---|
像 echo 这样的一次性方法 | 回复一次 |
start | 打开会话并保留其事件接收器 |
像 send 这样的命令 | 找到该会话并回复此命令 |
stop | 取消该会话 |
shutdown | 取消所有会话且不阻塞 |
请求和回复载荷各自限制为 64 KiB。C echo 实现 是完整的一次性示例。
#自动注册
应用在启动时加载每个选定的库,并检查名称、版本和 ABI。同一名称的冲突版本会失败。镜像在进程生命周期内保持加载;你的 shutdown 函数必须取消剩余会话。
#构建与打包产物
为每个支持的宿主构建原生库,并放到 lib/<platform>-<arch>/ 下。把该文件夹作为 npm 包发布,其 version 与清单一致。
{
"name": "@acme/extension-echo",
"version": "1.0.0",
"files": ["js", "lib", "quickgui.extension.json", "README.md"],
"exports": { ".": "./js/index.ts", "./package.json": "./package.json" }
}npm 包包含 TypeScript 绑定时,导出 js/index.ts。把 @quickgui/* 运行时包和 Solid 声明为 peer dependencies,开发副本放在 devDependencies,不要放进运行时 dependencies。扩展 README 只保留构建说明,用户指南放在网站上。
辅助文件放在库旁边。在清单的 resources 对象中按 darwin、linux 或 windows 声明它们的基本名称。
#测试与分发
使用者 go get 你的模块并运行 quickgui dev。CLI 会获取匹配的 npm 包;开发时也可以把 QUICKGUI_EXTENSION_DIR 设为本地产物目录。
确认未导入的应用不会打包该扩展,添加导入后会包含它,关闭会话会停止事件。最终用户拿到的是打包后的应用;运行不需要 Bun 或原生工具链。
