HarmonyOS7 网络请求:@ohos.net.http 封装一个够用的请求工具

前言
HarmonyOS7 的原生 HTTP 模块 @kit.NetworkKit,说好用也好用,说难用也难用——每次请求都要 createHttp、设 header、处理回调、destroy,代码写一堆还容易忘销毁。与其每次都写一遍,不如封装一个用着顺手的工具类。
这篇从原生 API 讲起,到封装思路,再到完整实现,最后跟 @State 联动刷新 UI,一条龙搞定。
我一开始直接用 http.createHttp() 写请求,一个页面 3 个接口,写了 3 遍几乎一样的代码。后来发现忘调 destroy(),内存泄漏了。这就很离谱——写网络请求还得记着销毁对象?所以必须封装。
原生 HTTP 模块基础
先看看原生 API 长什么样。
GET 请求
import { http } from '@kit.NetworkKit'
function doGet() {
const httpRequest = http.createHttp()
httpRequest.request('https://api.example.com/users', {
method: http.RequestMethod.GET,
header: { 'Content-Type': 'application/json' },
readTimeout: 10000,
connectTimeout: 10000
}, (err, data) => {
if (!err) {
console.info('响应码: ' + data.responseCode)
console.info('响应体: ' + JSON.stringify(data.result))
} else {
console.error('请求失败: ' + JSON.stringify(err))
}
httpRequest.destroy()
})
}

POST 请求
function doPost() {
const httpRequest = http.createHttp()
httpRequest.request('https://api.example.com/login', {
method: http.RequestMethod.POST,
header: { 'Content-Type': 'application/json' },
extraData: { username: 'admin', password: '123456' },
readTimeout: 10000,
connectTimeout: 10000
}, (err, data) => {
if (!err) {
console.info('响应码: ' + data.responseCode)
console.info('响应体: ' + JSON.stringify(data.result))
} else {
console.error('请求失败: ' + JSON.stringify(err))
}
httpRequest.destroy()
})
}
问题很明显:
- 每次都要
createHttp()+destroy() - 错误处理、超时处理每次都写一遍
- 回调式写法,容易嵌套地狱
- 没有统一的 header 管理(比如 token)
- 没有统一的响应格式处理
封装思路
封装前先想清楚要什么:
| 需求 | 实现方式 |
|---|---|
| 统一基础 URL | 构造函数传入 baseUrl |
| 自动带 token | 请求拦截器,自动注入 header |
| Promise 化 | 把回调封装成 Promise |
| 自动销毁 | 请求完成后自动 destroy |

