# 模块开发指南 本文档详细说明 Thing 应用的模块系统架构,帮助开发者快速理解并创建新模块。 ## 架构概述 Thing 采用**统一模块注册机制**,每个模块通过 `index.ts` 自描述其全部配置(名称、图标、组件、搜索项、进程配置、生命周期钩子等),由中央注册表 `moduleRegistry` 统一管理。 ``` ┌─────────────────────────────────────────────────────┐ │ main.ts │ │ import './modules' (触发注册) │ └──────────────────────┬──────────────────────────────┘ ▼ ┌─────────────────────────────────────────────────────┐ │ src/modules/index.ts │ │ 聚合入口:导入所有模块配置并注册 │ └──────────────────────┬──────────────────────────────┘ ▼ ┌─────────────────────────────────────────────────────┐ │ src/modules/registry.ts │ │ ModuleRegistry 单例:注册 / 查询 / 组件懒加载 │ └──────┬───────────────┬───────────────┬──────────────┘ │ │ │ ▼ ▼ ▼ App.vue Sidebar.vue GeneralSettings (组件加载) (侧边栏导航) (模块开关管理) ``` ## 核心文件说明 | 文件 | 职责 | |------|------| | `src/types/module.ts` | 模块配置类型定义 | | `src/modules/registry.ts` | 注册表单例:注册、查询、组件懒加载缓存 | | `src/modules/index.ts` | 聚合入口:导入并注册所有模块 | | `src/modules/icons.ts` | 模块图标映射表 | | `src/modules//index.ts` | 各模块的配置声明 | | `src/modules//*.vue` | 模块前端组件 | ## 模块配置类型 ```typescript // src/types/module.ts interface ModuleConfig { /** 模块唯一标识(如 'proxy'、'clipboard') */ id: string /** 显示名称 */ name: string /** 图标标识(对应 icons.ts 中的 key) */ icon: string /** 模块描述(显示在设置界面的模块管理中) */ description: string /** 模块分类 */ category: 'network' | 'tool' | 'system' | 'media' /** 默认是否启用(默认 true) */ defaultEnabled?: boolean /** 是否为内置模块(不可禁用,如设置模块) */ builtin?: boolean /** 懒加载组件的 loader 函数(推荐) */ loader?: () => Promise<{ default: Component }> /** 直接组件引用(内置模块可用,无需懒加载) */ component?: Component /** 全局搜索项 */ searchItems?: SearchIndexItem[] /** 进程配置(需要管理子进程的模块填写) */ process?: ModuleProcessConfig /** 生命周期钩子 */ lifecycle?: ModuleLifecycle /** 排序权重(数值越小越靠前,默认 100) */ order?: number } ``` ## 创建新模块 ### 第 1 步:创建模块目录 ``` src/modules/my-module/ ├── index.ts # 模块配置 └── MyModule.vue # 前端组件 ``` ### 第 2 步:编写模块配置 ```typescript // src/modules/my-module/index.ts import type { ModuleConfig } from '@/types/module' export const moduleConfig: ModuleConfig = { id: 'my-module', name: '我的模块', icon: 'my-module', // 需在 icons.ts 中添加映射 description: '模块功能描述', category: 'tool', defaultEnabled: true, loader: () => import('./MyModule.vue'), searchItems: [ { title: '功能名称', description: '功能描述', keywords: ['关键词1', '关键词2', 'keyword'] } ], order: 70 } ``` ### 第 3 步:注册模块 在 `src/modules/index.ts` 中添加导入: ```typescript import { moduleConfig as myModule } from './my-module' const allModules: ModuleConfig[] = [ // ... 已有模块 myModule ] moduleRegistry.registerAll(allModules) ``` ### 第 4 步:添加图标映射 在 `src/modules/icons.ts` 中添加: ```typescript import { Wrench } from '@lucide/vue' // 选择合适的图标 export const moduleIconMap: Record = { // ... 已有映射 'my-module': Wrench } ``` ### 第 5 步(可选):添加 Rust 后端命令 如果模块需要 Rust 后端支持,在 `src-tauri/src/` 中创建命令模块,并在 `lib.rs` 的 `invoke_handler` 中注册。 ## 进程管理 需要管理外部子进程的模块(如 mihomo、aria2)通过 `ModuleProcessConfig` 声明进程配置。 ### 配置示例 ```typescript // src/modules/proxy/index.ts export const moduleConfig: ModuleConfig = { // ... process: { name: 'mihomo', // 进程标识 executable: '', // 可执行文件路径(运行时确定) args: ['-f', 'config.yaml'], // 启动参数 autoStart: false, // 模块启用时是否自动启动 restartOnCrash: true, // 崩溃后自动重启 maxRestarts: 3 // 最大重启次数(0 = 不限制) } } ``` ### 前端 API ```typescript import { useProcessStore } from '@/stores/processStore' const processStore = useProcessStore() // 通过模块 ID 启动进程(自动读取模块配置) await processStore.startByModule('proxy') // 停止进程 await processStore.stopByModule('proxy') // 获取进程状态(从缓存读取) const status = processStore.getProcessStatus('proxy') // status.status: 'running' | 'stopped' | 'crashed' | 'starting' // 刷新所有进程状态 await processStore.refreshAll() ``` ### 自动化行为 模块启用/禁用时,`appStore.toggleModule` 会自动处理: | 操作 | 禁用模块 | 启用模块 | |------|----------|----------| | 进程 | 停止运行中的进程 | 若 `autoStart` 为 true,启动进程 | | 搜索项 | 移除该模块的搜索项 | 恢复该模块的搜索项 | | 组件缓存 | 清除组件缓存,释放内存 | 下次访问时重新懒加载 | | 生命周期 | 调用 `onDisable` 钩子 | 调用 `onEnable` 钩子 | | 侧边栏 | 从侧边栏隐藏 | 在侧边栏显示 | | Toast 通知 | 显示"已禁用 {模块名}" | 显示"已启用 {模块名}" | ## 生命周期钩子 ```typescript interface ModuleLifecycle { /** 模块首次加载时调用 */ onInit?: () => void | Promise /** 模块组件挂载时调用(切换到该模块) */ onActivate?: () => void | Promise /** 模块组件卸载时调用(切换离开该模块) */ onDeactivate?: () => void | Promise /** 模块被禁用时调用 */ onDisable?: () => void | Promise /** 模块被启用时调用 */ onEnable?: () => void | Promise } ``` 使用示例: ```typescript export const moduleConfig: ModuleConfig = { // ... lifecycle: { onEnable: async () => { console.log('模块已启用') // 初始化资源、建立连接等 }, onDisable: async () => { console.log('模块已禁用') // 释放资源、关闭连接等 }, onActivate: () => { console.log('用户切换到本模块') // 开始实时数据更新等 }, onDeactivate: () => { console.log('用户离开本模块') // 暂停实时更新以节省资源 } } } ``` ## 全局搜索 模块通过 `searchItems` 声明可被全局搜索的功能项。用户在标题栏搜索框输入关键词时,匹配的搜索项会显示在结果列表中。 ```typescript searchItems: [ { title: '代理设置', // 显示标题 description: '配置网络代理', // 显示描述 keywords: ['代理', 'proxy', '网络'] // 匹配关键词 } ] ``` 如果搜索项需要执行特定操作(如跳转到子页面、触发命令),在模块组件挂载时通过 `searchStore.registerAction` 注册: ```typescript import { useSearchStore } from '@/stores/searchStore' const searchStore = useSearchStore() onMounted(() => { // 注册第 0 个搜索项的 action searchStore.registerAction('my-module', 0, () => { // 跳转到特定子页面或执行操作 activeTab.value = 'settings' }) }) ``` ## 模块分类 | 分类 | 说明 | 适用场景 | |------|------|----------| | `network` | 网络相关 | 代理、下载器、网络工具 | | `tool` | 实用工具 | 剪贴板、文件搜索、文本处理 | | `system` | 系统相关 | 硬件监控、系统设置 | | `media` | 媒体相关 | 截图、录屏、图片处理 | 分类目前用于元信息标记,未来可用于设置界面的分组展示。 ## 模块禁用机制详解 当用户在设置页面切换模块开关时: ``` 用户点击开关 │ ▼ appStore.toggleModule(moduleId, enabled) │ ├─ 禁用时: │ ├─ searchStore.unregisterModule(moduleId) // 移除搜索项 │ ├─ processStore.stopByModule(moduleId) // 停止进程 │ ├─ config.lifecycle?.onDisable?.() // 生命周期钩子 │ ├─ moduleRegistry.clearComponentCache(id) // 清除组件缓存 │ ├─ modules[id].enabled = false // 更新状态 │ └─ toast.success('已禁用 {模块名}') // 通知用户 │ └─ 启用时: ├─ modules[id].enabled = true // 更新状态 ├─ config.lifecycle?.onEnable?.() // 生命周期钩子 ├─ searchStore.registerItem(...) // 恢复搜索项 ├─ processStore.startByModule(moduleId) // 启动进程(若 autoStart) └─ toast.success('已启用 {模块名}') // 通知用户 ``` 禁用后的效果: - 模块从侧边栏导航中隐藏 - 模块的搜索项从全局搜索中移除 - 模块的后台进程被停止 - 模块的组件缓存被清除,释放内存 - 如果当前正在查看被禁用的模块,自动切换到第一个可用模块 ## 注意事项 1. **模块 ID 必须唯一**:重复注册会被忽略并输出警告 2. **图标必须映射**:模块配置中的 `icon` 字符串必须在 `icons.ts` 中有对应映射,否则回退到 Settings 图标 3. **懒加载优先**:使用 `loader` 而非 `component`,避免首屏加载所有模块代码 4. **避免循环依赖**:模块的 `index.ts` 只导出配置,不导入其他模块的 store 5. **进程配置的 executable**:通常留空,由模块组件在运行时根据用户设置确定实际路径 6. **内置模块**:设置 `builtin: true` 的模块不可被用户禁用,开关处于禁用状态 ## 跨模块开发经验(代理模块沉淀) 以下要点来自代理模块开发,对后续涉及子进程管理、外部 API 交互、shadcn-vue 组件使用的模块同样适用。 ### Tauri 命令与主线程 - **同步命令(`pub fn`)会阻塞主线程**:Tauri 的同步命令在主线程执行,其内部的 `std::thread::sleep`、磁盘 I/O、网络请求会阻塞所有 async 命令的调度。涉及等待/阻塞操作的命令必须声明为 `pub async fn`,并用 `tauri::async_runtime::spawn_blocking(|| { std::thread::sleep(...) }).await` 将阻塞操作放到线程池。 - **Windows 端口释放有延迟**:`child.kill()` + `child.wait()` 后 TCP 端口不会立即可用,需等待约 800ms 再重新绑定。重启类命令应预留此延迟。 - **`Mutex` 持锁期间禁止 sleep**:`ProcessManager::check_and_cleanup` 等持锁函数中不要执行长时间 sleep,否则会阻塞所有需要该锁的命令(如状态查询)。应先释放锁再 sleep,或移出临界区。 - **进程监控线程**:`start_monitoring_thread` 每 3 秒检查一次进程状态,崩溃时自动重启(可配置 `maxRestarts`)。前端通过监听 `process-status-changed` 事件更新 UI。 ### 外部 API 交互 - **API 就绪轮询**:子进程 spawn 后 API 不会立即可用(需初始化配置、加载 geo 文件等)。前端应在请求前轮询健康检查接口(如 `/version`),500ms 间隔、10s 超时。 - **缓存配置避免频繁读盘**:后端 Manager 每次方法调用都从磁盘读 settings 会拖慢批量操作。建议在 Manager 内维护内存缓存,`save_settings` 时同步更新。 - **并发请求限流**:批量测速等场景不要一次性 `Promise.all` 全部请求,应分批(如每批 20 个),避免压垮子进程。 ### shadcn-vue / reka-ui 注意事项 - **Select 禁止空字符串 value**:`` 会触发警告并失效。使用哨兵值(如 `__default__`、`__all__`)替代空字符串,在 `@update:model-value` 回调中转回空值。 - **Select 双击问题**:reka-ui Select 的 DismissableLayer + closeAutoFocus 会导致连续点击两个 Select 时第一次点击仅关闭上一个、需第二次点击才打开下一个。当前未完美解决,建议同一界面避免放置过多相邻 Select。 - **Switch 使用 `model-value` / `update:model-value`**:reka-ui v2+ 的 Switch 不再使用 `checked` / `update:checked`。 - **Sonner toast 不可见**:通常是缺少 `vue-sonner/lib/index.css` 导入,而非 z-index 问题。确保在入口处导入该 CSS。 - **AlertDialog 替代 confirm()**:原生 `confirm()` 在 Tauri WebView 中样式不一致,使用 shadcn-vue AlertDialog 封装 Promise 化的 `showConfirm()` 函数,支持 destructive 样式。 ### 数据一致性与自愈 - **配置文件与磁盘 reconcile**:settings.json 中的列表(如订阅 profiles)可能与磁盘文件不同步(用户手动删除、反序列化失败被 default 覆盖)。每次 `load_settings` 时应扫描磁盘补全缺失条目,并在 `currentProfile` 为 null 时自动指向第一个。 - **操作顺序防重复**:导入文件后写入 settings 时,若先写文件再 load_settings(含 reconcile 扫盘),reconcile 会扫到新文件添加一次,随后 push 又添加一次。应先 load_settings → 写文件 → push(加去重保护)。 - **子进程崩溃循环防护**:子进程因依赖文件损坏(如 geo 数据库)崩溃时,`restart_on_crash` 会反复重启。需配置备用下载源(如 jsdelivr 镜像)避免无代理时 GitHub 超时。 ### UI 细节 - **瀑布流布局避免卡片等高撑开**:使用 `columns-1 md:columns-2 gap-4 [&>*]:mb-4 [&>*]:break-inside-avoid` 替代 `grid`,让卡片按内容高度自然排列。 - **模块禁用清理系统状态**:模块 `onDisable` 钩子应清理系统级副作用(如系统代理注册表项),避免模块停用后遗留导致系统异常。 - **应用退出清理**:在 `lib.rs` 的 `quit_app` 命令和托盘退出事件中都要调用 `cleanup_on_exit` + `stop_all`,确保任何退出路径都清理干净。