在 uni-app 开发中,网络请求是几乎每个项目都绕不开的核心环节。虽然 uni-app 提供了 uni.request API,但直接在业务代码中裸调它,会带来一系列问题:请求配置分散、token 管理混乱、错误处理重复、加载状态难以统一控制。因此,封装一个通用请求库,是 uni-app 面试中的高频考点,也是衡量开发者工程化思维的重要标尺。
本文将从面试回答的角度,系统讲解 uni-app 中如何封装一个通用请求库,涵盖设计思路、核心实现、拦截器机制、错误处理以及常见追问。
一、为什么要封装请求库?
在回答“怎么封装”之前,先要讲清楚“为什么封装”。面试官通常希望听到你对痛点的理解:
- 统一配置:baseURL、超时时间、请求头等集中管理,避免散落在各处。
- 统一拦截:请求前自动携带 token,响应后统一处理登录失效、错误提示。
- 统一错误处理:网络异常、HTTP 状态码异常、业务码异常分层处理。
- 统一加载状态:可配置是否显示 loading,避免每个页面重复写。
- 便于扩展:支持请求取消、重试、并发控制等高级能力。
- 类型友好:在 TypeScript 项目中提供泛型支持,提升开发体验。
一句话总结:封装请求库的目的是降低重复代码、提升可维护性、统一团队规范。
二、核心设计思路
一个通用的请求库,通常采用类 + 拦截器的设计模式,核心结构如下:
- Request 类:封装请求实例,持有配置和拦截器队列。
- 拦截器(Interceptor):分为请求拦截器和响应拦截器,支持注册多个。
- 请求方法:提供
request、get、post、put、delete等快捷方法。 - 错误处理:区分网络错误、HTTP 错误、业务错误。
- 类型定义:定义请求配置、响应结构、拦截器类型。
这种设计借鉴了 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 面试题:如何封装一个通用请求库?

