跳到正文

编写扩展

扩展可以提供普通 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 rust

go 是默认值,会生成可复用组件。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.dylib

C、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 或原生工具链。