323 lines
11 KiB
Markdown
323 lines
11 KiB
Markdown
# 模块开发指南
|
||
|
||
本文档详细说明 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/<name>/index.ts` | 各模块的配置声明 |
|
||
| `src/modules/<name>/*.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<string, Component> = {
|
||
// ... 已有映射
|
||
'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<void>
|
||
/** 模块组件挂载时调用(切换到该模块) */
|
||
onActivate?: () => void | Promise<void>
|
||
/** 模块组件卸载时调用(切换离开该模块) */
|
||
onDeactivate?: () => void | Promise<void>
|
||
/** 模块被禁用时调用 */
|
||
onDisable?: () => void | Promise<void>
|
||
/** 模块被启用时调用 */
|
||
onEnable?: () => void | Promise<void>
|
||
}
|
||
```
|
||
|
||
使用示例:
|
||
|
||
```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` 的模块不可被用户禁用,开关处于禁用状态
|