🔄 本文是TTS-Web-Vue系列的重要技术更新,重点介绍了项目中API调用的业务分层设计和local-tts-store功能整合到play.ts的优化过程。通过这次重构,我们实现了更清晰的代码分层、更高的复用性和更易于维护的代码结构。
📖 系列文章导航
欢迎查看主页
🔍 重构背景与动机
随着TTS-Web-Vue项目的功能不断扩展,我们遇到了以下挑战:
- API调用逻辑分散:语音合成API调用逻辑散布在多个组件和函数中,导致代码冗余和维护困难
- 业务逻辑与数据访问混杂:业务处理逻辑与底层API调用未明确分离
- 错误处理不一致:不同地方的错误处理逻辑不统一,用户体验不一致
- 状态管理碎片化:local-tts-store与其他状态管理代码分离,增加了代码理解和维护的复杂度
为了解决这些问题,我们决定实施以下重构:
- 建立API调用的清晰分层:将API调用分为数据访问层、业务逻辑层和表现层
- 整合local-tts-store到play.ts:将相关功能合并到统一的文件中,减少代码分散
- 统一错误处理:实现一致的错误处理和恢复策略
- 提高代码复用性:减少重复代码,提高模块化程度
💡 API调用的业务分层设计
分层架构概述
我们采用了经典的三层架构设计模式,并结合Vue.js的特点进行了调整:
- 数据访问层(DAL):直接与外部API交互,负责数据的获取和发送
- 业务逻辑层(BLL):处理业务规则、数据转换和验证逻辑
- 表现层(PL):处理用户界面和交互,调用业务逻辑层
这种分层设计带来的好处是:
- 关注点分离:每层只关注自己的职责
- 可测试性提高:各层可独立测试
- 复用性增强:逻辑可以被多个组件共享
- 维护性改善:修改一层不会影响其他层
实际实现架构
在项目中,我们的实现方式如下:
src/
├── api/ # 数据访问层
│ ├── tts.ts # TTS API调用实现
│ └── local-tts.ts # 本地TTS服务接口
├── store/ # 业务逻辑层
│ ├── play.ts # TTS业务逻辑和状态管理(整合了local-tts-store)
│ └── store.ts # 全局状态管理
└── components/ # 表现层└── main/└── Main.vue # 用户界面组件
🧩 数据访问层实现
数据访问层的主要职责是与外部API交互,处理HTTP请求和响应。我们在tts.ts
中实现了这一层:
// src/api/tts.ts
export async function callTTSApi(params: TTSParams): Promise<TTSResponse> {try {const { api, voiceData, speechKey, region, thirdPartyApi, tts88Key } = params;// 根据不同的 API 类型构建不同的请求 URL 和认证头let apiUrl = '';let headers: Record<string, string> = {'Content-Type': 'application/ssml+xml','X-Microsoft-OutputFormat': 'audio-16khz-128kbitrate-mono-mp3',};if (api === 4) {apiUrl = thirdPartyApi;// TTS88 API 使用 tts88Keyif (tts88Key) {headers['Authorization'] = `Bearer ${tts88Key}`;}} else if (api === 5) {// 导入本地TTS服务相关功能try {const { useFreeTTSstore } = await import('@/store/play');const localTTSStore = useFreeTTSstore();// 获取配置const config = localTTSStore.fullConfig;// 准备API请求的URL和参数apiUrl = `${config.baseUrl}/api/v1/free-tts-stream`;// 获取本地TTS所需的参数const isSSML = voiceData.activeIndex === "1"; // 判断是否为SSML内容// 内容处理逻辑...// 发送请求const response = await axios.post(apiUrl,requestBody,{headers,responseType: 'arraybuffer',timeout: 30000});// 返回二进制音频数据return {buffer: response.data};} catch (localError: any) {return {error: `FreeTTS服务错误: ${localError.message}`,errorCode: "LOCAL_TTS_ERROR"};}} else {// Azure APIapiUrl = `https://${region}.tts.speech.microsoft.com/cognitiveservices/v1`;headers['Ocp-Apim-Subscription-Key'] = speechKey;// 发送请求和处理响应...}// 其他错误处理和返回...} catch (error: any) {// 全局错误处理return {audioContent: '',error: error.message || '获取语音数据失败',errorCode: "GLOBAL_ERROR"};}
}
数据访问层的特点:
- 只关注API交互:不包含业务判断逻辑
- 统一的错误返回格式:各种错误情况都返回标准化的错误对象
- 清晰的接口定义:输入和输出类型明确定义
🔄 业务逻辑层实现
业务逻辑层负责处理业务规则、参数验证和错误处理。我们在play.ts
中实现了这一层,并整合了原来的local-tts-store功能:
// src/store/play.ts
import { createBatchTask, getBatchTaskStatus, deleteBatchTask, callTTSApi } from '@/api/tts';
import * as Pinia from 'pinia';
import { LocalTTSConfig, DEFAULT_LOCAL_TTS_CONFIG, checkServerConnection,getFreeLimitInfo,
} from '@/api/local-tts';// 定义错误类型
export enum FreeTTSErrorType {NONE = 0,QUOTA_EXCEEDED = 402, // 额度用完RATE_LIMITED = 429, // 请求频率限制BANNED = 403, // 被封禁SERVER_ERROR = 500, // 服务器错误CONNECTION_ERROR = -1 // 连接错误
}// 定义freeTTS服务状态和管理 - 整合了原来的local-tts-store
export const useFreeTTSstore = Pinia.defineStore('localTTSStore', {state: () => {return {// 配置config: {enabled: store.get('localTTS.enabled') ?? true,baseUrl: store.get('localTTS.baseUrl') ?? DEFAULT_LOCAL_TTS_CONFIG.baseUrl,defaultVoice: store.get('localTTS.defaultVoice') ?? DEFAULT_LOCAL_TTS_CONFIG.defaultVoice,defaultLanguage: store.get('localTTS.defaultLanguage') ?? DEFAULT_LOCAL_TTS_CONFIG.defaultLanguage,},// 服务器状态serverStatus: {connected: false,lastChecked: null as number | null,freeLimit: null as any | null,error: null as string | null,errorCode: FreeTTSErrorType.NONE},// 当前音频audio: {buffer: null as ArrayBuffer | null,url: null as string | null,isPlaying: false,error: null as string | null,errorCode: FreeTTSErrorType.NONE}};},getters: {// 获取完整配置fullConfig(): LocalTTSConfig {return {...DEFAULT_LOCAL_TTS_CONFIG,...this.config};},// 其他getter...},actions: {// 保存配置saveConfig() {Object.entries(this.config).forEach(([key, value]) => {store.set(`localTTS.${key}`, value);});},// 检查服务器连接async checkServerConnection() {try {const isConnected = await checkServerConnection(this.fullConfig);this.serverStatus.connected = isConnected;this.serverStatus.lastChecked = Date.now();if (isConnected) {this.serverStatus.error = null;this.serverStatus.errorCode = FreeTTSErrorType.NONE;// 如果连接成功,获取可用额度信息await this.getFreeLimitInfo();} else {this.setErrorState('无法连接到服务器', FreeTTSErrorType.CONNECTION_ERROR);}return isConnected;} catch (error: any) {this.setErrorState(`连接错误: ${error.message}`, FreeTTSErrorType.CONNECTION_ERROR);return false;}},// 其他actions...}
});/*** 获取TTS数据 - 业务逻辑层实现* 负责调用底层API,处理重试逻辑和特定的业务转换*/
async function getTTSData(params: TTSParams): Promise<TTSResponse> {const { api, voiceData } = params;const { activeIndex, retryCount = 3, retryInterval = 1 } = voiceData;// 参数验证 - 全面的参数校验if (!voiceData.ssmlContent && !voiceData.inputContent) {console.error('缺少转换内容');return {error: '没有可转换的内容',errorCode: 'EMPTY_CONTENT'};}// API类型相关验证if (api === 1 || api === 2 || api === 3) {// 验证Azure API必要参数if (!params.speechKey) {console.error('缺少 Azure Speech API Key');return {error: '请先在设置中配置 Azure Speech API Key',errorCode: 'MISSING_AZURE_KEY'};}// 其他验证逻辑...}// 处理特定业务逻辑try {// 根据API类型进行不同的预处理if (api === 3) { // Azureconsole.log("使用Azure API");// 可以在这里添加Azure特定的处理逻辑}else if (api === 4) { // TTS88console.log("使用TTS88 API");// 可以在这里添加TTS88特定的处理逻辑}else if (api === 5) { // 本地TTSconsole.log("使用本地TTS服务");// 获取当前选中的声音和配置const { useTtsStore } = await import('@/store/store');const ttsStore = useTtsStore();const selectedVoice = ttsStore.formConfig.voiceSelect;const speed = ttsStore.formConfig.speed;const pitch = ttsStore.formConfig.pitch;console.log("当前选择的声音:", selectedVoice, "语速:", speed, "音调:", pitch);}// 调用API层的函数,并包含重试逻辑let retry = 0;let lastError;while (retry < retryCount) {try {console.log(`尝试调用TTS API (尝试 ${retry + 1}/${retryCount})`);// 确保参数类型兼容const apiParams = {...params,// 确保必要属性不为undefinedspeechKey: params.speechKey || '',region: params.region || '',thirdPartyApi: params.thirdPartyApi || '',tts88Key: params.tts88Key || ''};const result = await callTTSApi(apiParams);// 检查是否有错误if (result.error) {// 错误增强处理// ...throw new Error(result.error);}// 返回结果return result;} catch (error: any) {console.error(`TTS API调用失败 (尝试 ${retry + 1}/${retryCount}):`, error);lastError = error;console.log(`等待 ${retryInterval} 秒后重试...`);await sleep(retryInterval * 1000);retry++;}}// 达到最大重试次数return {error: lastError?.message || "达到最大重试次数,请求失败",errorCode: "MAX_RETRY_EXCEEDED"};} catch (error: any) {console.error("TTS转换失败:", error);return {error: error.message || "TTS转换失败",errorCode: "GENERAL_ERROR"};}
}// 导出供组件使用的函数
export { getTTSData };
业务逻辑层的特点:
- 职责清晰:负责业务规则和参数验证,而不是API调用细节
- 错误增强:提供更友好、更具体的错误信息
- 重试机制:实现了自动重试逻辑
- 状态管理:整合了之前分散的状态管理功能
🔀 整合local-tts-store到play.ts
在重构过程中,我们将原来独立的local-tts-store.ts
文件整合到了play.ts
中,这样做的好处是:
- 减少文件数量:相关功能集中在一个文件中
- 避免循环依赖:解决了之前可能存在的循环引用问题
- 逻辑集中:所有与TTS播放相关的逻辑都在同一个地方
整合过程的主要工作包括:
- 将
local-tts-store.ts
中的类型定义、状态和方法移动到play.ts
- 重新组织和优化代码结构,确保逻辑流程清晰
- 更新所有引用
local-tts-store.ts
的地方,指向新的位置 - 优化错误处理和状态管理逻辑
🌟 表现层实现
表现层(即组件层)通过调用业务逻辑层的函数来完成功能,而不直接与API交互:
// 在组件中使用getTTSData函数
import { getTTSData } from '@/store/play';// 在组件方法中
async function startBtn() {// 准备参数const params = {api: formConfig.value.api,voiceData: {activeIndex: tabsValue.value,ssmlContent: ssmlContent.value,inputContent: inputs.value.content},speechKey: config.value.key,region: config.value.region,thirdPartyApi: config.value.thirdPartyApi,tts88Key: config.value.tts88Key};// 调用业务逻辑层函数const result = await getTTSData(params);// 处理结果if (result.error) {// 显示错误信息ElMessage.error(result.error);} else {// 处理成功结果handleAudioBlob(result.buffer);}
}
表现层的特点:
- 专注于用户交互:只关注UI渲染和事件处理
- 调用业务逻辑层:不直接进行API调用
- 处理显示逻辑:负责向用户展示结果或错误信息
📊 分层架构的优势
通过实施API调用业务分层,我们获得了以下优势:
- 代码组织更清晰:每一层都有明确的职责
- 复用性提高:业务逻辑可以被多个组件重用
- 测试变得简单:可以独立测试每一层
- 错误处理更一致:统一的错误处理策略
- 易于扩展:添加新功能时可以只修改相关层
- 维护成本降低:修改一个层的实现不会影响其他层
🔍 代码质量提升
错误处理统一化
重构后,我们实现了更统一的错误处理机制:
// 错误类型定义
export enum FreeTTSErrorType {NONE = 0,QUOTA_EXCEEDED = 402, // 额度用完RATE_LIMITED = 429, // 请求频率限制BANNED = 403, // 被封禁SERVER_ERROR = 500, // 服务器错误CONNECTION_ERROR = -1 // 连接错误
}// 错误处理函数
function getErrorCodeFromResponse(error) {if (!error || !error.response) {return FreeTTSErrorType.CONNECTION_ERROR;}const status = error.response.status;if (status === 402 || status === 403) {return FreeTTSErrorType.QUOTA_EXCEEDED;} else if (status === 429) {return FreeTTSErrorType.RATE_LIMITED;} else if (status >= 500) {return FreeTTSErrorType.SERVER_ERROR;}return FreeTTSErrorType.SERVER_ERROR;
}
代码复用性提高
通过将业务逻辑集中在getTTSData
函数中,我们减少了代码重复:
// 任何需要TTS功能的组件都可以直接调用此函数
import { getTTSData } from '@/store/play';// 在不同组件中使用相同的逻辑
const result = await getTTSData(params);
🚀 性能优化
动态导入优化
为了避免不必要的依赖加载,我们使用了动态导入:
// 只在需要时动态导入
if (api === 5) { // 本地TTSconst { useTtsStore } = await import('@/store/store');const ttsStore = useTtsStore();// 使用ttsStore...
}
缓存与重用
我们增加了结果缓存机制,避免重复的API调用:
// 简单的结果缓存实现
const resultCache = new Map();function getCacheKey(params) {// 生成缓存键...
}async function getTTSDataWithCache(params) {const cacheKey = getCacheKey(params);// 检查缓存if (resultCache.has(cacheKey)) {return resultCache.get(cacheKey);}// 调用API获取结果const result = await getTTSData(params);// 缓存结果if (!result.error) {resultCache.set(cacheKey, result);}return result;
}
🔮 未来展望
基于当前的分层架构,我们计划在未来实施以下优化:
- 更细粒度的分层:将业务逻辑层进一步拆分为多个专门的服务
- 更完善的错误恢复机制:自动尝试不同的API和参数组合
- 服务质量监控:添加性能和可用性监控
- 缓存优化:实现更智能的缓存策略
- 接口标准化:统一所有API的请求和响应格式
🎯 总结
通过实施API调用业务分层和整合local-tts-store到play.ts,我们实现了代码结构的显著优化:
- 关注点分离:清晰的分层架构使每部分代码都有明确的职责
- 代码复用性提高:业务逻辑可以被多个组件共享和重用
- 错误处理更一致:统一的错误处理机制提供了更好的用户体验
- 代码可维护性增强:修改一层的实现不会影响其他层
- 状态管理统一:整合相关功能减少了状态管理的复杂度
这些优化不仅提升了当前项目的代码质量,也为未来的功能扩展和维护提供了坚实的基础。我们推荐在类似的前端项目中采用这种分层架构设计,尤其是当项目规模增长到一定程度时。
🔗 相关链接
- Vue.js官方文档
- Pinia状态管理
- TypeScript文档
- TTS-Web-Vue项目主页
- 在线演示
注意:本文介绍的架构设计思路仅供学习和参考,具体实践时应根据项目特点进行调整。如有问题或建议,欢迎在评论区讨论!