366 lines
12 KiB
Markdown
366 lines
12 KiB
Markdown
# u-desk 插件化架构设计方案
|
||
|
||
> 状态:调研完成,待实施
|
||
> 日期:2026-04-11
|
||
|
||
## 一、现状痛点
|
||
|
||
| 痛点 | 现状 | 影响 |
|
||
|------|------|------|
|
||
| **app.go God Object** | 958 行,67 个方法全在一个 struct 上 | 难以维护,新功能必须改核心文件 |
|
||
| **App.vue 硬编码映射** | `getComponent()` 是 3 个 key 的字面量对象 | 新 Tab 必须改源码 |
|
||
| **文件预览 if/else 链** | `FileEditorPanel.vue` 有 ~12 层 v-if/v-else-if | 新增文件类型需改 5+ 处 |
|
||
| **大体积功能嵌入** | draw.io 等 ~12-15MB 功能无法按需加载 | 安装包膨胀 |
|
||
|
||
## 二、架构总览
|
||
|
||
```
|
||
+================================================================+
|
||
| u-desk Application |
|
||
+=================================================================
|
||
| Core (Go) Plugin Manager |
|
||
| - App facade - Registry / Lifecycle / Loader |
|
||
| - ConfigStore (SQLite) - TabRegistry |
|
||
| - EventBus (Wails Events) - PreviewRegistry |
|
||
+----------+----------+---------+-----------+----------+ |
|
||
| | | | | |
|
||
+-------+ +------+ +-----+ +--------+ +------+ +---+ |
|
||
|builtin: |builtin: |builtin | builtin: | future | future |
|
||
| file-sys| db-cli | md-edit| drawio | JS/WASM| Go .so |
|
||
+---------+---------+--------+-----------+--------+------------+
|
||
|
|
||
+==============================================================+|
|
||
| Frontend (Vue 3) ||
|
||
| PluginRegistry (TS) ||
|
||
| - TabProviders → 替代 App.vue 硬编码映射 ||
|
||
| - FilePreviewHandlers → 替代 FileEditorPanel if/else 链 ||
|
||
| - ComponentLoader → defineAsyncComponent 懒加载 ||
|
||
+==============================================================+
|
||
```
|
||
|
||
## 三、核心接口定义
|
||
|
||
### 3.1 后端插件接口(Go)
|
||
|
||
```go
|
||
// 文件路径: internal/plugin/plugin.go
|
||
|
||
// PluginID 插件唯一标识
|
||
type PluginID string
|
||
|
||
// PluginMetadata 插件元数据
|
||
type PluginMetadata struct {
|
||
ID PluginID `json:"id"`
|
||
Name string `json:"name"`
|
||
Version string `json:"version"`
|
||
Description string `json:"description"`
|
||
Author string `json:"author"`
|
||
TabKey string `json:"tab_key,omitempty"` // 提供的 Tab key
|
||
FileExtensions []string `json:"file_extensions,omitempty"` // 处理的文件扩展名
|
||
}
|
||
|
||
// PluginCapability 插件能力标志
|
||
type PluginCapability int
|
||
|
||
const (
|
||
CapabilityTabProvider PluginCapability = 1 << iota // 提供 Tab 页面
|
||
CapabilityFilePreview // 文件预览
|
||
)
|
||
|
||
// Plugin 核心插件接口
|
||
type Plugin interface {
|
||
Meta() PluginMetadata
|
||
Capabilities() PluginCapability
|
||
Init(ctx context.Context, core CoreServices) error
|
||
Start() error
|
||
Stop() error
|
||
}
|
||
|
||
// TabProvider Tab 提供者接口(可选)
|
||
type TabProvider interface {
|
||
TabDefinition() TabDef
|
||
TabComponentPath() string
|
||
}
|
||
|
||
// FilePreviewHandler 文件预览处理接口(可选)
|
||
type FilePreviewHandler interface {
|
||
CanPreview(filename string, mimeType string) bool
|
||
PreviewInfo(filename string) PreviewInfo
|
||
}
|
||
|
||
// PreviewInfo 预览元信息(发送给前端)
|
||
type PreviewInfo struct {
|
||
Type string `json:"type"` // "drawio", "image", "pdf"
|
||
Title string `json:"title"`
|
||
Icon string `json:"icon,omitempty"`
|
||
NeedsContainer bool `json:"needs_container,omitempty"`
|
||
ContainerConfig map[string]string `json:"container_config,omitempty"`
|
||
SupportsEdit bool `json:"supports_edit"`
|
||
PreloadHint string `json:"preload_hint,omitempty"`
|
||
}
|
||
|
||
// TabDef Tab 定义
|
||
type TabDef struct {
|
||
Key string `json:"key"`
|
||
Title string `json:"title"`
|
||
Icon string `json:"icon,omitempty"`
|
||
Enabled bool `json:"enabled"`
|
||
Order int `json:"order"`
|
||
}
|
||
```
|
||
|
||
### 3.2 前端插件接口(TypeScript)
|
||
|
||
```typescript
|
||
// 文件路径: frontend/src/plugin/types.ts
|
||
|
||
/** 插件能力标志 */
|
||
export enum PluginCapability {
|
||
None = 0,
|
||
TabProvider = 1 << 0, // 提供 Tab
|
||
FilePreview = 1 << 1, // 文件预览
|
||
Settings = 1 << 2, // 设置页
|
||
}
|
||
|
||
/** Tab 插件定义 */
|
||
export interface TabPluginDefinition {
|
||
key: string
|
||
title: string
|
||
icon?: string
|
||
componentLoader: () => Promise<Component> // 异步组件加载器
|
||
defaultVisible?: boolean
|
||
order?: number
|
||
}
|
||
|
||
/** 文件预览处理器定义 */
|
||
export interface FilePreviewHandlerDefinition {
|
||
id: string
|
||
name: string
|
||
icon: string
|
||
priority: number // 越大越优先
|
||
canHandle: (filename: string) => boolean
|
||
getComponent?: () => Promise<Component>
|
||
getRenderConfig?: (filePath: string) => RenderConfig
|
||
supportsEdit?: boolean
|
||
}
|
||
|
||
/** 渲染配置 */
|
||
export interface RenderConfig {
|
||
type: 'iframe' | 'html' | 'custom'
|
||
src?: string
|
||
htmlContent?: string
|
||
props?: Record<string, any>
|
||
}
|
||
```
|
||
|
||
## 四、插件管理器设计
|
||
|
||
### 4.1 后端 PluginManager
|
||
|
||
```go
|
||
// internal/plugin/manager.go
|
||
|
||
type Manager struct {
|
||
mu sync.RWMutex
|
||
plugins map[PluginID]Plugin
|
||
core CoreServices
|
||
tabReg *TabRegistry
|
||
previewReg *PreviewRegistry
|
||
ctx context.Context
|
||
}
|
||
|
||
func NewManager(core CoreServices) *Manager
|
||
func (m *Manager) Register(p Plugin) error // 注册插件
|
||
func (m *Manager) InitAll(ctx context.Context) error // 初始化所有插件
|
||
func (m *Manager) StartByTabKey(tabKey string) error // 按 Tab 懒启动
|
||
func (m *Manager) GetPluginInfos() []PluginMetadata // 获取插件列表
|
||
func (m *Manager) ResolvePreview(filename string) (*PreviewInfo, error)
|
||
func (m *Manager) Shutdown() error
|
||
```
|
||
|
||
### 4.2 前端注册中心
|
||
|
||
```typescript
|
||
// frontend/src/plugin/registry.ts
|
||
|
||
import { reactive } from 'vue'
|
||
|
||
const state = reactive({
|
||
tabPlugins: new Map<string, TabPluginDefinition>(),
|
||
previewHandlers: [] as FilePreviewHandlerDefinition[],
|
||
})
|
||
|
||
export function registerTabPlugin(def: TabPluginDefinition): void
|
||
export function getTabComponent(key: string): (() => Promise<Component>) | null
|
||
export function getAllTabDefinitions(): TabPluginDefinition[]
|
||
export function registerPreviewHandler(handler: FilePreviewHandlerDefinition): void
|
||
export function resolvePreviewHandler(filename: string): FilePreviewHandlerDefinition | null
|
||
```
|
||
|
||
## 五、关键改造点
|
||
|
||
### 5.1 FileEditorPanel.vue 重构(最大收益)
|
||
|
||
**改造前**:12 层 v-if/v-else-if 链(image/video/audio/pdf/html/md/excel/word/csv/text/binary)
|
||
|
||
**改造后**:
|
||
```vue
|
||
<template>
|
||
<!-- iframe 类型(draw.io 等) -->
|
||
<iframe v-if="renderConfig?.type === 'iframe'" :src="renderConfig.src" />
|
||
<!-- Vue 组件类型 -->
|
||
<component v-else-if="previewComponent" :is="previewComponent" v-bind="previewProps" />
|
||
</template>
|
||
|
||
<script setup>
|
||
import { resolvePreviewHandler } from '@/plugin/registry'
|
||
|
||
const handler = computed(() =>
|
||
props.config.currentFileName ? resolvePreviewHandler(props.config.currentFileName) : null
|
||
)
|
||
const previewComponent = computed(() => handler.value?.getComponent?.())
|
||
const renderConfig = computed(() => handler.value?.getRenderConfig?.(filePath))
|
||
</script>
|
||
```
|
||
|
||
### 5.2 App.vue 改造
|
||
|
||
**改造前**:
|
||
```ts
|
||
const getComponent = (key) => ({ 'file-system': FileSystem, 'db-cli': DbCli }[key])
|
||
```
|
||
|
||
**改造后**:
|
||
```ts
|
||
import '@/plugin/built-in' // 副作用:执行内置插件注册
|
||
import { getTabComponent } from '@/plugin/registry'
|
||
|
||
const getComponent = (key) => {
|
||
const loader = getTabComponent(key)
|
||
return loader ? defineAsyncComponent(loader) : null
|
||
}
|
||
```
|
||
|
||
### 5.3 Draw.io 插件示例(首个验证插件)
|
||
|
||
**后端** (`internal/plugin/builtin/drawio_plugin.go`):
|
||
```go
|
||
type DrawIoPlugin struct{ server *http.Server }
|
||
|
||
func (p *DrawIoPlugin) Meta() PluginMetadata {
|
||
return PluginMetadata{
|
||
ID: "builtin-drawio", Name: "Draw.io 查看器",
|
||
FileExtensions: []string{"drawio", "dio"},
|
||
}
|
||
}
|
||
func (p *DrawIoPlugin) Capabilities() PluginCapability { return CapabilityFilePreview }
|
||
func (p *DrawIoPlugin) CanPreview(filename, _ string) bool {
|
||
ext := filepath.Ext(filename)
|
||
return ext == ".drawio" || ext == ".dio"
|
||
}
|
||
```
|
||
|
||
**前端** (`frontend/src/plugin/built-in/drawio-handler.ts`):
|
||
```ts
|
||
registerPreviewHandler({
|
||
id: 'drawio-preview', priority: 95,
|
||
canHandle: (f) => /\.drawio?$/i.test(f),
|
||
getRenderConfig: (path) => ({
|
||
type: 'iframe',
|
||
src: `http://localhost:18765/drawio/index.html?chrome=0&lightbox=1&stealth=1#R${xmlContent}`
|
||
})
|
||
})
|
||
```
|
||
|
||
## 六、分阶段实施路径
|
||
|
||
### Phase 0:基础设施搭建
|
||
|
||
不改现有功能,只建立骨架:
|
||
|
||
| 文件 | 内容 |
|
||
|------|------|
|
||
| `internal/plugin/plugin.go` | 核心 Plugin/TabProvider/FilePreviewHandler 接口 |
|
||
| `internal/plugin/manager.go` | PluginManager 实现 |
|
||
| `internal/plugin/tab_registry.go` | Tab 注册表 |
|
||
| `internal/plugin/preview_registry.go` | 预览处理器注册表 |
|
||
| `frontend/src/plugin/types.ts` | TS 类型定义 |
|
||
| `frontend/src/plugin/registry.ts` | 前端注册中心 |
|
||
|
||
### Phase 1:Draw.io 插件验证
|
||
|
||
用第一个真实插件验证整条链路:
|
||
|
||
| 步骤 | 文件 | 操作 |
|
||
|------|------|------|
|
||
| 1 | `internal/plugin/builtin/drawio_plugin.go` | 新建 DrawIoPlugin |
|
||
| 2 | `frontend/src/plugin/built-in/drawio-handler.ts` | 新建前端 handler |
|
||
| 3 | `app.go` | 小改:引入 pluginMgr,注册 DrawIoPlugin |
|
||
| 4 | `FileEditorPanel.vue` | 微改:if/else 末尾追加 drawio 分支 |
|
||
|
||
**验证标准**:打开 `.drawio` 文件 → 显示预览 → 其他文件不受影响
|
||
|
||
### Phase 2:文件预览系统重构
|
||
|
||
将全部 12 种预览迁移到插件注册表:
|
||
|
||
1. 在 `preview-handlers.ts` 注册所有内置处理器
|
||
2. `FileEditorPanel.vue` template 改为 `<component :is>` / `<iframe>`
|
||
3. `filePreviewHandlers.js` 的 Excel/Word/CSV 逻辑拆分到对应组件
|
||
|
||
### Phase 3:Tab 系统插件化
|
||
|
||
1. `built-in/index.ts` 注册 3 个内置 Tab
|
||
2. `App.vue` getComponent 改为查 registry
|
||
3. KeepAlive include 动态化
|
||
|
||
### Phase 4:app.go 瘦身(可选延后)
|
||
|
||
方法签名保留(Wails v2 绑定要求),实现委托给 pluginMgr。
|
||
|
||
## 七、新增/变更文件清单
|
||
|
||
```
|
||
新增:
|
||
internal/plugin/
|
||
plugin.go # 接口定义
|
||
manager.go # PluginManager
|
||
tab_registry.go # Tab 注册表
|
||
preview_registry.go # 预览注册表
|
||
builtin/
|
||
drawio_plugin.go # Draw.io 插件(Phase 1)
|
||
frontend/src/plugin/
|
||
types.ts # TS 接口
|
||
registry.ts # 注册中心
|
||
built-in/
|
||
drawio-handler.ts # Draw.io 前端 handler(Phase 1)
|
||
preview-handlers.ts # 全部内置预览注册(Phase 2)
|
||
index.ts # 内置 Tab 注册(Phase 3)
|
||
|
||
修改:
|
||
app.go # 引入 pluginMgr(Phase 1)
|
||
frontend/src/.../FileEditorPanel.vue # 追加插件入口 / 重写
|
||
frontend/src/App.vue # getComponent 改 registry
|
||
```
|
||
|
||
## 八、风险与应对
|
||
|
||
| 风险 | 应对策略 |
|
||
|------|----------|
|
||
| Wails v2 无法动态绑定 API 方法 | App 上预留 `PluginCall(id, method, params)` 统一分发 |
|
||
| FileEditorPanel 改造影响面大 | Phase 1 只在 if/else 末尾追加;Phase 2 用 feature flag 切换新旧路径 |
|
||
| 包体积膨胀(draw.io ~12MB) | 条件编译 `go build tags` 或未来外部下载缓存 |
|
||
| 过度抽象增加复杂度 | YAGNI 原则:只在确实需要扩展点时才加接口 |
|
||
|
||
## 九、Wails v3 兼容性预留
|
||
|
||
Wails v3 (alpha.74) 主要变化对插件架构的影响:
|
||
|
||
| 维度 | v2 | v3 | 影响 |
|
||
|------|----|----|------|
|
||
| 绑定方式 | struct 方法自动生成 | 手动注册 handler | 更有利于插件化 |
|
||
| 前端调用 | `window.go.main.App.Xxx()` | `window.go.invoke()` | 需适配层 |
|
||
| 事件系统 | `runtime.EventsEmit` | 类似但 API 不同 | 需抽象层 |
|
||
|
||
建议在 eventbus 中封装一层 Wails 版本抽象,切换 v3 时只需替换底层实现。
|