HarmonyOS开发中RESTful API封装:网络层架构设计

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 时,我差点把键盘砸了。

这就是为什么我们需要网络层架构设计。它不是炫技,是救命稻草。

一个好的网络层应该具备什么能力?

  1. 统一配置:baseUrl、超时时间、公共请求头,一处配置全局生效
  2. 请求拦截:发请求前能加点料(比如自动注入 token)
  3. 响应拦截:收到数据后能做点事(比如统一错误处理)
  4. 类型安全:TypeScript 时代了,返回值得有类型
  5. 易于扩展:想加个缓存?想加个 mock?不动老代码

二、核心原理:分层架构设计

网络层不是"一个类打天下",而是分层的协作体系。我们采用经典的三层架构

flowchart TB subgraph 应用层[&#34;📱 应用层&#34;] A1[页面组件] A2[ViewModel] end subgraph 业务层[&#34;💼 业务层&#34;] B1[UserAPI] B2[ProductAPI] B3[OrderAPI] end subgraph 网络层[&#34;🌐 网络层&#34;] C1[HttpClient<br/>核心请求类] C2[RequestInterceptor<br/>请求拦截器] C3[ResponseInterceptor<br/>响应拦截器] end subgraph 基硎层[&#34;⚙️ 基硎层&#34;] D1[Config<br/>配置管理] D2[ErrorHandler<br/>错误处理] D3[CacheManager<br/>缓存管理] end A1 --> A2 A2 --> B1 A2 --> B2 A2 --> B3 B1 --> C1 B2 --> C1 B3 --> C1 C1 --> C2 C2 --> C3 C3 --> D1 C3 --> D2 C3 --> D3 classDef primary fill:#4A90E2,stroke:#2E5C8A,color:#fff classDef warning fill:#F5A623,stroke:#C17D10,color:#fff classDef error fill:#E74C3C,stroke:#C0392B,color:#fff classDef info fill:#7ED321,stroke:#5BA318,color:#fff class A1,A2 primary class B1,B2,B3 warning class C1,C2,C3 error class D1,D2,D3 info

各层职责清晰

  • 应用层:只管调用 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('协议错误');
  }
}

六、总结

搭建一个优雅的网络层,核心是分层解耦

  1. HttpClient 负责请求调度,是"心脏"
  2. 拦截器链 负责横切关注点,是"血管"
  3. 业务API 负责接口封装,是"四肢"
  4. 类型定义 负责契约约束,是"骨架"

记住几个关键点:

  • ✅ HTTP 对象用完必须销毁
  • ✅ 拦截器按顺序执行,注意依赖关系
  • ✅ 类型定义要完整,别用 any
  • ✅ 错误处理要统一,别到处 try-catch

下一篇我们深入拦截器链的设计,看看如何优雅地实现日志、认证、重试等功能。


💡 提示哦 :网络层代码建议放在 common/network/ 目录,业务API按模块放在 services/ 目录,保持职责清晰。

相关推荐
用户497863050731 天前
(一)小红的数组操作
算法·编程语言
Rust研习社1 天前
这 8 个 Rust 学习资源值得每个新手收藏起来
后端·rust·编程语言
Moonbit1 天前
MoonBit ×CCF开源创新大赛 倒计时24天!快来提交你的作品
程序员·编程语言
Rust研习社2 天前
Rust 错误处理的黄金搭档:一个定义错误,一个传播错误
后端·rust·编程语言
2601_951643723 天前
1 章 C语言概述
c语言·编程语言·历史·标准·优缺点
大熊猫侯佩4 天前
SwiftData 迁移深度指南:从入门到“填坑”(下集)
数据库·swift·编程语言
大熊猫侯佩4 天前
SwiftData 迁移深度指南:从入门到“填坑”(上集)
数据库·swift·编程语言
大熊猫侯佩5 天前
Swift 6.4 的 Ref / MutableRef 大揭秘:给值类型开一扇“安全的小窗”
ios·swift·编程语言
大熊猫侯佩6 天前
WWDC26 最被忽视的王炸:告别“伪并发”陷阱,Swift 6.4 的 async defer
ios·swift·编程语言