🔄 本文是TTS-Web-Vue系列的重要更新,重点介绍了项目的系统重构过程,特别是将Main.vue和MainOptions.vue中的TS代码分离到独立脚本文件中的设计思路和实现方案。通过这种架构优化,我们显著提升了代码的可维护性、可测试性和复用性。
📖 系列文章导航
查看主页
🔍 背景与重构动机
在Web应用开发过程中,随着功能的不断增加,组件文件往往会变得越来越臃肿。TTS-Web-Vue项目也不例外,尤其是核心组件Main.vue
和MainOptions.vue
:
- 代码膨胀:大量业务逻辑和状态管理代码混杂在组件文件中
- 可维护性降低:单文件几百甚至上千行代码,难以定位和修改特定功能
- 复用性差:组件内的功能难以在其他组件中重用
- 测试困难:组件和业务逻辑紧密耦合,单元测试变得复杂
为了解决这些问题,我们决定进行一次深度重构,主要目标是将组件中的TypeScript逻辑代码分离到独立的脚本文件中,实现关注点分离和逻辑复用。
💡 重构设计思路
架构设计原则
在重构设计过程中,我们遵循以下关键原则:
- 关注点分离:将UI渲染与业务逻辑彻底分开
- 组合式API优先:充分利用Vue3的组合式API(Composition API)特性
- 模块化设计:将相关功能聚合到独立模块中
- 类型安全:保持TypeScript类型定义的一致性和安全性
- 向后兼容:确保重构不破坏现有功能
- 渐进式迁移:允许新旧代码共存,渐进式完成迁移
文件结构设计
重构后的文件结构更加清晰:
src/
├── components/
│ ├── main/
│ │ ├── Main.vue // 只保留模板和样式
│ │ └── MainOptions.vue // 只保留模板和样式
│ └── ...
├── composables/ // 新增的组合式函数目录
│ ├── main.ts // 从Main.vue提取的逻辑
│ ├── main-option.ts // 从MainOptions.vue提取的逻辑
│ └── ...
└── ...
技术选型
- Composition API :使用
ref
、reactive
、computed
和watch
等响应式API - 组合式函数(Composables):将逻辑封装为可复用的函数
- TypeScript接口:为所有导出的函数和返回值定义清晰的接口
- 动态导入:使用动态导入解决循环依赖问题
🧩 核心实现方案
1. 提取组件逻辑到独立文件
从Main.vue
中提取逻辑到main.ts
:
typescript
// src/composables/main.ts
import { ref, watch, onMounted, nextTick, onUnmounted, reactive } from "vue";
import { useTtsStore } from "@/store/store";
import { useFreeTTSstore, FreeTTSErrorType } from "@/store/play";
import { storeToRefs } from "pinia";
import { getChineseName } from '@/voice-utils';
// 其他导入...
// 全局store实例,用于composables函数中访问
let globalTtsStore = null;
let globalInputs = ref({});
let globalPage = ref({});
let globalFormConfig = ref({});
let globalConfig = ref({});
// 初始化全局引用
const initGlobalRefs = () => {
// 初始化全局store
if (!globalTtsStore) {
globalTtsStore = useTtsStore();
const { inputs, page, formConfig, config } = storeToRefs(globalTtsStore);
globalInputs = inputs;
globalPage = page;
globalFormConfig = formConfig;
globalConfig = config;
}
};
// 主要的setup函数,供组件使用
function useMainSetup() {
const ttsStore = useTtsStore();
initGlobalRefs();
// 具体的业务逻辑实现...
return {
// 返回组件需要的状态和方法
};
}
// 导出所有需要的变量和函数
export {
// 组件和库
MainOptions,
VoiceSelector,
ConfigPage,
Loading,
FixedHeader,
FreeTTSErrorDisplay,
ElMessage,
ElMessageBox,
WebStore,
// 图标
MagicStick,
ChatLineSquare,
// 其他图标...
// 状态和引用
t,
useMainSetup,
docIframe,
iframeLoaded,
iframeError,
iframeCurrentSrc,
// 其他状态
isSSMLMode,
isLoading,
// 其他状态...
// 函数
getChineseName,
tryAlternativeUrl,
handleIframeLoad,
// 其他函数...
};
同样,从MainOptions.vue
中提取逻辑到main-option.ts
:
typescript
// src/composables/main-option.ts
import { ref, reactive, watch, onMounted, computed } from "vue";
import { optionsConfig as oc } from "@/components/main/options-config";
import { getStyleDes, getRoleDes } from "@/components/main/emoji-config";
import { ElMessage, ElMessageBox } from "element-plus";
// 其他导入...
// 定义返回类型接口
interface MainOptionsReturn {
formConfig: any;
localTTSStore: ReturnType<typeof useFreeTTSstore>;
oc: any;
t: any;
presets: any[];
currentPreset: any;
applyPreset: (presetId: string) => void;
// 其他属性和方法...
}
export function useMainOptions(props = { inDrawer: false }, emit?: any): MainOptionsReturn {
const { t } = useI18n();
const ttsStore = useTtsStore();
const localTTSStore = useFreeTTSstore();
const { page, inputs, tableData, isLoading } = storeToRefs(ttsStore);
const { formConfig, config } = storeToRefs(ttsStore);
const webstore = new WebStore();
// 控制弹出框显示
const showAdvancedSettings = ref(false);
// 添加展开/收起状态控制
const showFreeTTSInfo = ref(true);
// 具体的业务逻辑实现...
return {
formConfig,
localTTSStore,
oc,
t,
presets,
currentPreset,
applyPreset,
// 其他返回值...
};
}
2. 组件中使用提取的逻辑
重构后的Main.vue
变得简洁明了:
vue
<template>
<!-- 模板代码保持不变 -->
</template>
<script setup lang="ts">
// 导入所有需要的内容
import {
// 组件和库
MainOptions,
VoiceSelector,
ConfigPage,
Loading,
FixedHeader,
FreeTTSErrorDisplay,
ElMessage,
ElMessageBox,
WebStore,
// 图标
MagicStick,
ChatLineSquare,
// 其他导入...
// 状态和引用
t,
useMainSetup,
docIframe,
iframeLoaded,
iframeError,
// 其他状态和函数...
} from '@/composables/main';
// 使用setup函数
const mainSetup = useMainSetup();
</script>
<style>
/* 样式代码保持不变 */
</style>
同样,MainOptions.vue
也变得更加简洁:
vue
<template>
<!-- 模板代码保持不变 -->
</template>
<script setup lang="ts">
import { useMainOptions } from '@/composables/main-option';
// 接收props
const props = defineProps({
inDrawer: {
type: Boolean,
default: false
}
});
// 使用提取的逻辑
const {
formConfig,
localTTSStore,
oc,
t,
presets,
currentPreset,
applyPreset,
// 其他需要的变量和函数...
} = useMainOptions(props);
</script>
<style scoped>
/* 样式代码保持不变 */
</style>
3. 处理循环依赖问题
在重构过程中,我们发现一个棘手的问题:部分组件和逻辑之间存在循环依赖。我们通过以下策略解决:
typescript
// 使用动态导入替代静态导入
const MainOptions = defineAsyncComponent(() => import("../components/main/MainOptions.vue"));
// 在需要时动态导入store
async function someFunction() {
const { useTtsStore } = await import('@/store/store');
const ttsStore = useTtsStore();
// 使用ttsStore...
}
🌟 重构效果与收益
代码量对比
重构前后的代码量对比:
文件 | 重构前行数 | 重构后行数 | 减少比例 |
---|---|---|---|
Main.vue | 1264 | 450 | 64.4% |
MainOptions.vue | 978 | 380 | 61.1% |
主要收益
-
关注点分离:
- UI层只关注渲染和用户交互
- 业务逻辑集中在专门的脚本文件中
- 数据流更加清晰可控
-
代码可维护性提升:
- 功能模块化,易于定位和修改
- 减少了组件文件的复杂度
- 更好的代码组织和文档化
-
逻辑复用性增强:
- 业务逻辑可在多个组件间共享
- 减少代码重复
- 统一的状态管理和业务处理
-
测试友好:
- 业务逻辑可独立测试,不依赖UI
- 更容易模拟依赖和测试边缘情况
- 提高了代码的可测试性
-
性能优化:
- 按需加载和树摇优化
- 减少不必要的组件重渲染
- 更好的内存管理
🔧 实施过程中的挑战与解决方案
1. 循环依赖问题
挑战:组件之间的相互引用导致循环依赖。
解决方案:
- 使用动态导入(Dynamic Import)
- 重新设计数据流,减少组件间的直接依赖
- 引入中间层统一管理状态
2. 状态共享问题
挑战:提取逻辑后,多个组件需要共享状态。
解决方案:
- 统一使用Pinia进行状态管理
- 使用全局引用变量保存关键状态
- 实现
initGlobalRefs
函数确保引用正确初始化
3. 类型定义挑战
挑战:跨文件共享类型定义复杂。
解决方案:
- 创建统一的类型定义文件
- 为导出函数定义明确的接口
- 使用TypeScript的泛型和工具类型
4. 向后兼容性
挑战:确保重构不破坏现有功能。
解决方案:
- 渐进式迁移,而非一次性重写
- 编写详细的单元测试
- 在每个阶段进行充分的回归测试
📊 性能与加载优化
代码分割与懒加载
重构后,我们实现了更细粒度的代码分割:
javascript
// 动态导入组件
const MainOptions = defineAsyncComponent(() => import("../components/main/MainOptions.vue"));
按需加载优化
对于不是所有页面都需要的功能,我们实现了按需加载:
javascript
// 只在需要时导入特定功能
const handleAdvancedFeature = async () => {
const { processAdvancedData } = await import('./advanced-feature');
await processAdvancedData();
};
🔮 未来展望与最佳实践
组件设计最佳实践
基于此次重构经验,我们总结了以下Vue3组件设计最佳实践:
- 提前分离业务逻辑:在组件变大之前就考虑逻辑分离
- 使用组合式函数:将可复用逻辑封装为组合式函数
- 单一职责原则:每个组件和函数只负责一个功能点
- 类型优先设计:先定义接口和类型,再实现功能
- 状态管理分层:区分UI状态和业务状态,分别管理
未来优化方向
- 进一步模块化:将main.ts和main-option.ts拆分为更小的功能模块
- 自动化测试:为分离的业务逻辑编写全面的单元测试
- 文档生成:基于TypeScript类型自动生成API文档
- 性能监控:添加性能监控,量化重构带来的性能提升
🎯 总结
本次重构是TTS-Web-Vue项目发展的关键里程碑,通过将组件逻辑分离到独立文件,我们不仅解决了代码膨胀的问题,还大幅提升了代码的可维护性、可测试性和复用性。
重构过程遵循了Vue3的最佳实践,充分利用了组合式API的优势,形成了一套可持续发展的代码架构。这种架构不仅使当前功能更加稳定可靠,也为未来功能的扩展奠定了坚实基础。
对于任何中大型Vue项目,我们都推荐采用类似的逻辑分离方法,以确保项目在长期发展过程中保持健康和高效。
🔗 相关链接
注意:本文介绍的重构方法仅供学习和参考,具体实践时应根据项目特点进行调整。如有问题或建议,欢迎在评论区讨论!