HarmonyOS开发中RESTful API封装:网络层架构设计
从"一把梭"到"优雅架构",聊聊鸿蒙网络层的设计哲学
一、为什么需要封装网络层?
说实话,刚接触鸿蒙开发那会儿,我写网络请求是这样的:
typescript
// 每个页面都这么写...
http.request('https://api.example.com/user', {
method: http.RequestMethod.GET,
header: { 'Content-Type': 'application/json' }
}, (err, data) => {
if (err) {
console.error('请求失败');
return;
}
// 处理数据...
});
写着写着就发现问题了------代码重复得让人心慌。每个接口都要写一遍请求头配置、错误处理、数据解析,改个 baseUrl 得全局搜索替换,加个 token 认证得改几十个文件...
更要命的是,当后端接口从 /api/v1/user 升级到 /api/v2/user 时,我差点把键盘砸了。
这就是为什么我们需要网络层架构设计。它不是炫技,是救命稻草。
一个好的网络层应该具备什么能力?
- 统一配置:baseUrl、超时时间、公共请求头,一处配置全局生效
- 请求拦截:发请求前能加点料(比如自动注入 token)
- 响应拦截:收到数据后能做点事(比如统一错误处理)
- 类型安全:TypeScript 时代了,返回值得有类型
- 易于扩展:想加个缓存?想加个 mock?不动老代码
二、核心原理:分层架构设计
网络层不是"一个类打天下",而是分层的协作体系。我们采用经典的三层架构:
各层职责清晰:
- 应用层:只管调用 API,不关心网络细节
- 业务层:封装具体接口,定义业务类型
- 网络层:处理请求/响应,拦截器链
- 基礎层:配置、错误、缓存等基础设施
三、代码实战:从零搭建网络层
示例1:核心 HttpClient 类
这是整个网络层的"心脏",负责发起请求和调度拦截器:
typescript
// network/HttpClient.ets
import http from '@ohos.net.http';
import { RequestConfig, Response, Interceptor } from './types';
/**
* HttpClient - 网络请求核心类
* 职责:发起HTTP请求、管理拦截器链、统一错误处理
*/
export class HttpClient {
private baseUrl: string;
private timeout: number = 30000; // 默认30秒超时
private interceptors: Interceptor[] = []; // 拦截器链
private defaultHeaders: Record<string, string> = {};
constructor(config: { baseUrl: string; timeout?: number }) {
this.baseUrl = config.baseUrl;
if (config.timeout) {
this.timeout = config.timeout;
}
// 初始化默认请求头
this.defaultHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json'
};
}
/**
* 添加拦截器到拦截器链
* @param interceptor 拦截器实例
*/
use(interceptor: Interceptor): void {
this.interceptors.push(interceptor);
}
/**
* 核心请求方法
* @param config 请求配置
* @returns Promise<Response<T>>
*/
async request<T>(config: RequestConfig): Promise<Response<T>> {
// 1. 合并配置
const mergedConfig = this.mergeConfig(config);
// 2. 执行请求拦截器
let processedConfig = mergedConfig;
for (const interceptor of this.interceptors) {
if (interceptor.beforeRequest) {
processedConfig = await interceptor.beforeRequest(processedConfig);
}
}
// 3. 发起实际请求
const httpResponse = await this.executeRequest(processedConfig);
// 4. 执行响应拦截器
let response: Response<T> = {
data: httpResponse.result as T,
status: httpResponse.responseCode,
headers: httpResponse.header,
config: processedConfig
};
for (const interceptor of this.interceptors) {
if (interceptor.afterResponse) {
response = await interceptor.afterResponse(response);
}
}
return response;
}
/**
* 合并请求配置
* @private
*/
private mergeConfig(config: RequestConfig): RequestConfig {
return {
url: this.baseUrl + config.url,
method: config.method || 'GET',
headers: { ...this.defaultHeaders, ...config.headers },
params: config.params,
data: config.data,
timeout: config.timeout || this.timeout
};
}
/**
* 执行实际HTTP请求
* @private
*/
private async executeRequest(config: RequestConfig): Promise<http.HttpResponse> {
const httpRequest = http.createHttp();
try {
const response = await httpRequest.request(config.url, {
method: this.getMethod(config.method),
header: config.headers,
extraData: config.data,
connectTimeout: config.timeout,
readTimeout: config.timeout
});
return response;
} finally {
// 确保销毁HTTP对象,避免内存泄漏
httpRequest.destroy();
}
}
/**
* 转换请求方法枚举
* @private
*/
private getMethod(method: string): http.RequestMethod {
const methodMap: Record<string, http.RequestMethod> = {
'GET': http.RequestMethod.GET,
'POST': http.RequestMethod.POST,
'PUT': http.RequestMethod.PUT,
'DELETE': http.RequestMethod.DELETE,
'PATCH': http.RequestMethod.PATCH
};
return methodMap[method] || http.RequestMethod.GET;
}
// 快捷方法:GET请求
get<T>(url: string, params?: Record<string, any>): Promise<Response<T>> {
return this.request<T>({ url, method: 'GET', params });
}
// 快捷方法:POST请求
post<T>(url: string, data?: any): Promise<Response<T>> {
return this.request<T>({ url, method: 'POST', data });
}
// 快捷方法:PUT请求
put<T>(url: string, data?: any): Promise<Response<T>> {
return this.request<T>({ url, method: 'PUT', data });
}
// 快捷方法:DELETE请求
delete<T>(url: string): Promise<Response<T>> {
return this.request<T>({ url, method: 'DELETE' });
}
}
示例2:类型定义与拦截器接口
类型安全是网络层的灵魂,先定义好契约:
typescript
// network/types.ets
/**
* 请求配置接口
*/
export interface RequestConfig {
url: string; // 请求URL(相对路径)
method?: string; // 请求方法
headers?: Record<string, string>; // 请求头
params?: Record<string, any>; // URL参数
data?: any; // 请求体数据
timeout?: number; // 超时时间
}
/**
* 响应结构接口
*/
export interface Response<T = any> {
data: T; // 响应数据
status: number; // HTTP状态码
headers: Record<string, string>; // 响应头
config: RequestConfig; // 原始请求配置
}
/**
* 拦截器接口
* 拦截器可以在请求发出前和响应返回后进行拦截处理
*/
export interface Interceptor {
/**
* 请求拦截:在请求发出前执行
* 可用于添加认证token、记录日志等
*/
beforeRequest?(config: RequestConfig): Promise<RequestConfig>;
/**
* 响应拦截:在响应返回后执行
* 可用于统一错误处理、数据转换等
*/
afterResponse?<T>(response: Response<T>): Promise<Response<T>>;
}
/**
* API错误类
* 封装网络请求中的各类错误
*/
export class ApiError extends Error {
code: number; // 错误码
status: number; // HTTP状态码
data: any; // 错误响应数据
constructor(message: string, code: number, status: number = 0, data?: any) {
super(message);
this.code = code;
this.status = status;
this.data = data;
this.name = 'ApiError';
}
}
/**
* 错误码枚举
*/
export enum ErrorCode {
NETWORK_ERROR = -1, // 网络错误
TIMEOUT = -2, // 请求超时
SERVER_ERROR = 500, // 服务器错误
NOT_FOUND = 404, // 资源不存在
UNAUTHORIZED = 401, // 未授权
FORBIDDEN = 403, // 禁止访问
BAD_REQUEST = 400, // 请求错误
}
示例3:业务API封装实战
有了 HttpClient,封装业务接口就像搭积木一样简单:
typescript
// api/UserAPI.ets
import { HttpClient, Response } from '../network';
import { User, LoginParams, LoginResult } from '../models/user';
/**
* 用户相关API
* 封装所有用户模块的网络请求
*/
export class UserAPI {
private http: HttpClient;
constructor(http: HttpClient) {
this.http = http;
}
/**
* 用户登录
* @param params 登录参数(用户名、密码)
* @returns 登录结果(token、用户信息)
*/
async login(params: LoginParams): Promise<Response<LoginResult>> {
// 发送POST请求到 /auth/login
const response = await this.http.post<LoginResult>('/auth/login', params);
// 登录成功后,可以将token存储到本地
if (response.data.token) {
// TODO: 存储token到Preferences
console.info('[UserAPI] 登录成功,token已获取');
}
return response;
}
/**
* 获取当前用户信息
* @returns 用户详细信息
*/
async getCurrentUser(): Promise<Response<User>> {
return await this.http.get<User>('/user/current');
}
/**
* 更新用户资料
* @param user 用户信息(部分字段)
* @returns 更新后的完整用户信息
*/
async updateProfile(user: Partial<User>): Promise<Response<User>> {
return await this.http.put<User>('/user/profile', user);
}
/**
* 修改密码
* @param oldPassword 旧密码
* @param newPassword 新密码
*/
async changePassword(oldPassword: string, newPassword: string): Promise<Response<void>> {
return await this.http.post<void>('/user/password', {
oldPassword,
newPassword
});
}
/**
* 用户注销登录
*/
async logout(): Promise<Response<void>> {
const response = await this.http.post<void>('/auth/logout');
// 清除本地token
// TODO: 从Preferences中移除token
console.info('[UserAPI] 已注销登录');
return response;
}
}
示例4:全局网络层初始化
在应用启动时初始化网络层,配置拦截器:
typescript
// network/index.ets
import { HttpClient } from './HttpClient';
import { AuthInterceptor, LogInterceptor, ErrorInterceptor } from './interceptors';
/**
* 全局HttpClient实例
* 整个应用共享这一个实例
*/
let httpClient: HttpClient | null = null;
/**
* 初始化网络层
* 在应用启动时调用(EntryAbility.onCreate)
*/
export function initNetwork(): HttpClient {
if (httpClient) {
return httpClient;
}
// 创建HttpClient实例
httpClient = new HttpClient({
baseUrl: 'https://api.myapp.com/v1', // API基础地址
timeout: 30000 // 30秒超时
});
// 注册拦截器(按顺序执行)
httpClient.use(new LogInterceptor()); // 日志拦截器
httpClient.use(new AuthInterceptor()); // 认证拦截器
httpClient.use(new ErrorInterceptor()); // 错误拦截器
console.info('[Network] 网络层初始化完成');
return httpClient;
}
/**
* 获取全局HttpClient实例
*/
export function getHttpClient(): HttpClient {
if (!httpClient) {
throw new Error('网络层未初始化,请先调用 initNetwork()');
}
return httpClient;
}
// 导出便捷方法
export { HttpClient } from './HttpClient';
export * from './types';
四、踩坑与注意事项
坑1:HTTP对象未销毁导致内存泄漏
问题 :每次请求都 http.createHttp() 创建新对象,但忘记销毁,导致内存持续增长。
解决 :在 finally 块中确保销毁:
typescript
// ❌ 错误写法
const httpRequest = http.createHttp();
const response = await httpRequest.request(url, options);
// 忘记销毁!
// ✅ 正确写法
const httpRequest = http.createHttp();
try {
const response = await httpRequest.request(url, options);
return response;
} finally {
httpRequest.destroy(); // 无论成功失败都销毁
}
坑2:baseUrl 拼接错误
问题 :baseUrl 以 / 结尾,url 以 / 开头,导致双斜杠。
解决:标准化处理:
typescript
private normalizeUrl(baseUrl: string, url: string): string {
const base = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
const path = url.startsWith('/') ? url : `/${url}`;
return base + path;
}
坑3:并发请求的竞态问题
问题:快速切换页面时,旧页面的请求先返回,覆盖了新页面数据。
解决:使用请求 ID 或 AbortController 取消旧请求(下篇详解)。
坑4:响应数据类型不匹配
问题 :后端返回 { code: 0, data: {...} },但直接把整个响应当业务数据用了。
解决:在响应拦截器中统一解析:
typescript
// 假设后端统一返回格式:{ code: number, message: string, data: T }
afterResponse<T>(response: Response<any>): Promise<Response<T>> {
const { code, message, data } = response.data;
if (code !== 0) {
throw new ApiError(message, code, response.status);
}
// 只返回业务数据部分
return {
...response,
data: data as T
};
}
五、HarmonyOS 6 适配要点
HarmonyOS 6 对网络模块有一些重要更新:
1. HTTP 模块 API 变更
typescript
// HarmonyOS 5.x
import http from '@ohos.net.http';
// HarmonyOS 6(推荐)
import { http } from '@kit.NetworkKit';
2. 请求配置增强
HarmonyOS 6 新增了更多配置项:
typescript
// HarmonyOS 6 新增配置
const options: http.HttpRequestOptions = {
method: http.RequestMethod.POST,
header: { 'Content-Type': 'application/json' },
extraData: { key: 'value' },
// ✨ 新增:期望的响应类型
expectingDataType: http.HttpDataType.OBJECT, // 自动解析JSON
// ✨ 新增:使用HTTP2
usingProtocol: http.HttpProtocol.HTTP2,
// ✨ 新增:优先级
priority: http.HttpRequestPriority.HIGH
};
3. 响应数据直接解析
typescript
// HarmonyOS 5.x 需要手动解析
const data = JSON.parse(response.result as string);
// HarmonyOS 6 自动解析(设置 expectingDataType 后)
const data = response.result; // 已经是对象了!
4. 错误处理增强
typescript
// HarmonyOS 6 提供更详细的错误信息
try {
const response = await httpRequest.request(url, options);
} catch (error) {
// error 包含更详细的错误类型
if (error.code === 2300001) {
console.error('网络不可用');
} else if (error.code === 2300002) {
console.error('连接超时');
} else if (error.code === 2300003) {
console.error('协议错误');
}
}
六、总结
搭建一个优雅的网络层,核心是分层解耦:
- HttpClient 负责请求调度,是"心脏"
- 拦截器链 负责横切关注点,是"血管"
- 业务API 负责接口封装,是"四肢"
- 类型定义 负责契约约束,是"骨架"
记住几个关键点:
- ✅ HTTP 对象用完必须销毁
- ✅ 拦截器按顺序执行,注意依赖关系
- ✅ 类型定义要完整,别用 any
- ✅ 错误处理要统一,别到处 try-catch
下一篇我们深入拦截器链的设计,看看如何优雅地实现日志、认证、重试等功能。
💡 提示哦 :网络层代码建议放在
common/network/目录,业务API按模块放在services/目录,保持职责清晰。