uni-app 面试题:如何封装一个通用请求库?

在 uni-app 开发中,网络请求是几乎每个项目都绕不开的核心环节。虽然 uni-app 提供了 uni.request API,但直接在业务代码中裸调它,会带来一系列问题:请求配置分散、token 管理混乱、错误处理重复、加载状态难以统一控制。因此,封装一个通用请求库,是 uni-app 面试中的高频考点,也是衡量开发者工程化思维的重要标尺。

本文将从面试回答的角度,系统讲解 uni-app 中如何封装一个通用请求库,涵盖设计思路、核心实现、拦截器机制、错误处理以及常见追问。

一、为什么要封装请求库?

在回答“怎么封装”之前,先要讲清楚“为什么封装”。面试官通常希望听到你对痛点的理解:

  • 统一配置:baseURL、超时时间、请求头等集中管理,避免散落在各处。
  • 统一拦截:请求前自动携带 token,响应后统一处理登录失效、错误提示。
  • 统一错误处理:网络异常、HTTP 状态码异常、业务码异常分层处理。
  • 统一加载状态:可配置是否显示 loading,避免每个页面重复写。
  • 便于扩展:支持请求取消、重试、并发控制等高级能力。
  • 类型友好:在 TypeScript 项目中提供泛型支持,提升开发体验。

一句话总结:封装请求库的目的是降低重复代码、提升可维护性、统一团队规范

二、核心设计思路

一个通用的请求库,通常采用类 + 拦截器的设计模式,核心结构如下:

  1. Request 类:封装请求实例,持有配置和拦截器队列。
  2. 拦截器(Interceptor):分为请求拦截器和响应拦截器,支持注册多个。
  3. 请求方法:提供 requestgetpostputdelete 等快捷方法。
  4. 错误处理:区分网络错误、HTTP 错误、业务错误。
  5. 类型定义:定义请求配置、响应结构、拦截器类型。

这种设计借鉴了 axios 的思想,但底层基于 uni.request,因此需要处理 uni-app 特有的平台差异。

三、核心实现代码

下面给出一个可落地的简化版实现,面试时可以口述关键部分,手写时抓住主干即可。

1. 类型定义

interface RequestConfig {
  url: string;
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
  data?: any;
  header?: Record<string, string>;
  timeout?: number;
  showLoading?: boolean;
  loadingText?: string;
}

interface ResponseData<T = any> {
  code: number;
  message: string;
  data: T;
}

2. Request 类骨架

class HttpRequest {
  private baseURL: string;
  private timeout: number;
  private requestInterceptors: Array<(config: RequestConfig) => RequestConfig> = [];
  private responseInterceptors: Array<(res: any) => any> = [];

  constructor(options: { baseURL: string; timeout?: number }) {
    this.baseURL = options.baseURL;
    this.timeout = options.timeout || 10000;
  }

  // 注册请求拦截器
  interceptRequest(fn: (config: RequestConfig) => RequestConfig) {
    this.requestInterceptors.push(fn);
  }

  // 注册响应拦截器
  interceptResponse(fn: (res: any) => any) {
    this.responseInterceptors.push(fn);
  }

  // 核心请求方法
  async request<T = any>(config: RequestConfig): Promise<T> {
    // 1. 合并配置
    let finalConfig: RequestConfig = {
      method: 'GET',
      timeout: this.timeout,
      ...config,
      url: config.url.startsWith('http') ? config.url : this.baseURL + config.url,
    };

    // 2. 执行请求拦截器
    for (const interceptor of this.requestInterceptors) {
      finalConfig = interceptor(finalConfig);
    }

    // 3. 显示 loading
    if (finalConfig.showLoading) {
      uni.showLoading({ title: finalConfig.loadingText || '加载中...', mask: true });
    }

    try {
      // 4. 发起请求
      const res = await uni.request({
        url: finalConfig.url,
        method: finalConfig.method as any,
        data: finalConfig.data,
        header: finalConfig.header,
        timeout: finalConfig.timeout,
      });

      // 5. 执行响应拦截器
      let result: any = res;
      for (const interceptor of this.responseInterceptors) {
        result = interceptor(result);
      }

      return result as T;
    } catch (error) {
      // 6. 统一错误处理
      this.handleError(error);
      throw error;
    } finally {
      if (finalConfig.showLoading) {
        uni.hideLoading();
      }
    }
  }

