系列文章《鸿蒙开发从入门到专家》第 6 篇(中级篇)
前言
数据持久化搞定了,但应用不能只有本地数据。网络请求是现代 App 的命脉——登录鉴权、内容加载、表单提交,样样离不开 HTTP。
鸿蒙官方提供了 @kit.NetworkKit 里的 http 模块,但直接用它的 API 写业务代码会非常痛苦:每个请求都要手写异常处理、重复拼装 URL、手动管理 loading 状态。本文从官方 API 出发,一步步封装出一个企业级的网络层,并讨论数据流转、错误处理和缓存策略。
一、官方 HTTP API 基础
1.1 发起一个简单请求
import { http } from '@kit.NetworkKit'
async function fetchNewsList(): Promise<void> {
let httpRequest = http.createHttp()
try {
let response = await httpRequest.request(
'https://api.example.com/news/list',
{
method: http.RequestMethod.GET,
header: {
'Content-Type': 'application/json',
'Authorization': 'Bearer xxx_token'
},
connectTimeout: 10000,
readTimeout: 10000
}
)
if (response.responseCode === 200) {
let data = JSON.parse(response.result.toString())
console.info(`获取到 ${data.length} 条新闻`)
} else {
console.error(`请求失败: ${response.responseCode}`)
}
} catch (err) {
console.error(`网络错误: ${err.message}`)
} finally {
httpRequest.destroy() // 必须手动销毁,否则会内存泄漏
}
}
1.2 POST 请求与表单提交
async function submitLogin(phone: string, password: string): Promise<void> {
let httpRequest = http.createHttp()
try {
let response = await httpRequest.request(
'https://api.example.com/auth/login',
{
method: http.RequestMethod.POST,
header: {
'Content-Type': 'application/json'
},
extraData: JSON.stringify({
phone: phone,
password: password
})
}
)
let result = JSON.parse(response.result.toString())
if (result.code === 0) {
// 保存 token
await saveToken(result.data.token)
} else {
throw new Error(result.message)
}
} finally {
httpRequest.destroy()
}
}
1.3 文件上传
async function uploadAvatar(fileUri: string): Promise<void> {
let httpRequest = http.createHttp()
let response = await httpRequest.request(
'https://api.example.com/upload',
{
method: http.RequestMethod.POST,
header: {
'Content-Type': 'multipart/form-data'
},
extraData: [
{ name: 'file', value: 'uri', type: 'file', filePath: fileUri }
]
}
)
httpRequest.destroy()
}
二、封装网络层:从原始 API 到业务可用
直接使用 http.createHttp() 的问题很明显:
每个请求都要手动
destroy(),容易遗漏导致内存泄漏URL、header、超时配置到处重复
错误处理逻辑分散,难以统一
没有请求/响应拦截机制
我们封装一个 HttpClient 类:
// types/NetResponse.ets
interface NetResponse<T> {
code: number // 业务状态码
message: string // 提示信息
data: T // 实际数据
}
// network/HttpClient.ets
import { http } from '@kit.NetworkKit'
class HttpClient {
private baseURL: string = 'https://api.example.com'
private defaultTimeout: number = 15000
// 请求拦截器
private async interceptRequest(options: http.HttpRequestOptions): Promise<http.HttpRequestOptions> {
let token = await getToken()
if (token) {
options.header = {
...options.header,
'Authorization': `Bearer ${token}`
}
}
return options
}
// 响应拦截器
private async interceptResponse<T>(response: http.HttpResponse): Promise<NetResponse<T>> {
if (response.responseCode !== 200) {
throw new NetworkError(response.responseCode, `HTTP ${response.responseCode}`)
}
let result: NetResponse<T>
try {
result = JSON.parse(response.result.toString())
} catch {
throw new NetworkError(-1, '响应格式错误')
}
// Token 过期处理
if (result.code === 401) {
await clearToken()
throw new NetworkError(401, '登录已过期,请重新登录')
}
if (result.code !== 0) {
throw new NetworkError(result.code, result.message)
}
return result
}
async request<T>(
url: string,
method: http.RequestMethod = http.RequestMethod.GET,
data?: object,
customOptions?: Partial<http.HttpRequestOptions>
): Promise<T> {
let fullURL = url.startsWith('http') ? url : `${this.baseURL}${url}`
let options: http.HttpRequestOptions = {
method: method,
header: {
'Content-Type': 'application/json',
...customOptions?.header
},
connectTimeout: customOptions?.connectTimeout || this.defaultTimeout,
readTimeout: customOptions?.readTimeout || this.defaultTimeout,
...customOptions
}
if (data && (method === http.RequestMethod.POST || method === http.RequestMethod.PUT)) {
options.extraData = JSON.stringify(data)
}
// 请求拦截
options = await this.interceptRequest(options)
let httpRequest = http.createHttp()
try {
let response = await httpRequest.request(fullURL, options)
let result = await this.interceptResponse<T>(response)
return result.data
} finally {
httpRequest.destroy() // 确保一定销毁
}
}
// 便捷方法
async get<T>(url: string, params?: Record<string, string>): Promise<T> {
let queryString = params ? '?' + new URLSearchParams(params).toString() : ''
return this.request<T>(url + queryString, http.RequestMethod.GET)
}
async post<T>(url: string, data: object): Promise<T> {
return this.request<T>(url, http.RequestMethod.POST, data)
}
async put<T>(url: string, data: object): Promise<T> {
return this.request<T>(url, http.RequestMethod.PUT, data)
}
async delete<T>(url: string): Promise<T> {
return this.request<T>(url, http.RequestMethod.DELETE)
}
}
// 错误类
class NetworkError extends Error {
code: number
constructor(code: number, message: string) {
super(message)
this.code = code
this.name = 'NetworkError'
}
}
// 单例导出
export const httpClient = new HttpClient()
三、数据流转:从网络到界面的完整链路
网络请求不是孤立的操作,它要跟 UI 状态管理配合。一个标准的请求流程包含四个状态:
初始状态 → 加载中 → 成功/失败 → 显示数据/错误提示
我们用 @State 管理这些状态:
// pages/NewsListPage.ets
import { httpClient } from '../network/HttpClient'
interface NewsItem {
id: string
title: string
summary: string
coverUrl: string
publishTime: string
}
@Entry
@Component
struct NewsListPage {
@State newsList: NewsItem[] = []
@State loading: boolean = false
@State error: string = ''
@State hasMore: boolean = true
@State currentPage: number = 1
aboutToAppear() {
this.loadData()
}
async loadData(isRefresh: boolean = true) {
if (this.loading) return
this.loading = true
this.error = ''
if (isRefresh) {
this.currentPage = 1
}
try {
let result = await httpClient.get<NewsItem[]>('/news/list', {
page: this.currentPage.toString(),
size: '20'
})
if (isRefresh) {
this.newsList = result
} else {
this.newsList = [...this.newsList, ...result]
}
this.hasMore = result.length === 20
this.currentPage++
} catch (err) {
this.error = err instanceof Error ? err.message : '加载失败'
} finally {
this.loading = false
}
}
build() {
Column() {
if (this.loading && this.newsList.length === 0) {
// 首次加载
LoadingProgress()
.width(48)
.height(48)
.color('#317AF7')
} else if (this.error && this.newsList.length === 0) {
// 加载失败
Column({ space: 12 }) {
Text(this.error).fontSize(14).fontColor('#999')
Button('重新加载')
.onClick(() => this.loadData())
}
} else {
// 列表内容
List({ space: 8 }) {
LazyForEach(this.newsList, (item: NewsItem) => {
ListItem() {
// ... 新闻卡片布局
}
}, (item: NewsItem) => item.id)
// 底部加载更多
if (this.hasMore) {
ListItem() {
Row() {
if (this.loading) {
LoadingProgress().width(20).height(20)
}
Text(this.loading ? '加载中...' : '上拉加载更多')
.fontSize(14)
.fontColor('#999')
}
.height(50)
.justifyContent(FlexAlign.Center)
.onClick(() => {
if (!this.loading) {
this.loadData(false)
}
})
}
}
}
.width('100%')
.layoutWeight(1)
}
}
.width('100%')
.height('100%')
}
}
四、错误处理策略
4.1 错误分类与处理
4.2 自动重试机制
async function requestWithRetry<T>(
requestFn: () => Promise<T>,
maxRetries: number = 2
): Promise<T> {
let lastError: Error | null = null
for (let i = 0; i <= maxRetries; i++) {
try {
return await requestFn()
} catch (err) {
lastError = err instanceof Error ? err : new Error(String(err))
// 只有超时错误才重试
if (lastError.message.includes('timeout') && i < maxRetries) {
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))
continue
}
throw lastError
}
}
throw lastError!
}
// 使用
let data = await requestWithRetry(() => httpClient.get('/api/data'))
五、缓存策略:离线也能用
5.1 三级缓存架构
内存缓存(L1)→ 磁盘缓存(L2)→ 网络请求(L3)
class CacheManager {
private memoryCache: Map<string, { data: object; expireAt: number }> = new Map()
private readonly defaultTTL: number = 5 * 60 * 1000 // 5 分钟
// 内存缓存
setMemory(key: string, data: object, ttl?: number): void {
this.memoryCache.set(key, {
data,
expireAt: Date.now() + (ttl || this.defaultTTL)
})
}
getMemory<T>(key: string): T | null {
let item = this.memoryCache.get(key)
if (!item) return null
if (Date.now() > item.expireAt) {
this.memoryCache.delete(key)
return null
}
return item.data as T
}
// 带缓存的请求
async requestWithCache<T>(
key: string,
requestFn: () => Promise<T>,
options: { useCache?: boolean; ttl?: number } = {}
): Promise<T> {
let { useCache = true, ttl } = options
// 先读内存缓存
if (useCache) {
let cached = this.getMemory<T>(key)
if (cached) {
console.info('命中内存缓存')
return cached
}
}
// 网络请求
let data = await requestFn()
// 写入缓存
if (useCache) {
this.setMemory(key, data as object, ttl)
}
return data
}
}
export const cacheManager = new CacheManager()
5.2 使用缓存的请求
async loadNewsList(): Promise<NewsItem[]> {
return cacheManager.requestWithCache(
'news_list',
() => httpClient.get<NewsItem[]>('/news/list'),
{ ttl: 3 * 60 * 1000 } // 缓存 3 分钟
)
}
六、权限配置
别忘了在 module.json5 中声明网络权限:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
}
}
小结与下篇预告
本文搭建了一套企业级网络层:
HttpClient 封装:统一处理 baseURL、header、超时、token 注入
拦截器机制:请求前自动加 token,响应后统一处理 401
状态管理:loading / error / data 三态让 UI 有据可依
错误处理:分类处理 + 自动重试,提升用户体验
三级缓存:内存缓存让重复访问毫秒级响应
下一篇(第 7 篇)我们将进入 UI 进阶:自定义组件与动画。包括 @BuilderParam 插槽机制、组件复用策略、属性动画和转场动画的实战用法。