第15篇:网络请求封装

引言

柚兔学伴需要与多个后端服务交互——Coze 智能体 API、火山引擎 TTS 语音合成、百度翻译、汉字字典等。每个服务的域名、请求格式、响应结构各不相同。项目通过 HttpManagerHttpRequest 两层架构,实现了统一的网络请求封装,让上层 Model 层只需关注业务参数,无需关心底层 HTTP 细节。

HttpManager 单例

HttpManager 作为网络请求的统一入口,采用单例模式确保全局唯一:

// network/src/main/ets/HttpManager.ets
export class HttpManager {
  private static mInstance: HttpManager;
  private BASE_URL: string = UrlConstants.SERVER

  private constructor() {
  }

  static getInstance(): HttpManager {
    if (!HttpManager.mInstance) {
      HttpManager.mInstance = new HttpManager();
    }
    return HttpManager.mInstance;
  }

  setBaseUrl(url: string) {
    this.BASE_URL = url
  }
}
  • 私有构造函数:防止外部 new 实例化
  • 懒加载单例:首次调用 getInstance() 时创建
  • 可配置 BASE_URL:通过 setBaseUrl 支持动态切换服务器地址
  • 默认域名UrlConstants.SERVERhttps://api.coze.cn/

RequestOptions 接口

所有请求方法共享统一的参数接口:

export interface RequestOptions {
  domain?: string;                    // 自定义域名(可选,默认 BASE_URL)
  url: string;                        // 请求路径
  queryParams?: Record<string, string>; // URL 查询参数
  postBody?: object;                  // POST 请求体
  header?: Record<string, string>;    // 自定义请求头
  multiFormDataList?: Array<http.MultiFormData>; // 文件上传的表单数据
}

这个接口的设计哲学是"一个接口覆盖所有场景"——通过可选字段适配不同类型的请求,而非为每种请求定义独立参数类型。

通用响应模型

所有 Coze API 的响应都遵循统一结构:

// network/src/main/ets/common/CommonResponseModel.ets
export interface CommonResponseModel<T> {
  code: number
  desc: string
  msg: string
  data: T
  result: T
  transparent: number;
  success: boolean;
}

泛型 T 代表具体的业务数据类型,code 字段用于判断请求是否成功。Coze API 的成功码为 0

static readonly CODE_SUCCESS: number = 0

requestPost:键值对参数 POST

async requestPost<T>(option: RequestOptions): Promise<T> {
  if (option.queryParams == null) {
    option.queryParams = {}
  }
  let request = new PostRequest<T>(option.queryParams!!, option.url);
  request.domain = option.domain ? option.domain : this.BASE_URL
  return new Promise<T>((resolve, reject) => {
    request.execute().then((data: CommonResponseModel<T>) => {
      if (data.code === UrlConstants.CODE_SUCCESS) {
        resolve(data.data);
      } else {
        reject(data.desc);
      }
    }).catch((err: Error) => {
      reject('请求失败');
      ToastUtil.showToast('请求失败')
    });
  })
}

使用场景:当请求参数为简单的键值对时(如 queryParams),使用 PostRequest 将参数作为 postBody 发送。成功时 resolve(data.data),只返回业务数据;失败时 reject(data.desc) 返回错误描述。

requestPostBody:对象参数 POST

requestPostBody<T>(option: RequestOptions): Promise<T> {
  let request = new PostBodyRequest<T>(option.postBody!!, option.url);
  request.domain = this.BASE_URL
  return new Promise<T>((resolve, reject) => {
    request.execute().then((data: CommonResponseModel<T>) => {
      if (data.code === UrlConstants.CODE_SUCCESS) {
        resolve(data.data);
      } else {
        reject(data.desc);
      }
    }).catch((err: Error) => {
      reject('请求失败');
      ToastUtil.showToast(err.message)
    });
  })
}

