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

12 KiB
Raw Blame History

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

// 文件路径: 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

// 文件路径: 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

// 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 前端注册中心

// 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

改造后

<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 改造

改造前

const getComponent = (key) => ({ 'file-system': FileSystem, 'db-cli': DbCli }[key])

改造后

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)

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)

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 时只需替换底层实现。