Files
Thing/MODULE_DEV_GUIDE.md
T

323 lines
11 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.
# 模块开发指南
本文档详细说明 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` 的模块不可被用户禁用,开关处于禁用状态