requestPost 的区别:requestPostBody 传递的是完整的 object 对象作为请求体,适用于结构化参数(如创建会话的 CreatParam)。这是项目中最常用的方法,ChatModel 中的 conversationCreatechat 均使用此方法。

requestTtsPostBody:TTS 专用请求

requestTtsPostBody<T>(option: RequestOptions): Promise<T> {
  let request = new PostBodyRequest<T>(option.postBody!!, option.url, option.header);
  request.domain = UrlConstants.TTS_DOMAIN_URL

  return new Promise<T>((resolve, reject) => {
    request.execute().then((data: CommonResponseModel<T>) => {
      if (data.code === UrlConstants.CODE_TTS_SUCCESS || data.code === 200) {
        resolve(data.data);
      } else {
        reject(data.desc);
      }
    }).catch((err: Error) => {
      reject('请求失败');
    });
  })
}

TTS 请求的特殊之处:

  • 独立域名UrlConstants.TTS_DOMAIN_URLhttps://openspeech.bytedance.com/api/),不走 Coze 服务器
  • 自定义请求头:TTS API 要求特定的 Authorization 格式
  • 不同的成功码CODE_TTS_SUCCESS = 3000,同时兼容 200

requestSpeechPostBody:语音识别请求

requestSpeechPostBody<T>(option: RequestOptions): Promise<T> {
  let request = new PostTRequest<T>(option.postBody!!, option.url, option.header);
  request.domain = option.domain ? option.domain : UrlConstants.TTS_DOMAIN_URL

  return new Promise<T>((resolve, reject) => {
    request.execute().then((data: T) => {
      resolve(data);
    }).catch((err: Error) => {
      reject('请求失败');
    });
  })
}

语音识别请求的独特之处在于直接返回原始数据 T,而非包裹在 CommonResponseModel 中。这是因为语音识别 API 的响应结构不同于 Coze,不走统一的 code/data 包装。它使用 PostTRequest 而非 PostBodyRequest,泛型绑定类型不同。

requestGet:GET 请求

requestGet<T>(option: RequestOptions): Promise<T> {
  let request = new GetRequest<T>(option.url, option.queryParams!!);
  request.domain = this.BASE_URL
  return new Promise<T>((resolve, reject) => {
    request.execute().then((data: CommonResponseModel<T>) => {
      if (data.code === UrlConstants.CODE_SUCCESS) {
        resolve(data.data);
      } else {
        reject(data.msg);
      }
    }).catch((err: Error) => {
      reject('请求失败');
      ToastUtil.showToast(err.message)
    });
  })
}

GET 请求将参数放入 queryParams,在 URL 中拼接。用于 retrieve(查看对话详情)和 messageList(查看消息列表)等查询接口。注意失败时使用 data.msg 而非 data.desc,因为 GET 接口的错误信息字段名不同。

uploadFiles:文件上传

uploadFiles<T>(option: RequestOptions): Promise<T> {
  let request = new PostMultipartRequest<T>(option.url, option.multiFormDataList!!);
  request.domain = this.BASE_URL
  return new Promise<T>((resolve, reject) => {
    request.execute().then((data: CommonResponseModel<T>) => {
      if (data.code === UrlConstants.CODE_SUCCESS) {
        resolve(data.data);
      } else {
        reject(data.desc);
      }
    }).catch((err: Error) => {
      reject('请求失败');
      ToastUtil.showToast(err.message)
    });
  })
}

文件上传使用 PostMultipartRequest,发送 multipart/form-data 格式请求。在 ChatModel 中用于上传录音文件:

uploadFile(cacheFilePath: string, completeCallback: CompleteCallback) {
  let cloudPath = 'voice/' + cacheFilePath.split('/').pop() as string;
  bucket.uploadFile(getContext(this), {
    localPath: cacheFilePath,
    cloudPath: cloudPath,
  }).then(task => {
    this.addEventListener(task, this.onUploadCompleted(cloudPath, cacheFilePath, completeCallback));
    task.start();
  })
}

