插件开发指南(基于 ESBuild + TSX)
本指南面向首次开发 Foxel 前端插件 App 的开发者,说明宿主提供的能力、插件对象规范、生命周期、清单上报机制,以及使用 TypeScript + React + esbuild 从 TSX 构建成自包含 JS 的方案。
快速检查清单
- 已实现
window.FoxelRegister(plugin)
,仅导出一个plugin
对象 mount(container, ctx)
内渲染,必要时实现unmount
- 文件读取统一使用
ctx.urls.downloadUrl
- 仅操作
container
,样式使用唯一前缀/ID - 设置合适的
supportedExts
、name
、icon
、version
- 产物为 IIFE 单文件:
dist/plugin.js
工作原理与数据流
- 加载方式:宿主以
<script src="...">
注入远程插件 JS。插件脚本执行后必须调用window.FoxelRegister(plugin)
注册自身。 - 生命周期:
mount(container, ctx)
:宿主传入一个空容器节点与上下文ctx
;插件在该节点内渲染自己的 UI。unmount(container)?
:可选,宿主在卸载时调用,用于清理副作用(移除事件、销毁 React root 等)。
- 应用选择:宿主根据插件的
supportedExts
与当前文件扩展名匹配决定是否显示为可用应用。未上报或为空数组时默认允许(便于首次运行)。 - 清单上报:首次安装后,宿主会在成功加载插件并拿到
plugin
对象后,将其元数据上报至后端(非阻塞)。
读取文件
插件读取文件内容统一使用 ctx.urls.downloadUrl
,无需自行调用后端 API。
DOM 范围
仅操作传入的 container
节点。不要修改宿主页面其他 DOM 或全局样式;如需注入样式,请使用唯一前缀/ID 以避免污染。
宿主接口(Host API)
- 全局注册函数:
window.FoxelRegister?: (plugin: RegisteredPlugin) => void
- 插件对象
RegisteredPlugin
字段(均为可选,除mount
外):mount(container: HTMLElement, ctx: PluginMountCtx): void | Promise<void>
unmount?(container: HTMLElement): void | Promise<void>
key?: string
:唯一键,建议稳定可读(如org.demo.image-viewer
)。name?: string
:展示名称。version?: string
:版本号(如1.0.0
)。supportedExts?: string[]
:支持的文件扩展名(小写、无点,如['jpg','png']
)。defaultBounds?: { x?: number; y?: number; width?: number; height?: number }
:默认窗口位置与尺寸。defaultMaximized?: boolean
:是否默认最大化。icon?: string
:图标 URL(建议绝对 URL 或 data URI)。description?/author?/website?/github?
:元信息,用于“应用”页展示。
mount
上下文PluginMountCtx
:filePath: string
:当前打开的文件完整路径(虚拟文件系统路径)。entry: { name: string; is_dir: boolean; size: number; mtime: number; type?: string; is_image?: boolean }
urls: { downloadUrl: string }
:无需鉴权即可读取当前文件内容的临时 URL。host: { close(): void }
:请求宿主关闭当前应用视图。
说明:
- 插件不需要自己调用后端 API。读取文件内容统一使用
ctx.urls.downloadUrl
。 - 插件只能操作传入的
container
节点,不要修改宿主页面其他 DOM 或全局样式(如需注入样式,请设置唯一 ID,避免污染)。
清单上报与后端字段映射
宿主会将插件对象字段映射到后端(POST /api/plugins/{id}/metadata
):
- 命名映射兼容驼峰/下划线:
supportedExts -> supported_exts
、defaultBounds -> default_bounds
、defaultMaximized -> default_maximized
。 - 存储模型见:
models/database.py:Plugin
。 - 上报失败不影响插件运行(宿主忽略错误)。
开发与构建(TSX -> IIFE JS,自包含)
- 目标:输出单文件
dist/plugin.js
,格式为 IIFE,可直接通过<script>
加载执行。 - 依赖:
react
、react-dom
、typescript
、esbuild
(打包时将 React 一并内联,避免外部运行时依赖)。 - 构建命令(示例):
bash
esbuild src/index.tsx \
--bundle \
--format=iife \
--platform=browser \
--target=es2019 \
--jsx=automatic \
--minify \
--outfile=dist/plugin.js
- 体积优化:
--define:process.env.NODE_ENV="production"
文件结构建议:
package.json
:含build
指令。tsconfig.json
:开启严格模式,jsx
设为react-jsx
。foxel.d.ts
:声明宿主期望的类型,便于 TS 校验(只声明类型,不引入宿主代码)。src/App.tsx
:React 组件。src/index.tsx
:构造plugin
对象并window.FoxelRegister(plugin)
。
示例:文本查看器(TSX)
位置:https://github.com/DrizzleTime/foxel-text-viewer
- 功能:加载
downloadUrl
文本;工具栏提供“换行/复制/关闭”。 - 支持扩展名:
['txt','md','log','json','yaml','yml','sh','py','js','ts','css','html']
- 关键点:
- 在
mount
内创建 React root 渲染;在unmount
中销毁 root。 - 仅操作
container
;样式以唯一 ID 注入。
- 在
构建:npm i && npm run build
(生成 dist/plugin.js
)。
本地调试与安装
- 后端与前端开发环境:
bash
# 后端
uv venv && source .venv/bin/activate && uv sync && \
uvicorn main:app --reload --host 0.0.0.0 --port 8000
# 前端(Vite 默认端口 5173)
cd web && bun install && bun run dev
- 提供插件 JS 的可访问 URL:
- 方式 A:将示例构建产物复制到本仓库
web/public/plugins/
,例如web/public/plugins/my-text-viewer.js
,则开发时 URL 为http://127.0.0.1:5173/plugins/my-text-viewer.js
。 - 方式 B:使用任意静态服务器(如 nginx、Vercel、GitHub Pages)托管
dist/plugin.js
,使用其公网 URL。
- 在“应用”页安装:
- 打开“应用”页 -> “安装应用” -> 粘贴插件 JS URL -> 安装。
- 首次安装完成后宿主会更新清单信息;在文件管理页面选择对应文件即可找到插件应用。
最佳实践
- 仅操作
container
;通过唯一 ID 注入样式,避免污染宿主全局。 icon
使用绝对 URL 或 data URI;避免相对路径误指宿主站点。supportedExts
使用小写、无点后缀;与宿主匹配逻辑一致。- 使用
ctx.host.close()
触发关闭;不要直接移除container
。 - 如需并发请求,注意
unmount
时取消(AbortController
)。
清单上报
上报失败不会中断插件运行;宿主忽略该错误以保证体验。
常见问题
- 未显示为可用应用:确认后缀匹配
supportedExts
;或先清空supportedExts
以验证。 - 未上报清单:确认脚本已调用
window.FoxelRegister(plugin)
。 - 图标不显示:确保 URL 可访问或使用 data URI。
- 脚本加载失败:检查 URL 是否可访问;
<script>
不受 CORS 限制,但服务端可能有防盗链限制或 CSP 限制。
附录:类型声明(摘自宿主实现)
查看类型声明
ts
// foxel.d.ts 建议拷贝使用(简化版)
export interface RegisteredPlugin {
mount: (container: HTMLElement, ctx: {
filePath: string;
entry: {
name: string;
is_dir: boolean;
size: number;
mtime: number;
type?: string;
is_image?: boolean;
};
urls: { downloadUrl: string };
host: { close: () => void };
}) => void | Promise<void>;
unmount?: (container: HTMLElement) => void | Promise<void>;
key?: string; name?: string; version?: string;
supportedExts?: string[];
defaultBounds?: { x?: number; y?: number; width?: number; height?: number };
defaultMaximized?: boolean;
icon?: string; description?: string; author?: string; website?: string; github?: string;
}
declare global { interface Window { FoxelRegister?: (plugin: RegisteredPlugin) => void } }
按各示例 package.json
的 build
指令构建后,得到 dist/plugin.js
,用于在“应用”页安装。