Private
Public Access
1
0
Files
u-desk/docs/02-架构设计/插件化架构方案.md

366 lines
12 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.
# 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 1Draw.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 3Tab 系统插件化
1. `built-in/index.ts` 注册 3 个内置 Tab
2. `App.vue` getComponent 改为查 registry
3. KeepAlive include 动态化
### Phase 4app.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 前端 handlerPhase 1
preview-handlers.ts # 全部内置预览注册Phase 2
index.ts # 内置 Tab 注册Phase 3
修改:
app.go # 引入 pluginMgrPhase 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 时只需替换底层实现。