
鸿蒙5网络请求封装实战:打造ArkTS版"axios"
在鸿蒙5应用开发中,网络请求是必不可少的功能。虽然鸿蒙提供了基础的@ohos.net.http模块,但直接使用较为繁琐。本文将基于ArkCompiler开发环境,从零实现一个类似axios的网络请求库,提供更简洁高效的API。
一、鸿蒙原生HTTP模块分析
鸿蒙5提供了两种网络请求方式:
@ohos.net.http:基础HTTP客户端
@ohos.request:更高级的请求管理
原生请求示例:
import http from ‘@ohos.net.http’
// 创建请求对象
let httpRequest = http.createHttp()
// 发起请求
httpRequest.request(
“https://api.example.com/data”,
{
method: http.RequestMethod.GET,
header: { ‘Content-Type’: ‘application/json’ }
},
(err, data) => {
if (err) {
console.error(‘请求失败:’, err)
return
}
console.log(‘响应结果:’, data.result)
}
)
这种回调方式的缺点很明显:嵌套深、错误处理不便、缺乏拦截器等高级功能。
二、设计我们的HttpClient
我们将实现以下核心功能:
Promise风格的API
请求/响应拦截器
全局配置
自动JSON转换
超时处理
取消请求
2.1 基础架构设计
// http/HttpClient.ets
type HttpMethod = ‘GET’ | ‘POST’ | ‘PUT’ | ‘DELETE’ | ‘PATCH’
interface RequestConfig {
url: string
method?: HttpMethod
baseURL?: string
headers?: Record<string, string>
params?: Record<string, any>
data?: any
timeout?: number
}
interface HttpResponse<T = any> {
status: number
data: T
headers: Record<string, string>
config: RequestConfig
}
class HttpClient {
private instance = http.createHttp()
private interceptors = {
request: [] as Array<(config: RequestConfig) => RequestConfig>,
response: [] as Array<(response: HttpResponse) => HttpResponse>
}
private defaultConfig: Partial<RequestConfig> = {
timeout: 30000,
headers: {
‘Content-Type’: ‘application/json’
}
}
constructor(config?: Partial<RequestConfig>) {
if (config) {
this.defaultConfig = { …this.defaultConfig, …config }
}
}
// 核心请求方法
async request<T = any>(config: RequestConfig): Promise<HttpResponse<T>> {
// 实现将在下文展开
}
}
三、实现核心请求方法
3.1 请求预处理
private async processRequest(config: RequestConfig): Promise<RequestConfig> {
// 合并配置
let mergedConfig: RequestConfig = {
…this.defaultConfig,
…config
}
// 处理baseURL
if (mergedConfig.baseURL && !mergedConfig.url.startsWith(‘http’)) {
mergedConfig.url = ${mergedConfig.baseURL}${mergedConfig.url}
}
// 处理查询参数
if (mergedConfig.params) {
const params = new URLSearchParams()
Object.entries(mergedConfig.params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
params.append(key, String(value))
}
})
mergedConfig.url += ?${params.toString()}
}
// 执行请求拦截器
for (const interceptor of this.interceptors.request) {
mergedConfig = interceptor(mergedConfig)
}
return mergedConfig
}
3.2 响应处理
private async processResponse<T>(
response: http.HttpResponse,
config: RequestConfig
): Promise<HttpResponse<T>> {
// 基础响应结构
let result: HttpResponse<T> = {
status: response.responseCode,
headers: response.header,
config,
data: response.result
}
// 尝试解析JSON
if (typeof response.result === ‘string’) {
try {
result.data = JSON.parse(response.result)
} catch (e) {
// 非JSON数据保持原样
}
}
// 执行响应拦截器
for (const interceptor of this.interceptors.response) {
result = interceptor(result)
}
// 状态码检查
if (response.responseCode >= 400) {
throw new Error(Request failed with status code ${response.responseCode}
)
}
return result
}
3.3 完整的request方法实现
async request<T = any>(config: RequestConfig): Promise<HttpResponse<T>> {
try {
// 预处理配置
const processedConfig = await this.processRequest(config)
// 创建Promise包装
return new Promise<HttpResponse<T>>((resolve, reject) => {
// 设置超时定时器
let timeoutId: number | undefined
if (processedConfig.timeout) {
timeoutId = setTimeout(() => {
this.instance.destroy()
reject(new Error(`Timeout of ${processedConfig.timeout}ms exceeded`))
}, processedConfig.timeout)
}
// 发起请求
this.instance.request(
processedConfig.url,
{
method: this.mapMethod(processedConfig.method || 'GET'),
header: processedConfig.headers,
extraData: processedConfig.data ?
(typeof processedConfig.data === 'string' ?
processedConfig.data :
JSON.stringify(processedConfig.data)) :
undefined
},
async (err, response) => {
// 清除超时定时器
if (timeoutId) clearTimeout(timeoutId)
if (err) {
reject(err)
return
}
try {
const processedResponse = await this.processResponse<T>(response, processedConfig)
resolve(processedResponse)
} catch (e) {
reject(e)
}
}
)
})
} catch (error) {
return Promise.reject(error)
}
}
private mapMethod(method: HttpMethod): http.RequestMethod {
const map: Record<HttpMethod, http.RequestMethod> = {
‘GET’: http.RequestMethod.GET,
‘POST’: http.RequestMethod.POST,
‘PUT’: http.RequestMethod.PUT,
‘DELETE’: http.RequestMethod.DELETE,
‘PATCH’: http.RequestMethod.PATCH
}
return map[method]
}
四、添加便捷方法和拦截器
4.1 快捷方法
class HttpClient {
// … 之前的代码
get<T = any>(url: string, config?: Omit<RequestConfig, ‘url’ | ‘method’>) {
return this.request<T>({ …config, url, method: ‘GET’ })
}
post<T = any>(url: string, data?: any, config?: Omit<RequestConfig, ‘url’ | ‘method’ | ‘data’>) {
return this.request<T>({ …config, url, method: ‘POST’, data })
}
put<T = any>(url: string, data?: any, config?: Omit<RequestConfig, ‘url’ | ‘method’ | ‘data’>) {
return this.request<T>({ …config, url, method: ‘PUT’, data })
}
delete<T = any>(url: string, config?: Omit<RequestConfig, ‘url’ | ‘method’>) {
return this.request<T>({ …config, url, method: ‘DELETE’ })
}
}
4.2 拦截器实现
class HttpClient {
// … 之前的代码
useRequestInterceptor(
interceptor: (config: RequestConfig) => RequestConfig
): number {
return this.interceptors.request.push(interceptor) - 1
}
useResponseInterceptor<T = any>(
interceptor: (response: HttpResponse<T>) => HttpResponse<T>
): number {
return this.interceptors.response.push(interceptor) - 1
}
ejectRequestInterceptor(id: number) {
this.interceptors.request.splice(id, 1)
}
ejectResponseInterceptor(id: number) {
this.interceptors.response.splice(id, 1)
}
}
五、实现取消请求功能
interface CancelToken {
promise: Promise<never>
reason?: string
}
class CancelTokenSource {
token: CancelToken
cancel: (reason?: string) => void
constructor() {
let cancelFn: (reason?: string) => void
this.token = {
promise: new Promise((_, reject) => {
cancelFn = reject
})
} as CancelToken
this.cancel = (reason = ‘Request canceled’) => {
this.token.reason = reason
cancelFn(reason)
}
}
}
// 在RequestConfig中添加cancelToken
interface RequestConfig {
// …其他属性
cancelToken?: CancelToken
}
// 修改request方法
async request<T = any>(config: RequestConfig): Promise<HttpResponse<T>> {
try {
const processedConfig = await this.processRequest(config)
return new Promise<HttpResponse<T>>((resolve, reject) => {
// 取消令牌检查
if (processedConfig.cancelToken) {
processedConfig.cancelToken.promise.catch(reason => {
this.instance.destroy()
reject(reason)
})
}
// ...其余代码不变
})
} catch (error) {
return Promise.reject(error)
}
}
六、完整使用示例
6.1 创建实例并配置
// http/index.ets
const apiClient = new HttpClient({
baseURL: ‘https://api.example.com/v1’,
timeout: 10000,
headers: {
‘X-Client-Type’: ‘harmonyos’
}
})
// 添加请求拦截器
apiClient.useRequestInterceptor(config => {
console.log(‘发送请求:’, config.url)
// 添加认证token
const token = AppStorage.get(‘token’)
if (token) {
config.headers = {
…config.headers,
‘Authorization’: Bearer ${token}
}
}
return config
})
// 添加响应拦截器
apiClient.useResponseInterceptor(response => {
if (response.data?.code !== 0) {
throw new Error(response.data?.message || ‘请求失败’)
}
return response
})
export default apiClient
6.2 在页面中使用
// pages/UserPage.ets
import apiClient from ‘…/http’
import { CancelTokenSource } from ‘…/http/HttpClient’
@Entry
@Component
struct UserPage {
@State userData: any = null
@State loading: boolean = false
@State error: string = ‘’
private cancelSource?: CancelTokenSource
async getUserInfo(userId: string) {
this.loading = true
this.error = ‘’
this.cancelSource = new CancelTokenSource()
try {
const response = await apiClient.get(`/users/${userId}`, {
cancelToken: this.cancelSource.token
})
this.userData = response.data
} catch (e) {
this.error = e.message
} finally {
this.loading = false
}
}
onCancel() {
this.cancelSource?.cancel(‘用户手动取消’)
}
build() {
Column() {
if (this.loading) {
LoadingProgress()
.width(50)
.height(50)
Button(‘取消请求’)
.onClick(() => this.onCancel())
} else if (this.error) {
Text(this.error)
.fontColor(Color.Red)
} else if (this.userData) {
UserCard({ user: this.userData })
}
Button('获取用户信息')
.onClick(() => this.getUserInfo('123'))
.margin(20)
}
.width('100%')
.height('100%')
}
}
七、高级功能扩展
7.1 文件上传
// 在HttpClient中添加方法
async uploadFile<T = any>(
url: string,
fileUri: string,
fieldName: string = ‘file’,
config?: Omit<RequestConfig, ‘url’ | ‘method’ | ‘data’>
): Promise<HttpResponse<T>> {
const file = await fs.open(fileUri)
const stat = await fs.stat(fileUri)
const boundary = ‘----WebKitFormBoundary’ + Math.random().toString(16).substr(2)
const header = {
…config?.headers,
‘Content-Type’: multipart/form-data; boundary=${boundary}
}
let body = ‘’
body += --${boundary}\r\n
body += Content-Disposition: form-data; name="${fieldName}"; filename="${fileUri.split('/').pop()}"\r\n
body += Content-Type: application/octet-stream\r\n\r\n
const fileData = await fs.read(file.fd, { length: stat.size })
const end = \r\n--${boundary}--\r\n
const data = new Uint8Array([
…new TextEncoder().encode(body),
…fileData.buffer,
…new TextEncoder().encode(end)
])
await fs.close(file.fd)
return this.request<T>({
…config,
url,
method: ‘POST’,
headers: header,
data: data
})
}
7.2 请求重试机制
class HttpClient {
private retryConfig = {
retries: 3,
retryDelay: 1000,
retryCondition: (error: Error) =>
error.message.includes(‘timeout’) ||
error.message.includes(‘Network request failed’)
}
withRetry(config: {
retries?: number
retryDelay?: number
retryCondition?: (error: Error) => boolean
}) {
this.retryConfig = { …this.retryConfig, …config }
return this
}
private async requestWithRetry<T>(
config: RequestConfig,
retryCount: number = 0
): Promise<HttpResponse<T>> {
try {
return await this.request<T>(config)
} catch (error) {
if (retryCount < this.retryConfig.retries &&
this.retryConfig.retryCondition(error)) {
await new Promise(resolve =>
setTimeout(resolve, this.retryConfig.retryDelay))
return this.requestWithRetry<T>(config, retryCount + 1)
}
throw error
}
}
// 修改原有的request方法
async request<T = any>(config: RequestConfig): Promise<HttpResponse<T>> {
return this.requestWithRetry<T>(config)
}
}
八、性能优化建议
连接复用:在应用生命周期内保持HttpClient实例单例
数据缓存:对GET请求实现内存缓存
请求合并:对高频小请求进行合并
压缩处理:支持gzip压缩响应
DNS预取:提前解析已知域名
// 缓存实现示例
class HttpClient {
private cache = new Map<string, { expire: number; data: any }>()
private cacheConfig = {
defaultTTL: 60000 // 60秒
}
async getWithCache<T = any>(
url: string,
config?: Omit<RequestConfig, ‘url’ | ‘method’> & { cacheTTL?: number }
): Promise<HttpResponse<T>> {
const cacheKey = ${url}:${JSON.stringify(config?.params || {})}
const cached = this.cache.get(cacheKey)
if (cached && cached.expire > Date.now()) {
return {
status: 200,
data: cached.data,
headers: { 'x-cache': 'HIT' },
config: { url, method: 'GET', ...config }
}
}
const response = await this.get<T>(url, config)
if (response.status === 200) {
this.cache.set(cacheKey, {
expire: Date.now() + (config?.cacheTTL || this.cacheConfig.defaultTTL),
data: response.data
})
}
return {
...response,
headers: { ...response.headers, 'x-cache': 'MISS' }
}
}
}
结语
本文实现的HttpClient库提供了比原生HTTP模块更友好的API,同时保持了鸿蒙5的性能优势。关键点总结:
Promise封装:解决了回调地狱问题
拦截器机制:实现了灵活的请求/响应处理
取消功能:提升了用户体验
类型安全:充分利用ArkTS的类型系统
在实际项目中,可以根据需求进一步扩展:
添加WebSocket支持
实现离线缓存策略
集成状态管理
添加性能监控