| 统一错误处理 | 拦截器 + 统一错误码映射 |
| 统一响应格式 | 泛型解析 data.result |
| 超时统一配置 | 默认值 + 可覆盖 |
类图简化版:
HttpClient
├── baseUrl: string
├── defaultHeaders: Record<string, string>
├── timeout: number
├── get<T>(url, params?) → Promise<T>
├── post<T>(url, data?) → Promise<T>
├── put<T>(url, data?) → Promise<T>
├── delete<T>(url) → Promise<T>
└── request<T>(method, url, options?) → Promise<T> (核心方法)
完整封装代码
import { http, HttpResponse } from '@kit.NetworkKit'
interface Response<T> {
code: number
message: string
data: T
}
class HttpError extends Error {
code: number
constructor(code: number, message: string) {
super(message)
this.code = code
}
}
class HttpClient {
private baseUrl: string = ''
private defaultHeaders: Record<string, string> = {
'Content-Type': 'application/json'
}
private timeout: number = 15000
setBaseUrl(url: string) {
this.baseUrl = url
}
setHeader(key: string, value: string) {
this.defaultHeaders[key] = value
}
removeHeader(key: string) {
delete this.defaultHeaders[key]
}
setToken(token: string) {
this.defaultHeaders['Authorization'] = `Bearer ${token}`
}
clearToken() {
delete this.defaultHeaders['Authorization']
}
get<T>(url: string, params?: Record<string, string>): Promise<T> {
const fullUrl = this.buildUrl(url, params)
return this.request<T>(http.RequestMethod.GET, fullUrl)
}
post<T>(url: string, data?: Object): Promise<T> {
return this.request<T>(http.RequestMethod.POST, this.baseUrl + url, data)
}
put<T>(url: string, data?: Object): Promise<T> {
return this.request<T>(http.RequestMethod.PUT, this.baseUrl + url, data)
}
delete<T>(url: string): Promise<T> {
return this.request<T>(http.RequestMethod.DELETE, this.baseUrl + url)
}
private buildUrl(url: string, params?: Record<string, string>): string {
let fullUrl = this.baseUrl + url
if (params) {
const query = Object.keys(params)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
.join('&')
fullUrl += (fullUrl.includes('?') ? '&' : '?') + query
}
return fullUrl
}
private request<T>(method: http.RequestMethod, url: string, data?: Object): Promise<T> {
return new Promise<T>((resolve, reject) => {
const httpRequest = http.createHttp()
const options: http.HttpRequestOptions = {
method: method,
header: { ...this.defaultHeaders },
readTimeout: this.timeout,
connectTimeout: this.timeout,
expectDataType: http.HttpDataType.STRING
}
if (data && method !== http.RequestMethod.GET) {
options.extraData = JSON.stringify(data)
}
httpRequest.request(url, options, (err, responseData: HttpResponse) => {
httpRequest.destroy()
if (err) {
reject(new HttpError(err.code ?? -1, err.message ?? '网络请求失败'))
return
}
if (responseData.responseCode === 200 || responseData.responseCode === 201) {
try {
const result = JSON.parse(responseData.result as string) as Response<T>
if (result.code === 0 || result.code === 200) {
resolve(result.data)
} else {
reject(new HttpError(result.code, result.message))
}
} catch (e) {
reject(new HttpError(-1, '数据解析失败'))
}
} else if (responseData.responseCode === 401) {
this.clearToken()
reject(new HttpError(401, '登录已过期,请重新登录'))
} else {
reject(new HttpError(responseData.responseCode, `请求失败(${responseData.responseCode})`))
}
})
})
}
}
export const httpClient = new HttpClient()
逐方法讲解
setBaseUrl / setToken: 初始化时调一次就行。token 变了就重新 setToken。
get<T>(url, params?): 泛型方法,T 是响应数据的类型。params 会自动拼成 query string。用法:
const users = await httpClient.get<User[]>('/users', { page: '1', size: '20' })
post<T>(url, data?): data 自动 JSON.stringify。用法:
const result = await httpClient.post<LoginResult>('/login', { phone: '138xxxx', code: '1234' })
buildUrl: 处理 query 参数拼接,做了 encodeURIComponent,防止特殊字符炸 URL。
request<T>(核心方法):
createHttp()创建请求对象- 合并默认 header 和超时配置
- GET 请求不带 body,其他方法自动
JSON.stringify(data) - 请求完成后立刻
destroy(),不管成功失败 - 200/201 → 解析 JSON → 检查业务 code → 返回 data
- 401 → 清 token + 抛过期错误(可以在这里加跳登录页的逻辑)
- 其他状态码 → 直接抛 HttpError
请求拦截与错误处理
上面的封装已经包含了基础的拦截(token 注入、401 处理)。如果需要更复杂的拦截,比如自动重试、请求日志,可以这样扩展:
private async request<T>(method: http.RequestMethod, url: string, data?: Object): Promise<T> {
const maxRetry = 2
let lastError: Error = new Error('unknown')
for (let i = 0; i <= maxRetry; i++) {
try {
return await this.doRequest<T>(method, url, data)
} catch (err) {
lastError = err as Error
if (err instanceof HttpError && err.code === 401) {
throw err
}
if (i < maxRetry) {
console.warn(`请求失败,第 ${i + 1} 次重试...`)
await this.delay(1000 * (i + 1))
}
}
}
throw lastError
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
逻辑:
- 失败后自动重试,最多 2 次
- 401 不重试,直接抛(重试也没用)
- 重试间隔递增:1 秒、2 秒
- 其他错误重试完再抛
别滥用重试。写操作(POST/PUT/DELETE)重试可能导致重复提交,只给 GET 加重试就好。
与 @State 联动刷新 UI
封装好了,怎么在页面里用?配合 @State 实现请求即刷新:
import { httpClient } from '../utils/HttpClient'
interface Article {
id: number
title: string
content: string
}
@Entry
@Component
struct ArticleListPage {
@State articles: Article[] = []
@State isLoading: boolean = false
@State errorMsg: string = ''
aboutToAppear() {
this.loadArticles()
}
private async loadArticles() {
this.isLoading = true
this.errorMsg = ''
try {
this.articles = await httpClient.get<Article[]>('/articles')
} catch (err) {
this.errorMsg = (err as Error).message || '加载失败'
} finally {
this.isLoading = false
}
}
build() {
Column() {
if (this.isLoading) {
LoadingProgress().width(48).height(48)
} else if (this.errorMsg) {
Column() {
Text(this.errorMsg).fontColor('#FF4444').fontSize(14)
Button('重试')
.margin({ top: 12 })
.onClick(() => this.loadArticles())
}
} else {
List() {
ForEach(this.articles, (item: Article) => {
ListItem() {
Column() {
Text(item.title).fontSize(16).fontWeight(FontWeight.Bold)
Text(item.content).fontSize(13).fontColor('#666666').maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.padding(16)
}
})
}
.width('100%')
.layoutWeight(1)
}
}
.width('100%')
.height('100%')
}
}
要点:
@State articles和@State isLoading:请求结果直接赋值给@State变量,UI 自动刷新try/catch/finally:标准模式——加载中 → 成功显示 / 失败提示 → 关闭 loading- 失败时显示"重试"按钮,用户可以手动再请求
aboutToAppear里发请求,页面出现就加载
写在最后
网络请求封装没有什么高深的,核心就三件事:Promise 化、自动销毁、统一错误处理。我这套封装不完美,没有文件上传、没有请求取消,但覆盖了 80% 的场景。够用就行,别过度设计。
等业务真的需要文件上传了再加,不要提前堆功能。代码越少越好维护,这是真理。
更多推荐


所有评论(0)