第15篇:网络请求封装
引言
柚兔学伴需要与多个后端服务交互——Coze 智能体 API、火山引擎 TTS 语音合成、百度翻译、汉字字典等。每个服务的域名、请求格式、响应结构各不相同。项目通过HttpManager和HttpRequest两层架构,实现了统一的网络请求封装,让上层 Model 层只需关注业务参数,无需关心底层 HTTP 细节。
HttpManager 单例
HttpManager作为网络请求的统一入口,采用单例模式确保全局唯一:
// network/src/main/ets/HttpManager.etsexportclassHttpManager{privatestaticmInstance:HttpManager;privateBASE_URL:string=UrlConstants.SERVERprivateconstructor(){}staticgetInstance():HttpManager{if(!HttpManager.mInstance){HttpManager.mInstance=newHttpManager();}returnHttpManager.mInstance;}setBaseUrl(url:string){this.BASE_URL=url}}- 私有构造函数:防止外部
new实例化 - 懒加载单例:首次调用
getInstance()时创建 - 可配置 BASE_URL:通过
setBaseUrl支持动态切换服务器地址 - 默认域名:
UrlConstants.SERVER即https://api.coze.cn/
RequestOptions 接口
所有请求方法共享统一的参数接口:
exportinterfaceRequestOptions{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.etsexportinterfaceCommonResponseModel<T>{code:numberdesc:stringmsg:stringdata:Tresult:Ttransparent:number;success:boolean;}泛型T代表具体的业务数据类型,code字段用于判断请求是否成功。Coze API 的成功码为0:
staticreadonlyCODE_SUCCESS:number=0requestPost:键值对参数 POST
asyncrequestPost<T>(option:RequestOptions):Promise<T>{if(option.queryParams==null){option.queryParams={}}letrequest=newPostRequest<T>(option.queryParams!!,option.url);request.domain=option.domain?option.domain:this.BASE_URLreturnnewPromise<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>{letrequest=newPostBodyRequest<T>(option.postBody!!,option.url);request.domain=this.BASE_URLreturnnewPromise<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 中的conversationCreate、chat均使用此方法。
requestTtsPostBody:TTS 专用请求
requestTtsPostBody<T>(option:RequestOptions):Promise<T>{letrequest=newPostBodyRequest<T>(option.postBody!!,option.url,option.header);request.domain=UrlConstants.TTS_DOMAIN_URLreturnnewPromise<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_URL(https://openspeech.bytedance.com/api/),不走 Coze 服务器 - 自定义请求头:TTS API 要求特定的
Authorization格式 - 不同的成功码:
CODE_TTS_SUCCESS = 3000,同时兼容200
requestSpeechPostBody:语音识别请求
requestSpeechPostBody<T>(option:RequestOptions):Promise<T>{letrequest=newPostTRequest<T>(option.postBody!!,option.url,option.header);request.domain=option.domain?option.domain:UrlConstants.TTS_DOMAIN_URLreturnnewPromise<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>{letrequest=newGetRequest<T>(option.url,option.queryParams!!);request.domain=this.BASE_URLreturnnewPromise<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>{letrequest=newPostMultipartRequest<T>(option.url,option.multiFormDataList!!);request.domain=this.BASE_URLreturnnewPromise<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){letcloudPath='voice/'+cacheFilePath.split('/').pop()asstring;bucket.uploadFile(getContext(this),{localPath:cacheFilePath,cloudPath:cloudPath,}).then(task=>{this.addEventListener(task,this.onUploadCompleted(cloudPath,cacheFilePath,completeCallback));task.start();})}注意:文件上传实际使用的是 AGC 云存储 SDK(cloudStorage),而非HttpManager.uploadFiles。uploadFiles方法为其他文件上传场景预留。
HttpRequest 请求类体系
HttpRequest.ets定义了五种请求类,均继承自HttpRequest基类:
| 请求类 | 方法 | 参数类型 | Content-Type |
|---|---|---|---|
GetRequest | GET | queryParams | application/json |
PostRequest | POST | queryParams(作为 body) | application/json |
PostBodyRequest | POST | object | application/json |
PostTRequest | POST | object | application/json |
PostMultipartRequest | POST | MultiFormData[] | multipart/form-data |
所有 Coze 相关请求类自动注入认证头:
publicheader: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.etsexportclassUrlConstants{staticreadonlySERVER:string='https://api.coze.cn/'staticreadonlyCONVERSATION_CREATE_URL='v1/conversation/create'staticreadonlyGET_ONLINE_INFO_URL='v1/bot/get_online_info'staticreadonlyCHAT_URL='v3/chat'staticreadonlyRETRIEVE_URL='v3/chat/retrieve'staticreadonlyMSG_LIST_URL='v3/chat/message/list'staticreadonlyTTS_DOMAIN_URL='https://openspeech.bytedance.com/api/'staticreadonlyTTS_URL='v1/tts'staticreadonlyVOICE_RECOGNIZE_URL='v3/auc/bigmodel/submit'staticreadonlyVOICE_QUERY_URL='v3/auc/bigmodel/query'staticreadonlyCODE_SUCCESS:number=0staticreadonlyCODE_TTS_SUCCESS:number=3000}所有 API 路径和状态码集中管理,避免硬编码散落在各处。URL 只存储相对路径,域名通过domain字段在运行时拼接。
错误处理策略
HttpManager采用了分层的错误处理:
- 业务错误(
code !== CODE_SUCCESS):reject(data.desc)或reject(data.msg),将服务端错误描述传给调用方 - 网络异常(catch 分支):
reject('请求失败'),并调用ToastUtil.showToast直接提示用户 - 调用方处理:Model 层通过
.catch()捕获 reject 值,设置LoadingStatus.FAILED
.catch((err:BusinessError)=>{this.loadingStatus=LoadingStatus.FAILEDreturnthis.loadingStatus});这种三层设计确保了:网络层统一 Toast 提示、业务层获取具体错误信息、Model 层更新状态供 UI 响应。
方法选择指南
| 场景 | 推荐方法 | 示例 |
|---|---|---|
| 简单键值对 POST | requestPost | — |
| 对象参数 POST | requestPostBody | conversationCreate、chat |
| TTS 语音合成 | requestTtsPostBody | ttsMaker |
| 语音识别 | requestSpeechPostBody | voiceRecognition |
| 查询接口 | requestGet | retrieve、messageList |
| 文件上传 | uploadFiles | 录音文件上传 |
小结
本篇详细介绍了柚兔学伴的网络请求封装架构:
- HttpManager 单例:统一入口,私有构造,懒加载,可配置域名
- RequestOptions 统一参数:一个接口覆盖所有请求类型,通过可选字段适配
- 六种请求方法:针对不同场景(键值对、对象体、TTS、语音、GET、上传)提供专门方法
- CommonResponseModel 泛型:统一响应解析,
code判断成功,data返回业务数据 - 自动认证注入:所有 Coze 请求自动携带 Service Account Token
- 三层错误处理:Toast 提示 + reject 传递 + Model 状态更新