鸿蒙 ArkTS 网络请求实战:从 HTTP 到 Axios 封装,打造生产级请求层
网络请求是每个 App 的标配,但很多鸿蒙开发者在这里卡得一塌糊涂------要么请求发出去没反应,要么 Token 管理乱成一锅粥,要么每个页面都在重复写 http.createHttp()。
这篇文章从零开始,把鸿蒙原生 HTTP 模块的坑踩一遍,再一步步封装成可以直接用于生产的请求层,包含拦截器、Token 刷新、错误统一处理,代码拿走即用。
背景:鸿蒙网络请求有哪些选择?
目前 HarmonyOS NEXT 环境下,网络请求主要有两条路:
- 原生
@ohos.net.http模块:系统内置,零依赖,适合简单场景 @ohos/axios:官方支持的适配版本,API 风格和 Web 端一致,适合复杂项目
两者对比如下:
| 维度 | 原生 http | @ohos/axios |
|---|---|---|
| 依赖 | 无 | 需要 ohpm 安装 |
| API 风格 | 回调/Promise | Promise/async-await |
| 拦截器 | 无 | 有 |
| 并发控制 | 手动 | 内置 |
| 推荐场景 | 简单接口、工具脚本 | 生产级应用 |
实际项目中,建议是:原生 http 了解就行,生产项目直接上 axios。
第一步:用原生 HTTP 模块先跑通一个请求
打权限是第一道关卡,在 module.json5 里加上网络权限:
json
// module.json5
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
}
}
权限加好之后,用原生 http 发一个 GET 请求:
typescript
// pages/Index.ets
import http from '@ohos.net.http';
@Entry
@Component
struct Index {
@State message: string = '等待请求...'
aboutToAppear() {
this.fetchData()
}
async fetchData() {
// 每次请求需要创建一个 httpRequest 实例
const httpRequest = http.createHttp()
try {
const response = await httpRequest.request(
'https://jsonplaceholder.typicode.com/posts/1',
{
method: http.RequestMethod.GET,
header: {
'Content-Type': 'application/json'
},
connectTimeout: 10000, // 连接超时 10s
readTimeout: 10000 // 读取超时 10s
}
)
if (response.responseCode === 200) {
// responseCode 是数字,不是字符串
const data = JSON.parse(response.result as string)
this.message = data.title
} else {
this.message = `请求失败: ${response.responseCode}`
}
} catch (err) {
// err 是 BusinessError 类型
this.message = `网络异常: ${(err as Error).message}`
} finally {
// ⚠️ 重要:用完必须销毁,否则内存泄漏
httpRequest.destroy()
}
}
build() {
Column() {
Text(this.message)
.fontSize(16)
.padding(20)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
几个容易踩的坑:
httpRequest.destroy()必须调用,不然每次请求都会泄漏一个 httpRequest 实例response.result是string | ArrayBuffer,需要手动断言再解析- 超时要显式设置,默认超时时间很长,用户体验差
第二步:安装 @ohos/axios
安装之前,先查一下最新版本,避免装到过时版本:
bash
# 查询最新可用版本
ohpm show @ohos/axios
# 安装
ohpm install @ohos/axios
安装完在 oh-package.json5 里确认有这个依赖:
json
{
"dependencies": {
"@ohos/axios": "^2.2.0"
}
}
注意:版本号以
ohpm show查到的为准,本文使用 2.2.0 编写,后续版本 API 保持向后兼容。
验证安装成功,最基础的用法:
typescript
import axios from '@ohos/axios'
const response = await axios.get('https://jsonplaceholder.typicode.com/posts/1')
console.log(response.data.title)
第三步:封装 Token 管理模块
在封装请求层之前,先把 Token 存取的工具方法准备好,后续拦截器会用到。
typescript
// utils/auth.ets
import preferences from '@ohos.data.preferences';
import common from '@ohos.app.ability.common';
import axios from '@ohos/axios';
const PREF_NAME = 'app_auth'
const TOKEN_KEY = 'access_token'
const REFRESH_TOKEN_KEY = 'refresh_token'
// 内存缓存,避免每次读磁盘
let _token: string = ''
let _refreshToken: string = ''
// 获取 access token(优先内存,其次读持久化)
export function getToken(): string {
return _token
}
// 保存 token(同时写内存和持久化)
export async function saveToken(token: string, refreshToken: string, context: common.UIAbilityContext) {
_token = token
_refreshToken = refreshToken
const prefs = await preferences.getPreferences(context, PREF_NAME)
await prefs.put(TOKEN_KEY, token)
await prefs.put(REFRESH_TOKEN_KEY, refreshToken)
await prefs.flush()
}
// 从持久化中恢复 token(App 启动时调用)
export async function restoreToken(context: common.UIAbilityContext) {
try {
const prefs = await preferences.getPreferences(context, PREF_NAME)
_token = (await prefs.get(TOKEN_KEY, '')) as string
_refreshToken = (await prefs.get(REFRESH_TOKEN_KEY, '')) as string
} catch {
_token = ''
_refreshToken = ''
}
}
// 清除 token(退出登录时调用)
export async function clearToken(context?: common.UIAbilityContext) {
_token = ''
_refreshToken = ''
if (context) {
const prefs = await preferences.getPreferences(context, PREF_NAME)
await prefs.delete(TOKEN_KEY)
await prefs.delete(REFRESH_TOKEN_KEY)
await prefs.flush()
}
}
// 使用 refreshToken 换新的 access token
export async function refreshTokenRequest(): Promise<string> {
if (!_refreshToken) {
throw new Error('无 refreshToken,需要重新登录')
}
// 注意:这里直接用 axios 原始实例,避免循环拦截
const res = await axios.post('https://api.example.com/auth/refresh', {
refreshToken: _refreshToken
})
const newToken: string = res.data.data.accessToken
_token = newToken
return newToken
}
第四步:封装生产级请求层
有了 auth 模块,现在来封装核心请求层。新建 utils/request.ets:
typescript
// utils/request.ets
import axios, { AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from '@ohos/axios'
import { getToken, refreshTokenRequest, clearToken } from './auth'
import router from '@ohos.router'
// --------- 通用响应结构 ---------
export interface ApiResponse<T = unknown> {
code: number
message: string
data: T
}
// --------- 创建实例 ---------
const request: AxiosInstance = axios.create({
baseURL: 'https://api.example.com',
timeout: 15000,
headers: {
'Content-Type': 'application/json'
}
})
// --------- 请求拦截器:自动注入 Token ---------
request.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = getToken()
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config
},
(error) => Promise.reject(error)
)
// --------- 响应拦截器:统一处理 + Token 刷新 ---------
let isRefreshing = false
let pendingQueue: Array<(token: string) => void> = []
request.interceptors.response.use(
(response: AxiosResponse<ApiResponse<unknown>>) => {
const { code, message } = response.data
// 业务层错误(HTTP 200 但 code 不对)
if (code !== 0 && code !== 200) {
console.error(`业务错误 [${code}]: ${message}`)
return Promise.reject(new Error(message))
}
// 返回完整的 data 对象,业务层自己取 .data
return response
},
async (error) => {
const { response, config } = error
if (response?.status === 401) {
if (!isRefreshing) {
isRefreshing = true
try {
const newToken = await refreshTokenRequest()
pendingQueue.forEach(resolve => resolve(newToken))
pendingQueue = []
config.headers['Authorization'] = `Bearer ${newToken}`
return request(config)
} catch (refreshError) {
pendingQueue = []
clearToken()
router.replaceUrl({ url: 'pages/Login' })
return Promise.reject(refreshError)
} finally {
isRefreshing = false
}
} else {
// 已在刷新中,当前请求进队列等待
return new Promise((resolve) => {
pendingQueue.push((newToken: string) => {
config.headers['Authorization'] = `Bearer ${newToken}`
resolve(request(config))
})
})
}
}
// 其他 HTTP 错误
const errorMessages: Record<number, string> = {
400: '请求参数有误',
403: '没有访问权限',
404: '接口不存在',
500: '服务器内部错误',
503: '服务暂时不可用'
}
const msg = errorMessages[response?.status] ?? `网络错误 (${response?.status ?? '未知'})`
console.error(msg)
return Promise.reject(new Error(msg))
}
)
// --------- 对外暴露的便捷方法 ---------
export function get<T>(url: string, params?: Record<string, unknown>): Promise<ApiResponse<T>> {
return request.get<ApiResponse<T>>(url, { params })
.then(res => res.data as ApiResponse<T>)
}
export function post<T>(url: string, data?: Record<string, unknown>): Promise<ApiResponse<T>> {
return request.post<ApiResponse<T>>(url, data)
.then(res => res.data as ApiResponse<T>)
}
export function upload<T>(url: string, formData: FormData): Promise<ApiResponse<T>> {
return request.post<ApiResponse<T>>(url, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
}).then(res => res.data as ApiResponse<T>)
}
export { request }
第五步:请求取消------翻页/切 Tab 时防止接口乱序
列表页快速翻页时,旧请求的响应可能比新请求晚到,导致数据乱序。用 AbortController 取消过期请求:
typescript
// pages/NewsList.ets
import { get } from '../utils/request'
interface NewsItem {
id: number
title: string
summary: string
}
@Entry
@Component
struct NewsList {
@State list: NewsItem[] = []
@State page: number = 1
// 每次翻页重新创建,持有当前请求的取消控制器
private abortController: AbortController | null = null
async loadPage(pageNum: number) {
// 取消上一次还未完成的请求
this.abortController?.abort()
this.abortController = new AbortController()
try {
const res = await get<NewsItem[]>('/news', {
page: pageNum,
size: 20,
signal: this.abortController.signal // 传入 signal
})
this.list = res.data
this.page = pageNum
} catch (err) {
// AbortError 是正常取消,不用弹错误提示
if ((err as Error).name !== 'AbortError') {
console.error('加载失败:', (err as Error).message)
}
}
}
build() {
Column() {
List() {
ForEach(this.list, (item: NewsItem) => {
ListItem() {
Text(item.title)
.fontSize(16)
.padding(12)
}
})
}
Row() {
Button('上一页')
.onClick(() => this.loadPage(this.page - 1))
.enabled(this.page > 1)
Text(`第 ${this.page} 页`)
.margin({ left: 16, right: 16 })
Button('下一页')
.onClick(() => this.loadPage(this.page + 1))
}
.padding(16)
}
}
aboutToAppear() {
this.loadPage(1)
}
// 组件销毁时也要取消未完成的请求
aboutToDisappear() {
this.abortController?.abort()
}
}
第六步:在业务层中调用
有了封装层,定义接口和页面调用都变得很干净:
typescript
// api/user.ets
import { get, post } from '../utils/request'
export interface UserInfo {
id: number
name: string
avatar: string
}
export function getUserInfo(userId: number) {
return get<UserInfo>(`/user/${userId}`)
}
export function login(phone: string, password: string) {
return post<{ token: string; refreshToken: string; userInfo: UserInfo }>(
'/auth/login',
{ phone, password }
)
}
typescript
// pages/Profile.ets
import { getUserInfo, UserInfo } from '../api/user'
@Entry
@Component
struct Profile {
@State userInfo: UserInfo | null = null
@State loading: boolean = false
@State errorMsg: string = ''
async aboutToAppear() {
this.loading = true
try {
const res = await getUserInfo(123)
this.userInfo = res.data
} catch (err) {
this.errorMsg = (err as Error).message ?? '加载失败'
} finally {
this.loading = false
}
}
build() {
Column() {
if (this.loading) {
LoadingProgress().width(40).height(40)
} else if (this.errorMsg) {
Text(this.errorMsg).fontColor('#FF4444')
} else if (this.userInfo) {
Text(`你好,${this.userInfo.name}`).fontSize(20)
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
常见问题 Q&A
Q1:接口在浏览器能访问,在模拟器上请求失败?
检查两点:一是 module.json5 的 INTERNET 权限有没有加;二是接口是否是 HTTP 明文。鸿蒙默认不允许 HTTP 明文请求,需要在 module.json5 里配置 networkSecurityConfig 允许明文,或者后端直接升到 HTTPS。
Q2:并发多个请求时,Token 刷新逻辑会重复触发?
上文用 isRefreshing 标记 + pendingQueue 队列解决了这个问题。第一个 401 触发刷新,后续 401 全部进队列。刷新完成后统一用新 Token 重试,整个过程只发一次刷新请求。
Q3:大文件下载怎么处理?
大文件(几 MB 以上)推荐用原生 http.downloadFile,支持进度回调:
typescript
import http from '@ohos.net.http'
const httpRequest = http.createHttp()
httpRequest.on('dataReceiveProgress', (data) => {
const percent = Math.floor((data.receiveSize / data.totalSize) * 100)
console.log(`下载进度: ${percent}%`)
})
await httpRequest.requestInStream(
'https://example.com/bigfile.zip',
{ method: http.RequestMethod.GET }
)
httpRequest.destroy()
小文件(图片、配置)用 axios 的 responseType: 'arraybuffer' 即可。
总结
整个网络层的搭建涉及四个文件,各司其职:
| 文件 | 职责 |
|---|---|
utils/auth.ets |
Token 存取 + 刷新 |
utils/request.ets |
axios 实例 + 拦截器 + 封装方法 |
api/*.ets |
按业务模块定义具体接口 |
pages/*.ets |
只管调用 api,不关心网络细节 |
几个核心要点:
- 原生 http 简单好用,但别忘了
destroy() - 生产项目用 axios,拦截器处理通用逻辑
- Token 刷新一定要加队列机制,防止并发重复刷
- 快速翻页等场景记得取消过期请求
- auth 工具要用持久化存储,App 重启后 Token 不丢
网络层做好了,后续所有业务接口的开发都会轻松很多。如果还没用上这套封装,拿走就行,稍微改下 baseURL 和接口响应结构就能直接用。
有问题欢迎评论区交流,点个赞再走~