网络请求与数据交互:http 模块、拦截器与状态封装
几乎所有应用都要联网。鸿蒙通过
@kit.NetworkKit的http模块发起请求。本文讲清 GET/POST、请求头、文件上传、错误处理、JSON 封装,以及如何用拦截器与状态机把网络层做得可维护、可测试。
一、权限与基础请求
网络请求需在 module.json5 声明权限:
json5
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" }
]
最简 GET 请求:
typescript
import { http } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
async function getProducts(): Promise<Product[]> {
const req = http.createHttp();
try {
const resp = await req.request(
'https://api.example.com/products',
{ method: http.RequestMethod.GET, connectTimeout: 10000 }
);
if (resp.responseCode === 200) {
return JSON.parse(resp.result as string) as Product[];
}
throw new Error('HTTP ' + resp.responseCode);
} finally {
req.destroy(); // 务必释放,避免资源泄漏
}
}
http.createHttp() 返回的对象用完要 destroy(),否则连接泄漏。
二、POST 与请求体
typescript
async function login(username: string, pwd: string): Promise<void> {
const req = http.createHttp();
const resp = await req.request('https://api.example.com/login', {
method: http.RequestMethod.POST,
header: { 'Content-Type': 'application/json' },
extraData: JSON.stringify({ username, password: pwd }),
expectDataType: http.HttpDataType.OBJECT // 直接解析为对象
});
req.destroy();
const body = resp.result as Product; // 已是对象
console.info('登录结果:' + JSON.stringify(body));
}
expectDataType: OBJECT 让框架自动 JSON 解析,省去手写 JSON.parse。
三、统一封装:ApiClient
把重复逻辑(baseURL、token 注入、错误统一处理)抽成单例客户端:
typescript
class ApiClient {
private baseUrl = 'https://api.example.com';
private token: string = '';
setToken(t: string): void { this.token = t; }
async request<T>(path: string, options: http.HttpOptions): Promise<T> {
const req = http.createHttp();
try {
const resp = await req.request(this.baseUrl + path, {
...options,
header: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + this.token,
...(options.header ?? {})
}
});
if (resp.responseCode >= 200 && resp.responseCode < 300) {
return resp.result as T;
}
// 统一错误
if (resp.responseCode === 401) {
throw new ApiError('未授权', 401);
}
throw new ApiError('请求失败', resp.responseCode);
} finally {
req.destroy();
}
}
get<T>(path: string): Promise<T> {
return this.request<T>(path, { method: http.RequestMethod.GET });
}
post<T>(path: string, data: Object): Promise<T> {
return this.request<T>(path, {
method: http.RequestMethod.POST,
extraData: JSON.stringify(data)
});
}
}
export const api = new ApiClient();
class ApiError extends Error {
code: number;
constructor(msg: string, code: number) { super(msg); this.code = code; }
}
这样业务层只写 api.get<Product[]>('/products'),token 与错误自动处理。
四、拦截器思路
ArkTS 没有内置拦截器,但可在 ApiClient 里用中间件函数模拟:
typescript
type Interceptor = (req: http.HttpOptions) => http.HttpOptions;
private interceptors: Interceptor[] = [
(opt) => ({ ...opt, header: { ...opt.header, 'X-Client': 'HarmonyOS' } })
];
async request<T>(path: string, options: http.HttpOptions): Promise<T> {
let opt = options;
this.interceptors.forEach(i => { opt = i(opt); });
// ... 发请求
}
需要统一加签名、埋点、日志时,往 interceptors 数组加函数即可。
五、文件上传与下载
上传(multipart 形式可用 request 的 multipart 字段,或第三方库)。下载用 request.downloadFile:
typescript
import { request } from '@kit.ArkTS';
async function download(url: string): Promise<string> {
const task = await request.downloadFile(getContext(this), {
url,
filePath: getContext(this).filesDir + '/file.apk'
});
return new Promise((resolve, reject) => {
task.on('complete', () => resolve('下载完成'));
task.on('fail', (err: BusinessError) => reject(err));
});
}
六、网络状态监听
用 @kit.NetworkKit 的 connection 监听网络变化,断网时提示:
typescript
import { connection } from '@kit.NetworkKit';
connection.getDefaultNet().then((netHandle) => {
connection.getNetCapabilities(netHandle).then((cap) => {
const hasNet = cap.networkCap !== undefined;
console.info('有网络:' + hasNet);
});
});
// 订阅变化
connection.on('netAvailable', () => { console.info('网络恢复'); });
connection.on('netUnavailable', () => { console.info('网络断开'); });
七、在组件中优雅使用
结合状态管理,把"加载中/数据/错误"三态写出来:
typescript
@Entry
@Component
struct ProductList {
@State loading: boolean = false;
@State list: Product[] = [];
@State error: string = '';
aboutToAppear(): void { this.load(); }
async load(): Promise<void> {
this.loading = true;
this.error = '';
try {
this.list = await api.get<Product[]>('/products');
} catch (e) {
this.error = (e as Error).message;
} finally {
this.loading = false;
}
}
build() {
Column() {
if (this.loading) {
LoadingProgress()
} else if (this.error !== '') {
Text('加载失败:' + this.error)
Button('重试').onClick(() => { this.load(); })
} else {
List() {
ForEach(this.list, (p: Product) => {
ListItem() { Text(p.name) }
}, (p: Product) => p.id.toString())
}
}
}
}
}
这种"三态"模式是网络数据展示的标准写法。
八、防抖与竞态
快速输入搜索时,要防抖 + 取消旧请求,避免旧结果覆盖新结果:
typescript
private timer: number = -1;
onSearchChange(v: string): void {
clearTimeout(this.timer);
this.timer = setTimeout(() => { this.doSearch(v); }, 300);
}
http 请求对象可调用 req.destroy() 中断进行中的请求,处理竞态更彻底。
九、Mock 与测试
开发期可用本地 Mock 数据,避免依赖后端:
typescript
async function getProducts(): Promise<Product[]> {
if (BuildProfile.DEBUG) {
return [{ id: 1, name: 'Mock商品', price: 99 }]; // 本地假数据
}
return api.get<Product[]>('/products');
}
配合接口契约,前后端可并行开发。
十、安全注意事项
- 不要在客户端硬编码密钥 ,敏感信息放服务端或用
Asset存储。 - HTTPS 必须开启证书校验(默认开启,勿随意关闭)。
- 用户输入防注入:参数走 body,不拼 URL。
- token 用
@StorageLink+ 安全存储,别明文写文件。
十一、常见坑
| 现象 | 原因 | 解决 |
|---|---|---|
| 请求无响应 | 未申 INTERNET 权限 | module.json5 加权限 |
| 连接泄漏 | 忘记 destroy | finally 中 destroy |
| 中文乱码 | 未设 UTF-8 | header 加 charset |
| 解析报错 | result 是 string 不是对象 | 用 expectDataType: OBJECT |
| 真机失败模拟器成功 | 模拟器无网络限制 | 检查网络/代理 |
十二、总结
网络层的质量直接决定应用稳定性。本文从基础 http 请求,到统一 ApiClient 封装、拦截器、文件下载、网络监听、三态展示、防抖竞态与安全防护,给出了一套可落地的方案。核心原则:统一出口(一个 ApiClient)、统一错误、统一 token、请求必 destroy。把网络细节收进封装层,业务页面只关心"拿数据",代码既干净又稳健。
