鸿蒙5网络请求封装实战:打造ArkTS版"axios"

暗雨OL
发布于 2025-6-30 02:13
浏览
0收藏

在鸿蒙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支持
实现离线缓存策略
集成状态管理
添加性能监控

分类
标签
收藏
回复
举报
回复
    相关推荐