HarmonyOS《柚兔学伴》项目实战15-网络请求封装
第15篇:网络请求封装
引言
柚兔学伴需要与多个后端服务交互——Coze 智能体 API、火山引擎 TTS 语音合成、百度翻译、汉字字典等。每个服务的域名、请求格式、响应结构各不相同。项目通过 HttpManager 和 HttpRequest 两层架构,实现了统一的网络请求封装,让上层 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.SERVER即https://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 中的 conversationCreate、chat 均使用此方法。
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_URL(https://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.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 相关请求类自动注入认证头:
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 采用了分层的错误处理:
- 业务错误(
code !== CODE_SUCCESS):reject(data.desc)或reject(data.msg),将服务端错误描述传给调用方 - 网络异常(catch 分支):
reject('请求失败'),并调用ToastUtil.showToast直接提示用户 - 调用方处理:Model 层通过
.catch()捕获 reject 值,设置LoadingStatus.FAILED
.catch((err: BusinessError) => {
this.loadingStatus = LoadingStatus.FAILED
return this.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 状态更新
更多推荐


所有评论(0)