HarmonyOS网络请求框架的简单实现
HarmonyOS中实现一个简单的网络请求框架(如类似于 Android 的 Retrofit、OkHttp),可以基于其 JS/TS API(如fetch、httpRequest等)来封装。以TypeScript为例,给出一个简易的网络请求框架实现(AskTs风格,易于扩展和二次封装)。
1. 概述
HttpClient
是一个基于 HarmonyOS 的单例 HTTP 客户端实现,提供了类型安全的 HTTP 请求功能。它支持多种 HTTP 方法,并包含内置的错误处理和响应解析功能。
2. 特性
- 单例模式实现
- 支持多种 HTTP 方法(GET、POST、PUT、DELETE、PATCH)
- 自动请求头管理
- 内置错误处理机制
- 类型安全的响应解析
- 支持查询参数和请求体
- 可配置的超时时间
- JSON 请求/响应处理
3. 发送请求
3.1 GET 请求
// 基础 GET 请求
const response = await httpClient.get<ResponseType>('https://api.example.com/data');
// 带查询参数的 GET 请求
const response = await httpClient.get<ResponseType>(
'https://api.example.com/data',
{ param1: 'value1', param2: 'value2' }
);
// 带自定义请求头的 GET 请求
const response = await httpClient.get<ResponseType>(
'https://api.example.com/data',
{ param1: 'value1' },
{ 'Custom-Header': 'value' }
);
3.2 POST 请求
// 基础 POST 请求
const response = await httpClient.post<ResponseType>(
'https://api.example.com/data',
{ key: 'value' }
);
// 带自定义请求头的 POST 请求
const response = await httpClient.post<ResponseType>(
'https://api.example.com/data',
{ key: 'value' },
{ 'Custom-Header': 'value' }
);
3.3 PUT 请求
const response = await httpClient.put<ResponseType>(
'https://api.example.com/data',
{ key: 'value' }
);
3.4 DELETE 请求
const response = await httpClient.delete<ResponseType>('https://api.example.com/data');
3.5 PATCH 请求
const response = await httpClient.patch<ResponseType>(
'https://api.example.com/data',
{ key: 'value' }
);
4. 错误处理
客户端包含内置的错误处理机制,针对常见的 HTTP 状态码提供特定的错误信息:
- 404: "请求的资源不存在"
- 500: "服务器错误,请稍后再试"
- 502: "网关错误"
- 503: "服务不可用"
- 504: "网关超时"
- 其他错误: "网络错误,请检查连接"
5. 响应格式
客户端期望的响应格式如下:
interface BaseResponse<T> {
code: number; // 业务状态码
message: string; // 响应消息
biz?: T; // 业务数据
}
如果响应中包含 biz
字段,将自动提取并返回该字段的内容。否则,将返回整个响应内容。
6. 请求头
6.1 默认请求头:在所有请求中包含以下请求头:
const defaultHeaders: Record<string, string> = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
6.2 自定义的请求头
// 合并请求头
private mergeHeaders(customHeaders?: Record<string, string>): Record<string, string> {
const mergedHeaders: Record<string, string> = {};
// 先复制 defaultHeaders
Object.keys(defaultHeaders).forEach((key) => {
mergedHeaders[key] = defaultHeaders[key];
})
// 如果存在 customHeaders,再覆盖
if (customHeaders) {
Object.keys(customHeaders).forEach((key) => {
mergedHeaders[key] = customHeaders[key];
});
}
return mergedHeaders;
}
7. 完整的示例
import 'reflect-metadata';
import http from '@ohos.net.http'
import logger from './logger';
import { HttpError, BaseResponse } from './ResponseData';
import { RequestConfig, HttpRequestBody } from './RequestData';
/**
* @classdesc 网络请求客户端
* @author zw
* @date 2025/4/14
*/
export class HttpClient {
private httpRequest: http.HttpRequest
private static instance: HttpClient;
constructor() {
this.httpRequest = http.createHttp()
}
public static getInstance(): HttpClient {
if (!HttpClient.instance) {
HttpClient.instance = new HttpClient();
}
return HttpClient.instance;
}
// 发送请求
private async request<T>(config: RequestConfig): Promise<T> {
try {
const options: http.HttpRequestOptions = {
method: config.method as http.RequestMethod,
header: this.mergeHeaders(config.headers),
readTimeout: 60000,
connectTimeout: 60000
}
// 处理GET参数
let finalUrl = config.url
if (config.method == 'GET') {
if (config.params && Object.keys(config.params).length > 0) {
finalUrl += `?${this.serializeParams(config.params)}`
}
} else {
// 处理POST/PUT数据
if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(config.method!) && config.body) {
options.extraData = JSON.stringify(config.body)
}
}
const response = await this.httpRequest.request(finalUrl, options)
// 检查响应状态
if (response.responseCode == 200) {
// 解析响应数据
try {
let baseResponse: BaseResponse<T> = {} as BaseResponse<T>
let result: object = JSON.parse(response.result.toString())
const hasBiz: boolean = this.checkKeyInObject(result, 'biz')
console.log('result', JSON.stringify(result))
if (hasBiz) {
baseResponse = JSON.parse(response.result.toString())
// 检查业务状态码
if (baseResponse.code !== 0) {
throw new HttpError(baseResponse.code, baseResponse.message)
}
return baseResponse.biz as T
} else {
return response.result as T
}
} catch (e) {
throw new HttpError(response.responseCode, response.result.toString())
}
} else {
throw new HttpError(response.responseCode, response.result.toString())
}
} catch (error) {
logger.error(new Error(`Unknown HTTP Error: ${JSON.stringify(error)}`))
// 根据错误码显示不同的提示信息
if (error instanceof HttpError) {
let errorMessage = error.message
switch (error.code) {
case 404:
errorMessage = '请求的资源不存在'
break
case 500:
errorMessage = '服务器错误,请稍后再试'
break
case 502:
errorMessage = '网关错误'
break
case 503:
errorMessage = '服务不可用'
break
case 504:
errorMessage = '网关超时'
break
default:
errorMessage = '网络错误,请检查连接'
break
}
throw new HttpError(error.code, errorMessage)
} else {
throw new HttpError(error.code, '网络错误,请检查连接')
}
}
}
// 合并请求头
private mergeHeaders(customHeaders?: Record<string, string>): Record<string, string> {
const mergedHeaders: Record<string, string> = {};
// 先复制 defaultHeaders
Object.keys(defaultHeaders).forEach((key) => {
mergedHeaders[key] = defaultHeaders[key];
})
// 如果存在 customHeaders,再覆盖,这里可以自定义请求
if (customHeaders) {
Object.keys(customHeaders).forEach((key) => {
mergedHeaders[key] = customHeaders[key];
});
}
return mergedHeaders;
}
// 序列化参数
private serializeParams(params: Record<string, string | number | boolean>): string {
return Object.keys(params)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
.join('&')
}
// GET请求
public async get<T>(url: string, params?: Record<string, string | number>,
headers?: Record<string, string>): Promise<T> {
return this.request<T>({
url,
method: 'GET',
params,
headers
})
}
// POST请求
public async post<T>(url: string, body?: object, headers?: Record<string, string>): Promise<T> {
return this.request<T>({
url,
method: 'POST',
body,
headers
})
}
// PUT请求
public async put<T,>(url: string, body?: object, headers?: Record<string, string>): Promise<T> {
return this.request<T>({
url,
method: 'PUT',
body,
headers
})
}
// DELETE请求
public async delete<T>(url: string, headers?: Record<string, string>): Promise<T> {
return this.request<T>({
url,
method: 'DELETE',
headers
})
}
// PATCH请求
public async patch<T>(url: string, body?: object, headers?: Record<string, string>): Promise<T> {
return this.request<T>({
url,
method: 'PATCH',
body,
headers
})
}
// 递归检查对象中是否包含指定的key
private checkKeyInObject(obj: object, targetKey: string): boolean {
// 如果是数组,遍历每个元素
if (Array.isArray(obj)) {
for (let i = 0; i < obj.length; i++) {
const item: object | string | number | boolean | null = obj[i] as object | string | number | boolean | null
if (item !== null && typeof item === 'object') {
if (this.checkKeyInObject(item, targetKey)) {
return true
}
}
}
return false
}
// 使用Object.keys()遍历对象
const keys: string[] = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
const key: string = keys[i]
if (key === targetKey) {
return true
}
const value: object | string | number | boolean | null = obj[key] as object | string | number | boolean | null
if (value !== null && typeof value === 'object') {
if (this.checkKeyInObject(value, targetKey)) {
return true
}
}
}
return false
}
}
const defaultHeaders: Record<string, string> = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
import { HttpClient } from "../HttpClient"
import { UserApi } from "../api/UserApi"
export class WeatherRequest {
static async getWeather(params?: Record<string, string | number>): Promise<String> {
return await HttpClient.getInstance().get<string>(UserApi.indexApiUrl, params);
}
}
export class UserApi {
static indexApiUrl: string = 'http://v.juhe.cn/weather/index';
}
import { WeatherRequest } from '../http/request/WeatherRequest';
import { UserInfo } from '../http/UserInfo';
@Entry
@Component
struct NewDetailsPage {
@State message: string = '获取详情中。。。';
appKey: string = '51bbd89bb89e50b1668534882e7baeb0';
build() {
Column() {
Button('获取数据')
.id('NewDetailsPageHelloWorld')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.width(200)
.height(40)
.margin({ top: 20 })
.onClick(() => {
this.getWeather();
})
Scroll() {
Column() {
Text(this.message)
.fontSize(20)
.textAlign(TextAlign.Start)
.width('100%')
}
.width('100%')
.padding(10)
.justifyContent(FlexAlign.Start) // 确保内容从顶部开始
}
.height('60%')
.width('100%')
.margin({ top: 10 }) // 确保 Scroll 顶部没有 margin
.scrollable(ScrollDirection.Vertical)
}
.height('100%')
.width('100%')
}
public async getWeather() {
let map: Record<string, string> = {
"key": this.appKey,
"cityname": "合肥",
"dtype": "json",
"format": ""
}
const resp = await WeatherRequest.getWeather(map);
let userInfo: UserInfo = JSON.parse(resp.toString()) as UserInfo
this.message = resp.toString()
console.log("NewDetailsPage:==" + JSON.stringify(resp))
}
}