  // 快捷方法
  get<T = any>(url: string, data?: any, config?: Partial<RequestConfig>) {
    return this.request<T>({ url, method: 'GET', data, ...config });
  }

  post<T = any>(url: string, data?: any, config?: Partial<RequestConfig>) {
    return this.request<T>({ url, method: 'POST', data, ...config });
  }

  // 错误处理
  private handleError(error: any) {
    // 区分网络错误、超时、HTTP 错误
    uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' });
  }
}

3. 创建实例并配置拦截器

const http = new HttpRequest({
  baseURL: 'https://api.example.com',
  timeout: 10000,
});

// 请求拦截器:自动携带 token
http.interceptRequest((config) => {
  const token = uni.getStorageSync('token');
  if (token) {
    config.header = {
      ...config.header,
      Authorization: `Bearer ${token}`,
    };
  }
  return config;
});

// 响应拦截器:统一处理业务码
http.interceptResponse((response) => {
  const { statusCode, data } = response;
  if (statusCode !== 200) {
    throw new Error(`HTTP Error: ${statusCode}`);
  }
  // 业务码处理
  if (data.code === 401) {
    // 登录失效,跳转登录页
    uni.navigateTo({ url: '/pages/login/login' });
    throw new Error('登录已过期');
  }
  if (data.code !== 0) {
    uni.showToast({ title: data.message, icon: 'none' });
    throw new Error(data.message);
  }
  return data.data; // 直接返回业务数据
});

export default http;

四、关键细节与面试追问

1. 为什么用类而不是函数?

类可以创建多个实例,便于管理不同 baseURL 的请求(如主 API 和第三方 API),同时拦截器队列作为实例属性,天然隔离。函数式封装也能实现,但类的扩展性更好。

2. 拦截器为什么用数组而不是单个函数?

数组支持注册多个拦截器,符合“单一职责”原则。比如一个拦截器负责 token,另一个负责签名,互不干扰。执行顺序上,请求拦截器按注册顺序执行,响应拦截器可按需设计为顺序或逆序。

3. 如何处理并发请求和请求取消?

uni.request 返回的是一个 Promise,但同时也支持 requestTask 对象。可以在 Request 类中维护一个 pending 映射表,将请求标识与 requestTask 关联,提供 abort 方法取消请求。对于并发控制,可以实现一个简单的请求队列。

4. 如何适配多平台?

uni-app 的 uni.request 已经做了跨平台封装,但需要注意:小程序端不支持 timeout 配置(部分平台),H5 端存在跨域问题。可以在请求拦截器中根据 process.env.UNI_PLATFORM 做差异化处理。

5. TypeScript 泛型怎么用?

request<T> 方法中传入泛型,响应拦截器返回业务数据后,调用方可以通过 http.get<UserInfo>('/user') 获得类型提示,提升开发效率。

6. 与 axios 的区别?

axios 基于 XMLHttpRequest,适用于 H5;uni-app 的 uni.request 是跨端 API,底层在不同平台映射为不同实现。封装思路可以借鉴 axios,但不能直接使用 axios 的适配器。

五、总结

封装 uni-app 通用请求库的核心要点可以归纳为:

  • 一个类:管理配置和拦截器队列。
  • 两层拦截:请求拦截器处理 token、签名;响应拦截器处理业务码、错误提示。
  • 三种错误:网络错误、HTTP 错误、业务错误分层处理。
  • 多个快捷方法:get、post、put、delete 提升调用体验。
  • 可扩展:支持请求取消、重试、并发控制、多实例。

在面试中,建议先讲设计思路,再讲核心代码,最后补充追问点。这样既能体现工程化思维,又能展示落地能力,给面试官留下深刻印象。

未经允许不得转载:任鹏个人博客 » uni-app 面试题:如何封装一个通用请求库?

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