注意:文件上传实际使用的是 AGC 云存储 SDK(cloudStorage),而非 HttpManager.uploadFilesuploadFiles 方法为其他文件上传场景预留。

HttpRequest 请求类体系

HttpRequest.ets 定义了五种请求类,均继承自 HttpRequest 基类:

请求类方法参数类型Content-Type
GetRequestGETqueryParamsapplication/json
PostRequestPOSTqueryParams(作为 body)application/json
PostBodyRequestPOSTobjectapplication/json
PostTRequestPOSTobjectapplication/json
PostMultipartRequestPOSTMultiFormData[]multipart/form-data

所有 Coze 相关请求类自动注入认证头:

public header: Record<string, string> = {
  'Content-Type': 'application/json',
  'Authorization': `Bearer ${UrlConstants.COZE_SECRET_TOKEN}`,
}

COZE_SECRET_TOKEN 是 Coze API 的 Service Account Token,以 sat_ 开头,用于服务端对服务端的认证。

UrlConstants 常量管理

// network/src/main/ets/common/UrlConstants.ets
export class UrlConstants {
  static readonly SERVER: string = 'https://api.coze.cn/'
  static readonly CONVERSATION_CREATE_URL = 'v1/conversation/create'
  static readonly GET_ONLINE_INFO_URL = 'v1/bot/get_online_info'
  static readonly CHAT_URL = 'v3/chat'
  static readonly RETRIEVE_URL = 'v3/chat/retrieve'
  static readonly MSG_LIST_URL = 'v3/chat/message/list'

  static readonly TTS_DOMAIN_URL = 'https://openspeech.bytedance.com/api/'
  static readonly TTS_URL = 'v1/tts'
  static readonly VOICE_RECOGNIZE_URL = 'v3/auc/bigmodel/submit'
  static readonly VOICE_QUERY_URL = 'v3/auc/bigmodel/query'

  static readonly CODE_SUCCESS: number = 0
  static readonly CODE_TTS_SUCCESS: number = 3000
}

所有 API 路径和状态码集中管理,避免硬编码散落在各处。URL 只存储相对路径,域名通过 domain 字段在运行时拼接。

错误处理策略

HttpManager 采用了分层的错误处理:

  1. 业务错误code !== CODE_SUCCESS):reject(data.desc)reject(data.msg),将服务端错误描述传给调用方
  2. 网络异常(catch 分支):reject('请求失败'),并调用 ToastUtil.showToast 直接提示用户
  3. 调用方处理:Model 层通过 .catch() 捕获 reject 值,设置 LoadingStatus.FAILED
.catch((err: BusinessError) => {
  this.loadingStatus = LoadingStatus.FAILED
  return this.loadingStatus
});

这种三层设计确保了:网络层统一 Toast 提示、业务层获取具体错误信息、Model 层更新状态供 UI 响应。

方法选择指南

场景推荐方法示例
简单键值对 POSTrequestPost
对象参数 POSTrequestPostBodyconversationCreate、chat
TTS 语音合成requestTtsPostBodyttsMaker
语音识别requestSpeechPostBodyvoiceRecognition
查询接口requestGetretrieve、messageList
文件上传uploadFiles录音文件上传

小结

本篇详细介绍了柚兔学伴的网络请求封装架构:

  • HttpManager 单例:统一入口,私有构造,懒加载,可配置域名
  • RequestOptions 统一参数:一个接口覆盖所有请求类型,通过可选字段适配
  • 六种请求方法:针对不同场景(键值对、对象体、TTS、语音、GET、上传)提供专门方法
  • CommonResponseModel 泛型:统一响应解析,code 判断成功,data 返回业务数据
  • 自动认证注入:所有 Coze 请求自动携带 Service Account Token
  • 三层错误处理:Toast 提示 + reject 传递 + Model 状态更新
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