Files
Thing/MODULE_DEV_GUIDE.md
T

11 KiB
Raw Blame History

模块开发指南

本文档详细说明 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 模块前端组件

模块配置类型

// 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 步:编写模块配置

// 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 中添加导入:

import { moduleConfig as myModule } from './my-module'

const allModules: ModuleConfig[] = [
  // ... 已有模块
  myModule
]

moduleRegistry.registerAll(allModules)

第 4 步:添加图标映射

src/modules/icons.ts 中添加:

import { Wrench } from '@lucide/vue'  // 选择合适的图标

export const moduleIconMap: Record<string, Component> = {
  // ... 已有映射
  'my-module': Wrench
}

第 5 步(可选):添加 Rust 后端命令

如果模块需要 Rust 后端支持,在 src-tauri/src/ 中创建命令模块,并在 lib.rsinvoke_handler 中注册。

进程管理

需要管理外部子进程的模块(如 mihomo、aria2)通过 ModuleProcessConfig 声明进程配置。

配置示例

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

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 通知 显示"已禁用 {模块名}" 显示"已启用 {模块名}"

生命周期钩子

interface ModuleLifecycle {
  /** 模块首次加载时调用 */
  onInit?: () => void | Promise<void>
  /** 模块组件挂载时调用(切换到该模块) */
  onActivate?: () => void | Promise<void>
  /** 模块组件卸载时调用(切换离开该模块) */
  onDeactivate?: () => void | Promise<void>
  /** 模块被禁用时调用 */
  onDisable?: () => void | Promise<void>
  /** 模块被启用时调用 */
  onEnable?: () => void | Promise<void>
}

使用示例:

export const moduleConfig: ModuleConfig = {
  // ...
  lifecycle: {
    onEnable: async () => {
      console.log('模块已启用')
      // 初始化资源、建立连接等
    },
    onDisable: async () => {
      console.log('模块已禁用')
      // 释放资源、关闭连接等
    },
    onActivate: () => {
      console.log('用户切换到本模块')
      // 开始实时数据更新等
    },
    onDeactivate: () => {
      console.log('用户离开本模块')
      // 暂停实时更新以节省资源
    }
  }
}

全局搜索

模块通过 searchItems 声明可被全局搜索的功能项。用户在标题栏搜索框输入关键词时,匹配的搜索项会显示在结果列表中。

searchItems: [
  {
    title: '代理设置',           // 显示标题
    description: '配置网络代理',  // 显示描述
    keywords: ['代理', 'proxy', '网络']  // 匹配关键词
  }
]

如果搜索项需要执行特定操作(如跳转到子页面、触发命令),在模块组件挂载时通过 searchStore.registerAction 注册:

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 的模块不可被用户禁用,开关处于禁用状态