12 KiB
12 KiB
架构设计
一、架构全景
+==================================================================+
| u-desk Application |
+===================================================================+
| Core (Go) Plugin Manager |
| - App facade - Registry / Lifecycle / Loader |
| - ConfigStore (SQLite) - Builtin Plugins (编译时) |
| - EventBus (Wails Events) - External Plugins (运行时) |
| - UpdateEngine - Marketplace Client |
+----------+----------+---------+-----------+-----------+ |
| | | | | |
+-------+ +------+ +-----+ +--------+ +------+ +----+ |
|builtin: |builtin: |builtin | builtin: |extern:|extern |
| file-sys| md-edit| drawio | db-cli | user-A| user-B |
+---------+---------+--------+-----------+--------+--------------+
|
+=================================================================+|
| Frontend (Vue 3) ||
| PluginRegistry (TS) ||
| - TabProviders → 替代 App.vue 硬编码映射 ||
| - FilePreviewHandlers → 替代 FileEditorPanel if/else 链 ||
| - ComponentLoader → defineAsyncComponent 懒加载 ||
| - PluginSettingsUI → 设置面板插件管理区块 ||
+=================================================================+
二、两层插件体系
| 维度 | 内置插件 (Builtin) | 外部插件 (External/Market) |
|---|---|---|
| 来源 | 编译时打包进二进制 | 运行时从市场安装 |
| 位置 | internal/plugin/builtin/ |
用户数据目录 plugins/ |
| 生命周期 | 随应用启动 | 用户控制 |
| 管理操作 | 启用 / 禁用 | 安装 / 卸载 / 启用 / 禁用 / 更新 |
| 更新方式 | 随应用版本更新 | 独立更新(复用 UpdateAPI) |
| 安全级别 | 完全信任(同进程) | 沙箱隔离(Phase 5) |
| 示例 | 文件系统、Markdown、Draw.io | 第三方预览器、云存储同步 |
三、设置面板形态(最终态)
┌─ 设置 ──────────────────────────────────────────────┐
│ │
│ ┌─ Tab 配置 ─┐ ┌─ 版本更新 ─┐ ┌─ 插件管理 ─┐ │
│ │ │ │ │ │ │ │
│ │ 拖拽排序 │ │ 自动检查 │ │ 内置插件 │ │
│ │ 显隐控制 │ │ 手动检查 │ │ ✓ Draw.io │ │
│ │ 默认选择 │ │ 下载/安装 │ │ ✓ Markdown │ │
│ │ │ │ │ │ ○ 数据库CLI │ │
│ └─────────────┘ └─────────────┘ │ │ │
│ │ 外部插件 │ │
│ │ ✓ 云盘同步 v2.1│ │
│ │ ○ 思维导图(停用)│ │
│ │ │ │
│ │ [浏览插件市场] │ │
│ └──────────────┘ │
└──────────────────────────────────────────────────────┘
四、UI 插槽体系(Slot System)
4.1 当前布局(硬编码)
┌─────────────────────────────────┐
│ 标题栏 + Tab 导航 + 窗口控制 │ ← 固定区域,不可扩展
├─────────────────────────────────┤
│ │
│ <component :is="activeTab"> │ ← 内容区,getComponent() 硬编码
│ │
├─────────────────────────────────┤
│ 更新通知 / 设置抽屉 │ ← 固定区域
└─────────────────────────────────┘
问题:所有 UI 区域都是硬编码的。新插件无法在标题栏加按钮、无法在工具栏扩展、无法在侧边栏注入面板。
4.2 插件化后的插槽布局
┌── slot: titlebar-extra ──────────────────────────────┐
│ [u-desk] [文件管理 | Markdown] [🔌插件A] [🔌插件B] [—][□][×] │
├──────────────────────────────────────────────────────┤
│ slot: sidebar-left │ slot: main-content │ slot: sidebar-right │
│ │ │ │
│ [内置侧边栏] │ <component :is="activeTab"> │ (预留) │
│ ───────────── │ │ │
│ [插件侧边面板] │ │ │
│ │ │ │
├────────────────────┴───────────────────────────────────┴──────────────────────┤
│ slot: toolbar-extra: [📋] [🔍] [🔌插件工具按钮...] │
├──────────────────────────────────────────────────────────────────────────────┤
│ slot: status-bar: [就绪] [行: 128] [编码: UTF-8] [🔌插件状态项...] │
└──────────────────────────────────────────────────────────────────────────────┘
右键菜单 → slot: context-menu-extra(追加菜单项)
设置面板 → slot: settings-panel(内嵌区块)
4.3 插槽定义表
| 插槽 ID | 位置 | 控制方式 | 谁可注册 | Phase |
|---|---|---|---|---|
titlebar-extra |
标题栏右侧(窗口控制按钮左侧) | 声明式:PluginMetadata.uiSlots[] | 内置 + 插件 | 3 |
tab-bar |
Tab 导航条内部 | 自动:CapabilityTabProvider 插件自动合并 | PluginManager 驱动 | 1 |
main-content |
中央主内容区 | 自动:getComponent() 查 registry | registry 驱动 | 1 |
sidebar-left |
左侧边栏底部(FileSystem Sidebar 之后) | 声明式 | 插件 | 3 |
sidebar-right |
右侧边栏(预览区旁) | 声明式 | 插件 | 3 |
toolbar-extra |
工具栏末尾追加按钮 | 声明式 | 插件 | 2 |
status-bar |
底部状态栏追加信息 | 声明式 | 插件 | 3 |
context-menu-extra |
右键菜单追加项 | 声明式 | 插件 | 2 |
settings-panel |
设置面板内嵌区块 | 声明式:CapabilitySettings | 插件 | 3 |
4.4 插槽注册机制(前端)
// frontend/src/plugin/slots.ts
interface SlotDefinition {
id: string // 插槽 ID
component?: Component // 注入的 Vue 组件
position?: 'prepend' | 'append' | 'before' | 'after' // 插入位置
order?: number // 同一插槽内的排序权重
}
interface UISlotRegistry {
/** 插件声明要占用哪些插槽 */
registerSlot(pluginId: string, def: SlotDefinition): void
/** App.vue 查询某插槽有哪些组件 */
getSlotComponents(slotId: string): SlotDefinition[]
/** 所有已注册的插槽概览 */
getAllSlots(): Map<string, SlotDefinition[]>
}
4.5 App.vue 插槽改造示意
<template>
<!-- 标题栏 -->
<div class="titlebar">
<div class="titlebar-left">u-desk</div>
<a-tabs v-model="activeTab" class="header-tabs">...</a-tabs>
<!-- 插槽: titlebar-extra -->
<component
v-for="slot in getSlotComponents('titlebar-extra')"
:key="slot.id"
:is="slot.component"
/>
<WindowControls />
</div>
<!-- 主区域 -->
<div class="main-layout">
<!-- 插槽: sidebar-left -->
<component
v-for="slot in getSlotComponents('sidebar-left')"
:is="slot.component"
/>
<!-- 插槽: main-content(核心) -->
<KeepAlive :include="cacheableComponents">
<component :is="getComponent(activeTab)" />
</KeepAlive>
</div>
<!-- 工具栏插槽等... -->
</template>
4.6 后端接口支持
// PluginMetadata 新增字段
type PluginMetadata struct {
// ... 现有字段 ...
UISlots []string `json:"ui_slots,omitempty"` // 声明占用的 UI 插槽列表
}
// Manager 新增方法
func (m *Manager) GetUISlotContents(slotID string) []UISlotEntry
4.7 切割原则
| 原则 | 说明 |
|---|---|
| 最小暴露 | 只开放必要的插槽,不把整个布局变成"配置驱动" |
| 位置固定 | 每个插槽的位置在 App.vue 中固定,插件只能决定"往里放什么",不能移动插槽本身 |
| 顺序可控 | 同一插槽多插件通过 order 控制排列顺序 |
| 隔离渲染 | 每个插槽内的插件组件有独立作用域,避免样式/状态污染 |
| 优雅降级 | 插件组件报错不影响宿主 UI(ErrorBoundary 包裹) |
五、关键改造点对比
5.1 App.vue 改造(Phase 3)
改造前:
import FileSystem from './components/FileSystem/index.vue'
import MarkdownEditor from './views/markdown-editor/index.vue'
const getComponent = (key: string) => ({
'file-system': FileSystem,
'markdown-editor': MarkdownEditor
}[key] || null)
改造后:
import '@/plugin/built-in' // 副作用:执行所有内置插件注册
import { getTabComponent } from '@/plugin/registry'
const getComponent = (key: string) => {
const loader = getTabComponent(key)
return loader ? defineAsyncComponent(loader) : null
}
5.2 FileEditorPanel.vue 改造(Phase 2)
改造前:10 层 v-if/v-else-if 链(binary/image/video/audio/pdf/excel/word/csv/html/md/text)
改造后:
<template>
<div class="editor-content">
<iframe v-if="renderConfig?.type === 'iframe'" :src="renderConfig.src" />
<component v-else-if="previewComponent" :is="previewComponent" v-bind="previewProps" />
<CodeEditor v-else ... />
</div>
</template>
<script setup lang="ts">
import { resolvePreviewHandler } from '@/plugin/registry'
import { computed } from 'vue'
const handler = computed(() =>
props.config.currentFileName ? resolvePreviewHandler(props.config.currentFileName) : null
)
const previewComponent = computed(() => handler.value?.getComponent?.())
const renderConfig = computed(() => handler.value?.getRenderConfig?.(props.filePath ?? ''))
</script>