用 Taro 做跨平台小程序开发时,请求层是第一个要解决的基础设施问题。Web 端用 fetch 或 axios,小程序端用 Taro.request——API 不同、能力不同、限制也不同。我们在项目中封装了一套统一的请求层,让业务代码不需要关心运行在哪个平台。
平台差异对比
| 特性 | Web (H5) | 微信小程序 |
|---|---|---|
| 请求 API | fetch / XMLHttpRequest | wx.request |
| Cookie | 自动携带 | 不支持 |
| 跨域限制 | 有(需 CORS) | 无(但需配置合法域名) |
| 请求并发 | 无限制 | 最多 10 个 |
| HTTPS | 可选 | 强制 |
这些差异意味着不能简单地用同一个 HTTP 库。Taro 提供了 Taro.request 作为跨平台封装,但它的功能比较基础,缺少拦截器、自动重试等能力。
基础封装
// lib/request.ts
import Taro from '@tarojs/taro';
interface RequestConfig {
url: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
data?: object;
headers?: Record<string, string>;
}
interface ApiResponse<T = any> {
code: number;
data: T;
message: string;
}
const BASE_URL = getBaseUrl();
function getBaseUrl(): string {
// 根据环境切换
if (process.env.NODE_ENV === 'development') {
return 'https://dev-api.example.com';
}
return 'https://api.example.com';
}
async function request<T>(config: RequestConfig): Promise<T> {
const token = Taro.getStorageSync('access_token');
const response = await Taro.request({
url: `${BASE_URL}${config.url}`,
method: config.method || 'GET',
data: config.data,
header: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...config.headers,
},
});
return handleResponse<T>(response);
}
响应处理与错误分发
Taro.request 有个特殊行为:HTTP 4xx/5xx 状态码不会抛异常,它只有在网络错误时才会 reject。所以必须手动检查状态码:
function handleResponse<T>(response: Taro.request.SuccessCallbackResult): T {
const { statusCode, data } = response;
if (statusCode === 401) {
handleUnauthorized();
throw new Error('Unauthorized');
}
if (statusCode === 404) {
throw new Error('Resource not found');
}
if (statusCode >= 400) {
const message = (data as any)?.message || `请求失败 (${statusCode})`;
Taro.showToast({ title: message, icon: 'none' });
throw new Error(message);
}
const result = data as ApiResponse<T>;
return result.data;
}
这是容易踩的大坑——很多开发者用 try/catch 包裹 Taro.request,以为能捕获 400 错误,实际上捕获不到。
401 处理:跳转登录页
小程序的页面跳转和 Web 端不同,不能用 window.location.href:
let isRedirecting = false;
function handleUnauthorized() {
if (isRedirecting) return;
isRedirecting = true;
Taro.removeStorageSync('access_token');
// 小程序用 redirectTo,H5 用 location
if (process.env.TARO_ENV === 'weapp') {
Taro.redirectTo({ url: '/pages/login/index' });
} else {
window.location.href = '/login';
}
setTimeout(() => { isRedirecting = false; }, 3000);
}
process.env.TARO_ENV 是 Taro 的编译时常量,打包时会被替换为具体平台标识,不会引入多余代码。
请求重试
小程序的网络环境不如桌面浏览器稳定,偶发的网络抖动很常见。对于幂等的 GET 请求,加自动重试:
async function requestWithRetry<T>(
config: RequestConfig,
retries = 2
): Promise<T> {
for (let i = 0; i <= retries; i++) {
try {
return await request<T>(config);
} catch (error) {
if (i === retries) throw error;
// 只对网络错误重试,业务错误直接抛出
if (error instanceof Error && error.message.includes('request:fail')) {
await sleep(1000 * (i + 1)); // 递增延迟
continue;
}
throw error;
}
}
throw new Error('Unreachable');
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
关键是只对网络错误重试(request:fail 是 Taro 网络错误的标志),业务错误(如参数不合法)重试多少次都没用。
Loading 状态管理
小程序中通常用 Taro.showLoading 显示加载状态。但多个并发请求时,第一个请求完成就关闭了 Loading,其他请求还在进行中:
let loadingCount = 0;
function showLoading() {
if (loadingCount === 0) {
Taro.showLoading({ title: '加载中...' });
}
loadingCount++;
}
function hideLoading() {
loadingCount--;
if (loadingCount <= 0) {
loadingCount = 0;
Taro.hideLoading();
}
}
用引用计数确保所有请求都完成后才关闭 Loading。
导出便捷方法
export const http = {
get: <T>(url: string, data?: object) =>
request<T>({ url, method: 'GET', data }),
post: <T>(url: string, data?: object) =>
request<T>({ url, method: 'POST', data }),
put: <T>(url: string, data?: object) =>
request<T>({ url, method: 'PUT', data }),
del: <T>(url: string, data?: object) =>
request<T>({ url, method: 'DELETE', data }),
};
业务代码调用:
const userList = await http.get<User[]>('/api/users');
await http.post('/api/orders', { productId: 123 });
总结
Taro 跨平台请求层的核心挑战在于抹平平台差异:Taro.request 的非抛异常行为、Token 存储方式、页面跳转 API、网络稳定性。封装好这一层后,业务开发只需要 http.get / http.post,不用关心代码运行在浏览器还是小程序中。