🔄 本文是TTS-Web-Vue系列的重要技术更新,重点介绍了项目中API调用的业务分层设计和local-tts-store功能整合到play.ts的优化过程。通过这次重构,我们实现了更清晰的代码分层、更高的复用性和更易于维护的代码结构。

📖 系列文章导航

欢迎查看主页

🔍 重构背景与动机

随着TTS-Web-Vue项目的功能不断扩展,我们遇到了以下挑战:

  1. API调用逻辑分散:语音合成API调用逻辑散布在多个组件和函数中,导致代码冗余和维护困难
  2. 业务逻辑与数据访问混杂:业务处理逻辑与底层API调用未明确分离
  3. 错误处理不一致:不同地方的错误处理逻辑不统一,用户体验不一致
  4. 状态管理碎片化:local-tts-store与其他状态管理代码分离,增加了代码理解和维护的复杂度

为了解决这些问题,我们决定实施以下重构:

  1. 建立API调用的清晰分层:将API调用分为数据访问层、业务逻辑层和表现层
  2. 整合local-tts-store到play.ts:将相关功能合并到统一的文件中,减少代码分散
  3. 统一错误处理:实现一致的错误处理和恢复策略
  4. 提高代码复用性:减少重复代码,提高模块化程度

💡 API调用的业务分层设计

分层架构概述

我们采用了经典的三层架构设计模式,并结合Vue.js的特点进行了调整:

  1. 数据访问层(DAL):直接与外部API交互,负责数据的获取和发送
  2. 业务逻辑层(BLL):处理业务规则、数据转换和验证逻辑
  3. 表现层(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 使用 tts88Key
      if (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 API
      apiUrl = `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"
    };
  }
}

数据访问层的特点:

  1. 只关注API交互:不包含业务判断逻辑
  2. 统一的错误返回格式:各种错误情况都返回标准化的错误对象
  3. 清晰的接口定义:输入和输出类型明确定义

🔄 业务逻辑层实现

业务逻辑层负责处理业务规则、参数验证和错误处理。我们在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) { // Azure
      console.log("使用Azure API");
      // 可以在这里添加Azure特定的处理逻辑
    }
    else if (api === 4) { // TTS88
      console.log("使用TTS88 API");
      // 可以在这里添加TTS88特定的处理逻辑
    }
    else if (api === 5) { // 本地TTS
      console.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,
          // 确保必要属性不为undefined
          speechKey: 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 };

业务逻辑层的特点:

  1. 职责清晰:负责业务规则和参数验证,而不是API调用细节
  2. 错误增强:提供更友好、更具体的错误信息
  3. 重试机制:实现了自动重试逻辑
  4. 状态管理:整合了之前分散的状态管理功能

🔀 整合local-tts-store到play.ts

在重构过程中,我们将原来独立的local-tts-store.ts文件整合到了play.ts中,这样做的好处是:

  1. 减少文件数量:相关功能集中在一个文件中
  2. 避免循环依赖:解决了之前可能存在的循环引用问题
  3. 逻辑集中:所有与TTS播放相关的逻辑都在同一个地方

整合过程的主要工作包括:

  1. local-tts-store.ts中的类型定义、状态和方法移动到play.ts
  2. 重新组织和优化代码结构,确保逻辑流程清晰
  3. 更新所有引用local-tts-store.ts的地方,指向新的位置
  4. 优化错误处理和状态管理逻辑

🌟 表现层实现

表现层(即组件层)通过调用业务逻辑层的函数来完成功能,而不直接与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);
  }
}

表现层的特点:

  1. 专注于用户交互:只关注UI渲染和事件处理
  2. 调用业务逻辑层:不直接进行API调用
  3. 处理显示逻辑:负责向用户展示结果或错误信息

📊 分层架构的优势

通过实施API调用业务分层,我们获得了以下优势:

  1. 代码组织更清晰:每一层都有明确的职责
  2. 复用性提高:业务逻辑可以被多个组件重用
  3. 测试变得简单:可以独立测试每一层
  4. 错误处理更一致:统一的错误处理策略
  5. 易于扩展:添加新功能时可以只修改相关层
  6. 维护成本降低:修改一个层的实现不会影响其他层

🔍 代码质量提升

错误处理统一化

重构后,我们实现了更统一的错误处理机制:

// 错误类型定义
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) { // 本地TTS
  const { 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;
}

🔮 未来展望

基于当前的分层架构,我们计划在未来实施以下优化:

  1. 更细粒度的分层:将业务逻辑层进一步拆分为多个专门的服务
  2. 更完善的错误恢复机制:自动尝试不同的API和参数组合
  3. 服务质量监控:添加性能和可用性监控
  4. 缓存优化:实现更智能的缓存策略
  5. 接口标准化:统一所有API的请求和响应格式

🎯 总结

通过实施API调用业务分层和整合local-tts-store到play.ts,我们实现了代码结构的显著优化:

  1. 关注点分离:清晰的分层架构使每部分代码都有明确的职责
  2. 代码复用性提高:业务逻辑可以被多个组件共享和重用
  3. 错误处理更一致:统一的错误处理机制提供了更好的用户体验
  4. 代码可维护性增强:修改一层的实现不会影响其他层
  5. 状态管理统一:整合相关功能减少了状态管理的复杂度

这些优化不仅提升了当前项目的代码质量,也为未来的功能扩展和维护提供了坚实的基础。我们推荐在类似的前端项目中采用这种分层架构设计,尤其是当项目规模增长到一定程度时。

🔗 相关链接

注意:本文介绍的架构设计思路仅供学习和参考,具体实践时应根据项目特点进行调整。如有问题或建议,欢迎在评论区讨论!

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