前言

对于每一位从 Web 前端转型到鸿蒙开发的工程师来说,网络请求往往是第一个让人感到水土不服的地方。在原生开发中,我们习惯了使用系统提供的 http 模块,虽然它功能强大,但那繁琐的配置、基于回调或原始 Promise 的调用方式,以及手动处理 JSON 序列化的枯燥过程,总让人怀念起在前端世界里使用 Axios 那种行云流水的体验。

你是否想过,如果能把 Axios 的拦截器、统一配置和简洁 API 带到鸿蒙应用开发中,该是多么惬意的一件事?

好消息是,得益于 OpenHarmony 的开放生态,@ohos/axios 库已经成为了鸿蒙网络开发的标准配置。但仅仅安装一个库是远远不够的。

在企业级的应用架构中,我们需要构建一道坚固的网络通信防线:我们需要一个统一的入口来管理超时和 BaseURL,需要一对敏锐的拦截器来自动注入 Token 和处理全局异常,更需要利用 TypeScript 强大的泛型系统来确保每一条回来的数据都是类型安全的。

今天,我们就来像搭建地基一样,一步步封装出一套健壮、优雅的网络请求工具类。

一、 为什么我们需要二次封装 Axios

直接在业务组件中引入 axios 并调用 axios.get 看起来是最快的路径,但这种“快”是以后期的维护噩梦为代价的。想象一下,当后端突然通知 API 的域名变更了,或者安全部门要求在所有请求头里加一个加密签名字段,如果你在几十个页面里散落了上百个 axios 调用,那你可能得加班改到天亮。

封装的核心意义在于 收口。我们通过创建一个单例的 NetworkService 类,将所有的网络行为约束在一个可控的范围内。在这个类中,我们维护一个 Axios 实例,并配置好基础的 connectTimeout(连接超时)和 readTimeout(读取超时)。在 API 20 的环境下,移动网络的不稳定性要求我们对超时有着精细的控制,通常我们会设置为 10 秒左右。此外,通过封装,我们可以屏蔽掉底层库的差异。万一哪天鸿蒙推出了一个比 Axios 更好的官方网络库,我们只需要修改这个工具类的内部实现,而业务层的代码完全不需要改动。

二、 拦截器的艺术 Token 的注入与无感刷新

拦截器(Interceptors)是 Axios 的灵魂,也是我们处理鉴权逻辑的最佳战场。在 请求拦截器 中,我们扮演的是“守门员”的角色。每一次网络请求发出之前,都会经过这里。我们需要从 AppStoragePersistentStorage 中取出当前用户的 Token,并将其挂载到 Authorization 请求头上。这不仅简化了业务代码,更避免了因为漏传 Token 而导致的鉴权失败。

相比之下,响应拦截器 的逻辑则更为复杂。后端返回的数据通常包裹着一层外壳,比如 { code: 200, data: {...}, message: "success" }。业务层关心的往往只有 data 里面的内容。我们可以在响应拦截器中直接解构这层外壳,如果 code 是 200,就直接返回 data;如果不是,就抛出一个自定义异常。这样,我们在页面里拿到的就是干干净净的业务数据。

最棘手的场景莫过于 Token 过期。当后端返回 401 状态码时,如果我们直接让用户跳回登录页,体验是非常糟糕的。高级的封装会在响应拦截器里捕获这个 401 错误,暂停当前的所有请求,在后台悄悄发起一个“刷新 Token”的请求。一旦刷新成功,更新本地存储的 Token,并自动 重发 刚才失败的那个请求。对于用户来说,这一切都是无感的,他只会觉得应用稍微顿了一下,然后继续正常运行。这才是专业级应用该有的体验。

三、 泛型接口:给数据穿上防弹衣

TypeScript 的最大优势在于类型安全,但在网络请求中,很多人却把这个优势丢掉了,满屏的 any 让人看着心惊肉跳。当我们发起一个 getUserInfo 请求时,IDE 应该能智能提示出返回值里有 nameage 字段,而不是让我们去猜。

为了实现这一点,我们需要利用 TypeScript 的 泛型(Generics)。首先,我们要定义一个标准的后端响应接口 BaseResponse<T>,它包含 codemessage 和泛型字段 data: T。然后,在我们的封装方法中,我们也接收一个泛型 T,并将其传递给 Axios。这样一来,当我们调用 request<UserInfo>('/api/user') 时,返回值就会自动被推导为 Promise<UserInfo>。这种类型约束就像是给数据穿上了防弹衣,任何字段拼写错误或者类型不匹配,在编译阶段就会被红线标出,而不是等到运行时才崩溃。

import axios, { AxiosInstance, AxiosResponse, InternalAxiosRequestConfig, AxiosError } from '@ohos/axios';
import { promptAction } from '@kit.ArkUI';

// -------------------------------------------------------------
// 1. 定义标准数据接口
// -------------------------------------------------------------

interface UserInfo {
  id: number;
  name: string;
  username: string;
  email: string;
  avatar?: string;
  level?: number;
}

