[鸿蒙从零到一] HarmonyOS 网络请求与 JSON 解析实战:类型安全、错误分层与状态联动
[鸿蒙从零到一] HarmonyOS 网络请求与 JSON 解析实战:类型安全、错误分层与状态联动
前言
网络请求几乎存在于每个鸿蒙应用中:资讯页要拉取列表,个人中心要读取用户资料,表单页要提交数据。能发出一个 HTTP 请求只是起点,真正影响工程质量的是如何管理权限、超时、状态码、JSON 类型、业务错误和页面状态。
本文以“文章列表”功能为例,使用 HarmonyOS 的 @kit.NetworkKit 和 ArkTS 搭建一条完整链路。我们会先完成基础请求,再逐步抽离类型模型、请求客户端和仓储层,最后让 ArkUI 页面正确处理加载、成功、空数据与失败状态。
请求链路应该分清哪些职责
一个可维护的网络模块通常包含以下层次:
- 页面层:触发加载并展示状态,不解析底层响应。
- 仓储层:表达“获取文章列表”这样的业务动作。
- 客户端层:统一处理 URL、请求头、超时、HTTP 状态码和资源释放。
- 模型层:定义服务端数据和页面所需数据的类型。
如果页面直接调用 http.createHttp(),它很快就会同时承担请求参数、JSON 解析、错误提示和重试逻辑。接口一多,相同代码会散落到各个页面,修改鉴权或超时策略也会变得困难。
配置网络访问权限
应用访问网络前,需要在模块的 module.json5 中声明权限:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
}
}
INTERNET 属于系统授权权限,通常不需要弹窗向用户动态申请,但仍必须在配置文件中声明。生产环境应优先使用 HTTPS,并避免把测试域名、令牌或私有地址硬编码到页面代码中。
完成一个基础 GET 请求
HarmonyOS 提供 http.createHttp() 创建请求对象。请求结束后要调用 destroy() 释放资源,无论成功还是失败都不能遗漏。
import { http } from '@kit.NetworkKit'
async function loadRawArticles(): Promise<string> {
const request = http.createHttp()
try {
const response = await request.request(
'https://api.example.com/v1/articles?page=1&pageSize=20',
{
method: http.RequestMethod.GET,
header: {
'Accept': 'application/json'
},
connectTimeout: 10000,
readTimeout: 15000,
expectDataType: http.HttpDataType.STRING
}
)
if (response.responseCode < 200 || response.responseCode >= 300) {
throw new Error(`HTTP status: ${response.responseCode}`)
}
return response.result as string
} finally {
request.destroy()
}
}
这里显式指定 HttpDataType.STRING,让 JSON 解析由应用控制。这样可以在解析失败时给出清晰错误,也便于先检查响应结构,而不是直接对不确定的对象做类型断言。
为 JSON 响应建立类型模型
假设服务端返回如下数据:
{
"code": 0,
"message": "ok",
"data": {
"items": [
{
"id": "a1001",
"title": "ArkUI 状态管理实践",
"author_name": "Harmony Developer",
"published_at": 1784822400000
}
],
"has_more": false
}
}
ArkTS 接口可以描述预期结构:
interface ArticleDto {
id: string
title: string
author_name: string
published_at: number
}
interface ArticlePageDto {
items: ArticleDto[]
has_more: boolean
}
interface ApiResponse<T> {
code: number
message: string
data: T
}
export interface Article {
id: string
title: string
authorName: string
publishedAt: number
}
DTO 保留服务端字段形式,业务模型使用应用内部统一的命名。两者分开后,服务端字段变化不会直接污染整个 UI 层。
需要注意,JSON.parse() 返回的内容来自运行时,类型断言不会自动验证数据。正式项目可以使用经过团队评估、适配 ArkTS 的校验方案,或者在边界处编写必要的字段检查。
function isArticleDto(value: Object): boolean {
const item = value as Record<string, Object>
return typeof item.id === 'string' &&
typeof item.title === 'string' &&
typeof item.author_name === 'string' &&
typeof item.published_at === 'number'
}
function mapArticle(dto: ArticleDto): Article {
return {
id: dto.id,
title: dto.title,
authorName: dto.author_name,
publishedAt: dto.published_at
}
}
字段校验应放在网络边界,而不是等到页面渲染时才发现 title 不存在。
封装统一的 HttpClient
接下来把重复的请求逻辑集中到客户端中。客户端负责基础地址、公共请求头、状态码、JSON 解析和资源释放。
import { http } from '@kit.NetworkKit'
export class NetworkError extends Error {
constructor(
message: string,
readonly statusCode?: number,
readonly cause?: Object
) {
super(message)
this.name = 'NetworkError'
}
}
export class HttpClient {
constructor(private readonly baseUrl: string) {}
async get<T>(path: string, token?: string): Promise<T> {
const request = http.createHttp()
const headers: Record<string, string> = {
'Accept': 'application/json'
}
if (token !== undefined && token.length > 0) {
headers['Authorization'] = `Bearer ${token}`
}
try {
const response = await request.request(`${this.baseUrl}${path}`, {
method: http.RequestMethod.GET,
header: headers,
connectTimeout: 10000,
readTimeout: 15000,
expectDataType: http.HttpDataType.STRING
})
if (response.responseCode < 200 || response.responseCode >= 300) {
throw new NetworkError(
'服务暂时不可用',
response.responseCode
)
}
try {
return JSON.parse(response.result as string) as T
} catch (error) {
throw new NetworkError('响应数据格式错误', response.responseCode, error)
}
} catch (error) {
if (error instanceof NetworkError) {
throw error
}
throw new NetworkError('网络连接失败', undefined, error as Object)
} finally {
request.destroy()
}
}
}
客户端不要直接弹 Toast,也不要写入页面状态。它不知道当前请求来自列表页、后台同步还是启动流程,只需返回数据或抛出结构明确的错误。
区分 HTTP 错误与业务错误
HTTP 状态成功不代表业务一定成功。服务端可能返回 200,但 JSON 中的 code 表示登录失效、参数错误或访问受限。因此仓储层还要检查业务响应。
export class ApiError extends Error {
constructor(
readonly code: number,
message: string
) {
super(message)
this.name = 'ApiError'
}
}
export class ArticleRepository {
constructor(private readonly client: HttpClient) {}
async list(page: number, pageSize: number): Promise<Article[]> {
const response = await this.client.get<ApiResponse<ArticlePageDto>>(
`/v1/articles?page=${page}&pageSize=${pageSize}`
)
if (response.code !== 0) {
throw new ApiError(response.code, response.message)
}
if (!Array.isArray(response.data.items)) {
throw new NetworkError('文章列表格式错误')
}
return response.data.items.map((item: ArticleDto) => mapArticle(item))
}
}
这种分层让调用方能按错误性质处理:连接失败可以提示检查网络,鉴权失效可以进入登录流程,服务端异常可以展示稍后重试,而解析异常应记录必要的诊断信息并关注接口兼容性。
POST 请求与请求体序列化
提交数据时,应明确请求类型并使用 JSON.stringify() 序列化请求体:
interface CreateArticleInput {
title: string
content: string
}
async function createArticle(input: CreateArticleInput): Promise<void> {
const request = http.createHttp()
try {
const response = await request.request(
'https://api.example.com/v1/articles',
{
method: http.RequestMethod.POST,
header: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
extraData: JSON.stringify(input),
connectTimeout: 10000,
readTimeout: 15000,
expectDataType: http.HttpDataType.STRING
}
)
if (response.responseCode < 200 || response.responseCode >= 300) {
throw new NetworkError('提交失败', response.responseCode)
}
} finally {
request.destroy()
}
}
不要手工拼接 JSON 字符串。JSON.stringify() 能正确处理引号和转义字符,也能让请求模型保持清晰。提交前仍要进行业务校验,例如标题为空或正文超长时,不应依赖服务端兜底。
用页面状态驱动 ArkUI
网络页面至少要区分加载、成功、空数据和失败。可以先定义一个简单状态模型:
enum LoadStatus {
IDLE,
LOADING,
SUCCESS,
EMPTY,
ERROR
}
@Entry
@Component
struct ArticleListPage {
@State status: LoadStatus = LoadStatus.IDLE
@State articles: Article[] = []
@State errorMessage: string = ''
private readonly repository = new ArticleRepository(
new HttpClient('https://api.example.com')
)
async aboutToAppear(): Promise<void> {
await this.loadArticles()
}
private async loadArticles(): Promise<void> {
this.status = LoadStatus.LOADING
this.errorMessage = ''
try {
const result = await this.repository.list(1, 20)
this.articles = result
this.status = result.length === 0 ? LoadStatus.EMPTY : LoadStatus.SUCCESS
} catch (error) {
this.errorMessage = error instanceof Error ? error.message : '加载失败'
this.status = LoadStatus.ERROR
}
}
@Builder
private content() {
if (this.status === LoadStatus.LOADING) {
LoadingProgress()
} else if (this.status === LoadStatus.ERROR) {
Column({ space: 12 }) {
Text(this.errorMessage)
Button('重新加载').onClick(() => this.loadArticles())
}
} else if (this.status === LoadStatus.EMPTY) {
Text('暂无文章')
} else {
List() {
ForEach(this.articles, (article: Article) => {
ListItem() {
Column({ space: 6 }) {
Text(article.title).fontSize(18).fontWeight(FontWeight.Medium)
Text(article.authorName).fontSize(14).fontColor('#666666')
}
.width('100%')
.alignItems(HorizontalAlign.Start)
.padding(16)
}
}, (article: Article) => article.id)
}
}
}
build() {
Column() {
this.content()
}
.width('100%')
.height('100%')
}
}
状态切换应由一次加载流程集中管理。不要只维护一个 isLoading 布尔值,否则空数据、旧数据刷新失败和首次加载失败很容易混在一起。
处理重复请求与页面离开
搜索框、筛选器和快速点击可能触发多个并发请求。旧请求比新请求更晚返回时,会把页面覆盖成过期结果。简单场景可以用递增标识只接收最近一次结果:
private requestVersion: number = 0
private async search(keyword: string): Promise<void> {
const currentVersion = ++this.requestVersion
this.status = LoadStatus.LOADING
try {
const result = await this.repository.search(keyword)
if (currentVersion !== this.requestVersion) {
return
}
this.articles = result
this.status = result.length === 0 ? LoadStatus.EMPTY : LoadStatus.SUCCESS
} catch (error) {
if (currentVersion !== this.requestVersion) {
return
}
this.status = LoadStatus.ERROR
}
}
页面销毁后也要避免继续更新页面状态。具体项目可以结合组件生命周期、请求取消能力或状态容器统一管理,核心原则是过期结果不能写回当前界面。
重试、日志与安全边界
重试并不是请求失败后无条件再发一次。GET 等幂请求在临时网络故障时可以有限重试,并加入间隔;创建订单、支付、提交表单等操作若没有幂等键,自动重试可能产生重复数据。
日志也要控制边界:
- 可以记录请求路径、耗时、状态码和错误类别。
- 不应记录访问令牌、Cookie、密码和完整身份证件信息。
- 请求体和响应体可能含隐私数据,生产日志默认不要全文输出。
- 面向用户的提示应简洁,底层异常细节留给受控诊断日志。
基础地址、环境和公共请求头建议通过构建配置或依赖注入提供。鉴权令牌应从合适的安全存储读取,不要写死在源码中。
常见问题
忘记销毁 HttpRequest
每次创建的请求对象都应在 finally 中调用 destroy()。只在成功分支释放,会让异常路径持续占用资源。
只判断业务 code
代理错误、网关故障和服务端异常可能返回非成功 HTTP 状态,甚至返回 HTML。应先检查状态码,再解析 JSON 和判断业务码。
用类型断言代替运行时校验
as ApiResponse<T> 只影响编译阶段,不会修复缺失字段。关键接口应在数据边界验证数组、必填字段和基础类型。
把原始错误直接展示给用户
底层异常可能包含实现细节,且对用户没有帮助。应将其转换为明确的业务提示,同时保留可控的诊断上下文。
每个页面各写一套请求代码
重复代码会导致超时、请求头和错误规则不一致。统一客户端处理协议细节,仓储层处理业务语义,页面只消费结果和状态。
小结
HarmonyOS 网络功能的核心不只是调用 request(),而是建立清晰的数据边界。客户端统一处理 HTTP、超时、JSON 和资源释放,仓储层判断业务结果并完成模型转换,ArkUI 页面则通过明确状态展示加载结果。
在此基础上,再按业务风险加入鉴权刷新、缓存、分页、取消、有限重试和监控,网络模块才能从“可以请求”成长为稳定、可测试、可演进的工程能力。

浙公网安备 33010602011771号