标签: #HarmonyOS #Axios #网络请求 #ArkTS #Token刷新 #架构设计


📉 前言:为什么原生 http 模块不够用?

原生 http.createHttp() 的痛点:

  1. 代码冗余:每次都要写 extraDataconnectTimeout,还要手动解析 JSON。
  2. 缺乏拦截器:想给所有请求统一加 Header 或者统一处理错误,只能写个包装函数,很难维护。
  3. 类型弱:返回的数据全是 string,需要手动 JSON.parse 并断言类型。

引入 @ohos/axios 后,我们不仅能复用前端的拦截器思维,还能利用 ArkTS 的泛型系统实现类型安全的网络层


🏗️ 一、 核心逻辑:Token 自动刷新流程

这是本篇的重难点。当多个并发请求同时触发 401 时,我们不能发起多次刷新请求,而是应该加锁。

并发控制流程图 (Mermaid):

401 未授权?

否 (我是第一个)

调用刷新接口

是 (别人正在刷)

刷新成功

解锁 & 广播

重发

刷新失败 (过期)

发起请求 A, B, C

响应拦截器

当前是否正在刷新?

加锁: isRefreshing = true

换取新 AccessToken

加入等待队列 (Promise Pending)

保存新 Token

执行队列中的请求

重发请求 A

强制登出

请求成功


🛠️ 二、 环境准备

安装鸿蒙版 Axios:

ohpm install @ohos/axios


💻 三、 代码实战:企业级封装

我们新建一个 AxiosRequest.ts 文件。

1. 定义基础结构与类型

为了让调用者用得爽,我们先定义好返回结构。

import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from '@ohos/axios';

// 后端返回的标准结构
interface BaseResponse<T> {
  code: number;
  message: string;
  data: T;
}

// 扩展 Axios 配置,允许传递自定义参数(如:是否需要 Loading)
interface CustomRequestConfig extends AxiosRequestConfig {
  showLoading?: boolean;
}

2. 实现单例类与请求拦截

请求拦截器的作用很简单:有 Token 就带上

class AxiosHttpRequest {
  private instance: AxiosInstance;
  
  constructor() {
    this.instance = axios.create({
      baseURL: 'https://api.example.com/v1',
      timeout: 10000,
      headers: { 'Content-Type': 'application/json' }
    });

    // --- 请求拦截器 ---
    this.instance.interceptors.request.use(
      (config: CustomRequestConfig) => {
        // 从 AppStorage 或 Preferences 获取 Token
        const token = AppStorage.Get<string>('accessToken');
        if (token) {
          config.headers['Authorization'] = `Bearer ${token}`;
        }
        return config;
      },
      (error) => Promise.reject(error)
    );

    // 响应拦截器在下一步实现...
  }
}

3. 核心:响应拦截与无感刷新 (The Magic)

这里我们需要两个辅助变量:

  • isRefreshing: 防止多次调用刷新接口。
  • requestsQueue: 存储在刷新期间进来的其他请求。
  // ... 类内部变量
  private isRefreshing = false;
  private requestsQueue: Function[] = [];

  // ... 在 constructor 中继续添加响应拦截器
  this.instance.interceptors.response.use(
    (response: AxiosResponse) => {
      // 这里的逻辑根据你们后端业务码来定
      // 假设 http status 200 但 code 401 也是 token 过期
      const res = response.data as BaseResponse<any>;
      if (res.code === 401) {
         return this.handle401Error(response.config);
      }
      return response;
    },
    (error: AxiosError) => {
      // 处理 HTTP 状态码为 401 的情况
      if (error.response?.status === 401) {
        return this.handle401Error(error.config);
      }
      return Promise.reject(error);
    }
  );

  // --- 处理 401 的核心逻辑 ---
  private handle401Error(originConfig: AxiosRequestConfig) {
    if (!this.isRefreshing) {
      this.isRefreshing = true;
      
      // 1. 发起刷新 Token 请求 (注意:这里最好用一个新的 axios 实例,避免死循环)
      return this.refreshToken()
        .then((newToken) => {
          // 2. 刷新成功,保存新 Token
          AppStorage.SetOrCreate('accessToken', newToken);
          
          // 3. 修改原请求的 Header
          originConfig.headers['Authorization'] = `Bearer ${newToken}`;
          
          // 4. 执行队列中的请求
          this.requestsQueue.forEach(cb => cb(newToken));
          this.requestsQueue = [];
          
          // 5. 重发当前请求
          return this.instance(originConfig);
        })
        .catch((err) => {
          // 6. 刷新也失败了?那是真的过期了,去登录页吧
          this.requestsQueue = [];
          // router.pushUrl({ url: 'pages/Login' })
          return Promise.reject(err);
        })
        .finally(() => {
          this.isRefreshing = false;
        });
    } else {
      // 如果正在刷新,则把当前请求挂起,放入队列
      return new Promise((resolve) => {
        this.requestsQueue.push((newToken: string) => {
          originConfig.headers['Authorization'] = `Bearer ${newToken}`;
          resolve(this.instance(originConfig));
        });
      });
    }
  }

  // 模拟刷新 Token 的接口
  private async refreshToken(): Promise<string> {
    // 实际业务中这里调用后端刷新接口
    // const refreshToken = AppStorage.Get('refreshToken');
    return "new_generated_token_123"; 
  }

4. 封装便捷方法 (GET/POST)

最后,暴露简单易用的 API。

  // T 是返回数据的类型,D 是请求参数的类型
  public get<T>(url: string, params?: any): Promise<T> {
    return this.instance.get<BaseResponse<T>>(url, { params })
      .then(res => res.data.data);
  }

  public post<T>(url: string, data?: any): Promise<T> {
    return this.instance.post<BaseResponse<T>>(url, data)
      .then(res => res.data.data);
  }
}

// 导出单例
export const http = new AxiosHttpRequest();


🚀 四、 调用演示:丝滑体验

在你的 UI 组件 (.ets) 中:

import { http } from '../utils/AxiosRequest';

interface UserProfile {
  id: number;
  name: string;
}

@Entry
@Component
struct ProfilePage {
  @State user: UserProfile | null = null;

  async aboutToAppear() {
    try {
      // 泛型支持:res 自动推断为 UserProfile 类型
      // 哪怕 Token 过期,这里也会自动重试并成功返回
      const res = await http.get<UserProfile>('/user/profile');
      this.user = res;
      console.info('用户名称:', res.name);
    } catch (error) {
      console.error('请求失败:', error);
    }
  }

  build() {
    // UI ...
  }
}


🎯 总结

通过这次封装,我们实现了:

  1. 代码解耦:UI 层不需要关心 Token 怎么传,也不需要关心 401 怎么处理。
  2. 类型安全:利用 TypeScript 泛型,接口返回什么类型,代码里就是什么类型。
  3. 极致体验Request Queue (请求队列) 的设计,确保了在并发请求场景下,Token 刷新接口只会被调用一次,避免了资源浪费和逻辑错误。

这是鸿蒙 App 开发中性价比最高的基建工作之一。

Next Step:
现在的封装还没处理 Loading 动画。试着修改 Interceptor,在请求开始时调用 promptAction.showToast 或自定义 Loading 组件,请求结束时关闭它,实现全局自动 Loading。

Logo

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

更多推荐