// -------------------------------------------------------------
// 2. 封装网络服务类
// -------------------------------------------------------------
class NetworkService {
  private instance: AxiosInstance;
  private readonly BASE_URL = 'https://jsonplaceholder.typicode.com';

  constructor() {
    this.instance = axios.create({
      baseURL: this.BASE_URL,
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json',
      }
    });

    this.initInterceptors();
  }

  // 初始化拦截器
  private initInterceptors(): void {
    // --- 请求拦截器 ---
    this.instance.interceptors.request.use(
      (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
        const token = AppStorage.get<string>('userToken');
        if (token) {
          config.headers['Authorization'] = `Bearer ${token}`;
        }
        return config;
      },
      (error: AxiosError): Promise<never> => {
        return Promise.reject(error);
      }
    );

    // --- 响应拦截器 ---
    this.instance.interceptors.response.use(
      (response: AxiosResponse): AxiosResponse => {
        const status = response.status;
        if (status === 200 || status === 201) {
          return response;
        }

        // 如果 HTTP 状态码不对,抛出错误
        throw new Error(`HTTP Error: ${status}`);
      },
      (error: AxiosError): Promise<never> => {
        let message = '';
        if (error.response?.status === 404) {
          message = '资源不存在 (404)';
        } else if (error.message.includes('timeout')) {
          message = '网络请求超时';
        } else {
          message = '网络连接异常';
        }

        promptAction.showToast({ message: message });
        return Promise.reject(error);
      }
    );
  }

  public async get<T>(url: string, params?: Record<string, Object>): Promise<T> {
    const response = await this.instance.get<T, AxiosResponse<T, Object>, Object>(url, { params });
    return response.data;
  }

  public async post<T>(url: string, data?: Object): Promise<T> {
    const response = await this.instance.post<T, AxiosResponse<T, Object>, Object>(url, data);
    return response.data;
  }
}

export const http = new NetworkService();


// -------------------------------------------------------------
// 3. 页面中使用示例
// -------------------------------------------------------------
@Entry
@Component
struct NetworkDemoPage {
  @State currentUser: UserInfo | null = null;
  @State isLoading: boolean = false;

  build() {
    Column() {
      Text('网络请求实战')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 40, bottom: 20 })

      if (this.isLoading) {
        LoadingProgress()
          .width(50)
          .height(50)
          .color(Color.Blue)
      } else if (this.currentUser) {
        Column({ space: 10 }) {
          Image(this.currentUser.avatar || 'https://img.alicdn.com/tfs/TB13.BWp.T1gK0jSZFrXXcNCXXa-200-200.png')
            .width(80)
            .height(80)
            .borderRadius(40)
            .backgroundColor('#f0f0f0')

          Text(this.currentUser.username)
            .fontSize(20)
            .fontWeight(FontWeight.Bold)

          Text(this.currentUser.email)
            .fontSize(14)
            .fontColor('#666')

          Text(`等级: Lv.${this.currentUser.level || 5}`)
            .fontSize(12)
            .fontColor(Color.White)
            .backgroundColor('#FFB900')
            .padding({ left: 10, right: 10, top: 2, bottom: 2 })
            .borderRadius(10)
        }
        .width('90%')
        .padding(20)
        .backgroundColor(Color.White)
        .borderRadius(16)
        .shadow({ radius: 20, color: '#1A000000', offsetY: 5 })
      } else {
        Column() {
          Text('暂无数据')
            .fontColor('#999')
            .margin({ bottom: 10 })
          Text('点击下方按钮测试真实接口')
            .fontSize(12)
            .fontColor('#CCC')
        }
        .margin(20)
      }

      Button('获取用户信息 (GET)')
        .width('80%')
        .height(50)
        .margin({ top: 40 })
        .backgroundColor('#007DFF')
        .onClick(async () => {
          this.isLoading = true;
          this.currentUser = null;

          try {
            const res = await http.get<UserInfo>('/users/1');

            this.currentUser = res;
            if (this.currentUser) {
              this.currentUser.level = 8;
              this.currentUser.avatar = 'https://img.alicdn.com/tfs/TB13.BWp.T1gK0jSZFrXXcNCXXa-200-200.png';
            }

            promptAction.showToast({ message: '数据获取成功' });
          } catch (error) {
            console.error('Request failed: ' + JSON.stringify(error));
          } finally {
            setTimeout(() => {
              this.isLoading = false;
            }, 500);
          }
        })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F1F3F5')
  }
}

四、 总结

网络层的封装是鸿蒙应用架构中最基础也最关键的一环。通过集成 @ohos/axios,我们不仅获得了熟悉的开发体验,更通过拦截器机制解决了鉴权和异常处理的痛点。而泛型接口的应用,则让我们的代码在灵活性与安全性之间找到了完美的平衡。一个优秀网络工具类,应该像水电气一样,平时你感觉不到它的存在,但在你需要的每一刻,它都能稳定、高效地为你输送数据。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