uniapp-request-sdk
v1.7.1
Published
用于uniapp小程序的请求库的sdk
Readme
uniapp-request-sdk
一个为 uni-app 小程序框架设计的高效、易用的 HTTP 请求库。支持自动重试、动态 token 管理、请求头预处理等企业级特性。
目录
- 适用场景
- 核心能力
- 安装
- 5 分钟接入
- 快速开始
- 工程接入建议
- 详细配置
- API 文档
- 文件上传
- 文件下载
- 使用示例
- 响应格式
- 错误处理
- 平台差异处理
- 配置参数详解
- 请求头预处理器(新增功能)
- 最佳实践
- 企业级实战案例
- 常见问题
- 版本历史
适用场景
这个 SDK 适合以下场景:
- uni-app 项目中统一封装
GET、POST、PUT、DELETE、uploadFile、downloadFile - 需要自动处理
403后重新获取 token 再重试的业务场景 - 需要为所有请求统一追加公共请求头、日志字段、签名字段
- 需要在 App、小程序、H5 等多端尽量复用一套网络层逻辑
- 需要在上传文件时监听进度并驱动页面上的进度条展示
- 需要在下载文件时监听进度并驱动页面上的下载进度条展示
如果你的服务端返回结构不是本文档中的默认格式,也可以基于当前 SDK 继续二次封装。
核心能力
- 统一的请求实例管理,支持运行时动态更新参数
- 支持全局公共请求头与单次请求头合并
- 支持同步或异步的请求头预处理器
headerProcessor - 支持
401、403、网络异常、超时等通用场景处理 - 支持文件上传、上传超时控制和上传进度监听
- 支持文件下载、下载超时控制和下载进度监听
- 默认兼容相对路径与完整 URL 两种调用方式
特性
- ✅ 自动重试机制 - 请求失败自动重试,可配置重试次数和延迟
- ✅ 动态 Token 管理 - 支持自动获取和更新 token,兼容 APP 原生交互
- ✅ 请求头预处理 - 支持同步/异步预处理器,动态生成或修改请求头
- ✅ 完整的异常处理 - 统一的错误处理、HTTP 状态码处理、权限管理
- ✅ 文件上传支持 - 专门优化的文件上传接口,独立超时控制,进度监听
- ✅ 文件下载支持 - 专门优化的文件下载接口,独立超时控制,进度监听
- ✅ 平台兼容 - 支持 iOS App、Android App、H5 等多平台
- ✅ TypeScript 支持 - 完整的类型定义和 JSDoc 注释,更好的开发体验
- ✅ 完全向后兼容 - 不破坏现有代码,渐进式增强
安装
npm 安装
npm install uniapp-request-sdkyarn 安装
yarn add uniapp-request-sdk5 分钟接入
import UniRequest from 'uniapp-request-sdk';
const request = new UniRequest({
baseUrl: 'https://api.example.com',
timeout: 10000,
uploadTimeout: 30000,
downloadTimeout: 30000,
tokenHeader: 'Authorization',
tokenPrefix: 'Bearer ',
onErrorHandler: (error) => {
console.error('请求失败:', error);
},
});
export default request;import request from '@/utils/request';
export function getUserProfile() {
return request.get('/user/profile');
}
export function updateUserProfile(data: Record<string, any>) {
return request.post('/user/profile/update', data);
}
export function uploadAvatar(filePath: string) {
return request.uploadFile('/user/avatar/upload', filePath, undefined, 'file', undefined, (progress) => {
console.log('上传进度:', progress.progress);
});
}
export function downloadDocument(documentId: string) {
return request.downloadFile(`/documents/${documentId}/download`, undefined, undefined, (progress) => {
console.log('下载进度:', progress.progress);
});
}完成以上两步后,你的工程已经具备:
- 统一请求入口
- 统一错误处理能力
- 文件上传能力
- 上传进度监听能力
- 文件下载能力
- 下载进度监听能力
快速开始
基础使用
import UniRequest from 'uniapp-request-sdk';
// 1. 创建请求实例
const request = new UniRequest({
baseUrl: 'https://api.example.com',
timeout: 10000,
});
// 2. 发送 GET 请求
const data = await request.get('/users/list');
// 3. 发送 POST 请求
const result = await request.post('/users', { name: 'John' });
// 4. 发送 DELETE 请求
await request.delete('/users/1');
// 5. 发送 PUT 请求
await request.put('/users/1', { name: 'Jane' });
// 6. 上传文件
const uploadResult = await request.uploadFile(
'/upload',
'/path/to/file.jpg',
undefined,
'file',
undefined,
(progress) => {
console.log('当前上传进度:', progress.progress);
},
);
// 7. 下载文件
const downloadPath = await request.downloadFile('/download/document.pdf', undefined, undefined, (progress) => {
console.log('当前下载进度:', progress.progress);
});工程接入建议
推荐在业务工程中按下面的方式组织网络层:
src/
├── api/ # 按业务域拆分接口
│ ├── user.ts
│ ├── auth.ts
│ └── workflow.ts
├── utils/
│ └── request.ts # SDK 实例初始化
└── pages/ # 页面中只调用 api 层推荐职责划分如下:
utils/request.ts:只负责创建UniRequest实例和放置全局配置api/*.ts:只负责定义接口函数,不写 UI 逻辑pages/*或components/*:只消费api层返回的数据
推荐这样做的原因:
- 请求配置只有一份,便于统一维护
- token、签名、超时、日志字段不会散落在页面中
- 后续替换域名、调整 header、增加埋点时改动范围最小
详细配置
初始化配置
const request = new UniRequest({
// 基础配置
baseUrl: 'https://api.example.com', // API 基础地址(必须)
username: 'john_doe', // 用户名(用于日志)
// 超时设置
timeout: 10000, // 普通请求超时(毫秒,默认 10s)
uploadTimeout: 5000, // 文件上传超时(毫秒,默认 5s)
// 重试配置
maxRetryCount: 3, // 最大重试次数(默认 3)
retryDelay: 3000, // 重试延迟时间(毫秒,默认 3s)
// 请求头配置
header: {
// 全局请求头
'Content-Type': 'application/json',
'X-Custom-Header': 'value',
},
// Token 配置
token: 'initial-token', // 初始 token
tokenHeader: 'Authorization', // token 所在的 header 字段名(默认)
tokenPrefix: 'Bearer ', // token 前缀(默认带空格)
tokenEventName: 'getToken', // 向 APP 获取 token 的事件名
getTokenFun: async () => {
// 自定义获取 token 函数
return await fetchTokenFromStorage();
},
// 错误处理
onErrorHandler: (error) => {
// 统一错误处理函数
console.error('请求错误:', error);
// 可以在这里上报错误日志
},
// 新增:请求头预处理
headerProcessor: async (header) => {
// 动态处理请求头(异步)
const timestamp = Date.now();
const signature = await generateSignature(timestamp);
return {
'X-Timestamp': timestamp.toString(),
'X-Signature': signature,
};
},
});运行时修改配置
// 使用 setParams 方法动态更新配置
request.setParams({
token: 'new-token', // 更新 token
baseUrl: 'https://new-api.example.com', // 更新 API 地址
headerProcessor: (header) => {
// 更新头处理器
return {
'X-New-Header': 'new-value',
};
},
});API 文档
GET 请求
// 基础 GET 请求
const data = await request.get<ResponseType>('/users/list');
// 带查询参数
const data = await request.get<ResponseType>('/users/list', {
page: 1,
limit: 10,
});
// 自定义请求头
const data = await request.get<ResponseType>(
'/users/list',
{},
{
'X-Custom-Header': 'custom-value',
},
);POST 请求
// 基础 POST 请求
const result = await request.post<ResponseType>('/users', {
name: 'John',
email: '[email protected]',
});
// 自定义请求头
const result = await request.post<ResponseType>(
'/users',
{ name: 'John' },
{
'X-Custom-Header': 'custom-value',
},
);DELETE 请求
const result = await request.delete<ResponseType>('/users/1');PUT 请求
const result = await request.put<ResponseType>('/users/1', {
name: 'Updated Name',
});文件上传
// 基础文件上传
const result = await request.uploadFile<ResponseType>(
'/upload', // 上传 URL
'/path/to/file.jpg', // 文件路径
);
// 带 FormData 的文件上传
const result = await request.uploadFile<ResponseType>('/upload', '/path/to/file.jpg', {
// FormData 字段
description: 'My upload',
category: 'profile',
});
// 自定义文件字段名和请求头
const result = await request.uploadFile<ResponseType>(
'/upload',
'/path/to/file.jpg',
{ description: 'My upload' },
'avatar', // 文件字段名(默认 'file')
{
// 自定义请求头
'X-Upload-Token': 'upload-token',
},
);
// 监听上传进度
const result = await request.uploadFile<ResponseType>(
'/upload',
'/path/to/file.jpg',
{ description: 'My upload' },
'file',
undefined,
(progress) => {
console.log('上传进度百分比:', progress.progress);
console.log('已上传字节数:', progress.totalBytesSent);
console.log('总字节数:', progress.totalBytesExpectedToSend);
},
);文件下载
// 基础文件下载
const tempFilePath = await request.downloadFile('/download/file.pdf');
// 带自定义保存路径的下载(仅小程序有效)
const filePath = await request.downloadFile(
'/download/file.pdf',
'user_documents/file.pdf', // 本地保存路径(仅小程序平台支持)
);
// 自定义请求头的下载
const filePath = await request.downloadFile(
'/download/file.pdf',
undefined, // 保存路径
{
// 自定义请求头
'X-Download-Token': 'download-token',
},
);
// 监听下载进度
const filePath = await request.downloadFile('/download/large-file.zip', undefined, undefined, (progress) => {
console.log('下载进度百分比:', progress.progress);
console.log('已下载字节数:', progress.totalBytesWritten);
console.log('总字节数:', progress.totalBytesExpectedToWrite);
});文件上传
方法签名
uploadFile<T>(
url: string,
filePath: string,
formData?: Record<string, any>,
name = 'file',
header?: Record<string, string>,
onProgressUpdateCallback?: (result: OnProgressUpdateResult) => void,
): Promise<T>参数说明
| 参数 | 类型 | 是否必填 | 说明 |
| -------------------------- | ------------------------ | -------- | ------------------------------------ |
| url | string | 是 | 上传接口地址,支持相对路径和完整 URL |
| filePath | string | 是 | 待上传文件的本地临时路径 |
| formData | Record<string, any> | 否 | 附加的表单字段 |
| name | string | 否 | 文件字段名,默认是 file |
| header | Record<string, string> | 否 | 当前上传请求的自定义请求头 |
| onProgressUpdateCallback | (result) => void | 否 | 上传进度回调 |
上传进度回调说明
当传入 onProgressUpdateCallback 后,SDK 会在内部调用 uni.uploadFile() 返回的 UploadTask.onProgressUpdate() 进行绑定。
回调参数结构如下:
type OnProgressUpdateResult = {
progress?: number;
totalBytesSent?: number;
totalBytesExpectedToSend?: number;
};使用注意事项
onProgressUpdateCallback是可选参数,不传时上传行为与旧版本保持一致- 如果只想传进度回调、不需要自定义
header,第 5 个参数需要显式传undefined - 上传接口底层仍然返回 Promise,适合继续用
await或.then() - 如果上传过程中发生失败并触发重试,进度回调会按”当前这次上传尝试”的进度继续上报
- 如果服务端返回的是字符串 JSON,SDK 会自动尝试解析后再取其中的
data
文件下载
新增功能说明
从 v1.6.2 版本开始,SDK 新增了 downloadFile 方法,提供与 uploadFile 对称的文件下载功能。支持:
- ✅ 下载文件到本地临时目录或指定路径
- ✅ 自定义请求头
- ✅ 实时下载进度监听
- ✅ 自动重试、Token 管理等企业级特性
- ✅ 跨平台兼容(App、小程序、H5)
方法签名
downloadFile<T extends string = string>(
url: string,
filePath?: string,
header?: Record<string, string>,
onProgressUpdateCallback?: (result: DownloadFileProgressUpdateResult) => void,
): Promise<T>参数说明
| 参数 | 类型 | 是否必填 | 说明 |
| -------------------------- | ------------------------ | -------- | -------------------------------------------------------------------- |
| url | string | 是 | 下载资源的 URL,支持相对路径和完整 URL |
| filePath | string | 否 | 文件保存路径(本地路径),仅小程序平台支持;如不指定则保存到临时目录 |
| header | Record<string, string> | 否 | 当前下载请求的自定义请求头 |
| onProgressUpdateCallback | (result) => void | 否 | 下载进度回调 |
返回值说明
方法返回 Promise,解析为下载文件的临时路径(tempFilePath):
// 基础用法
const filePath = await request.downloadFile('/files/document.pdf');
// filePath 是文件的临时路径,如 '/tmp/file_20240314.pdf'
// 支持泛型约束
const filePath: string = await request.downloadFile<string>('/files/document.pdf');下载进度回调说明
当传入 onProgressUpdateCallback 后,SDK 会在内部调用 uni.downloadFile() 返回的 DownloadTask.onProgressUpdate() 进行绑定。
回调参数结构如下:
type DownloadFileProgressUpdateResult = {
progress?: number; // 下载进度百分比 (0-100)
totalBytesWritten?: number; // 已下载字节数
totalBytesExpectedToWrite?: number; // 总字节数
};与上传的区别
| 特性 | uploadFile | downloadFile |
| ---------- | ---------------------- | -------------------- |
| 超时默认值 | 5 秒 | 30 秒 |
| 文件来源 | 本地文件 | 远程 URL |
| 附加参数 | formData(表单字段) | filePath(保存路径) |
| 进度类型 | totalBytesSent | totalBytesWritten |
| 返回值 | 服务端响应的 data 字段 | 文件临时路径 |
平台兼容性
| 特性 | iOS App | Android App | 小程序 | H5 | | -------------- | ------- | ----------- | ------ | ---- | | 基础下载 | ✅ | ✅ | ✅ | ✅ | | 进度监听 | ✅ | ✅ | ✅ | 部分 | | 自定义保存路径 | ❌ | ❌ | ✅ | ❌ | | 文件 URI | ✅ | ✅ | ✅ | 部分 |
使用注意事项
- 下载的文件保存在临时目录,应用关闭后会被清理,需要持久化时调用
uni.saveFile() - 小程序平台支持指定
filePath保存到指定目录,其他平台会忽略此参数 - 下载超时时间默认为 30 秒,可通过
downloadTimeout配置修改 - 下载过程中如果失败,SDK 会自动重试(最多
maxRetryCount次) - 下载大文件时建议增加
downloadTimeout的值 - 下载过程中用户可以通过返回的 Promise 的 abort 功能中止下载(需要获取 DownloadTask)
使用示例
示例 1:基础请求
import UniRequest from 'uniapp-request-sdk';
// 创建实例
const request = new UniRequest({
baseUrl: 'https://api.example.com',
});
// 使用
export async function getUsers() {
try {
const response = await request.get('/users');
console.log('用户列表:', response);
return response;
} catch (error) {
console.error('获取用户列表失败:', error);
}
}示例 2:Token 管理
import UniRequest from 'uniapp-request-sdk';
const request = new UniRequest({
baseUrl: 'https://api.example.com',
tokenEventName: 'getToken', // APP 端事件名
onErrorHandler: (error) => {
if (error.statusCode === 401) {
console.log('用户已登出');
// 跳转到登录页
uni.redirectTo({ url: '/pages/login/index' });
}
},
});
// 登录后更新 token
async function login(username: string, password: string) {
const { token } = await request.post('/auth/login', {
username,
password,
});
// 更新 token
request.setParams({ token });
}示例 3:动态请求头预处理
import UniRequest from 'uniapp-request-sdk';
import { generateSignature } from './crypto';
const request = new UniRequest({
baseUrl: 'https://api.example.com',
headerProcessor: async (header) => {
// 生成签名
const timestamp = Date.now();
const sign = await generateSignature({
timestamp,
token: header['Authorization'],
});
return {
'X-Timestamp': timestamp.toString(),
'X-Sign': sign,
};
},
});示例 4:文件上传
import UniRequest from 'uniapp-request-sdk';
const request = new UniRequest({
baseUrl: 'https://api.example.com',
uploadTimeout: 30000, // 文件上传超时 30 秒
});
// 选择文件并上传
async function selectAndUploadFile() {
uni.chooseImage({
count: 1,
success: async (res) => {
try {
const result = await request.uploadFile('/upload', res.tempFilePaths[0], {
// 附加信息
description: '头像',
category: 'avatar',
});
console.log('上传成功:', result);
} catch (error) {
console.error('上传失败:', error);
}
},
});
}示例 4-1:带上传进度的文件上传
import UniRequest from 'uniapp-request-sdk';
const request = new UniRequest({
baseUrl: 'https://api.example.com',
uploadTimeout: 30000,
});
async function selectAndUploadFile() {
uni.chooseImage({
count: 1,
success: async (res) => {
const filePath = res.tempFilePaths[0];
let currentProgress = 0;
try {
const result = await request.uploadFile(
'/upload',
filePath,
{
category: 'avatar',
},
'file',
undefined,
(progress) => {
currentProgress = progress.progress || 0;
console.log('上传中:', currentProgress);
},
);
console.log('上传完成:', result);
} catch (error) {
console.error('上传失败:', error, currentProgress);
}
},
});
}示例 4-2:文件下载(新增)
import UniRequest from 'uniapp-request-sdk';
const request = new UniRequest({
baseUrl: 'https://api.example.com',
downloadTimeout: 30000, // 文件下载超时 30 秒
});
// 简单下载
async function downloadFile() {
try {
const filePath = await request.downloadFile('/files/document.pdf');
console.log('下载完成,文件路径:', filePath);
// 显示文件
uni.openDocument({
filePath: filePath,
showMenu: true,
});
} catch (error) {
console.error('下载失败:', error);
}
}
// 带进度监听的下载
async function downloadFileWithProgress() {
try {
let downloadProgress = 0;
const filePath = await request.downloadFile('/files/large-file.zip', undefined, undefined, (progress) => {
downloadProgress = progress.progress || 0;
console.log(`下载进度: ${downloadProgress}%`);
console.log(`已下载: ${progress.totalBytesWritten} / ${progress.totalBytesExpectedToWrite} 字节`);
// 更新 UI 进度条
// updateProgressBar(downloadProgress);
});
console.log('下载完成:', filePath);
} catch (error) {
console.error('下载失败:', error, downloadProgress);
}
}
// 小程序中下载到指定路径
async function downloadFileToPath() {
try {
const filePath = await request.downloadFile(
'/files/contract.pdf',
'user_documents/contract.pdf', // 保存到应用沙箱
);
console.log('文件已保存:', filePath);
} catch (error) {
console.error('下载失败:', error);
}
}
// 带自定义 header 的下载(如需要鉴权)
async function downloadSecureFile() {
try {
const filePath = await request.downloadFile(
'/files/secure-document.pdf',
undefined,
{
'X-Download-Token': 'secure-token-xxx',
'X-User-Id': 'user-123',
},
(progress) => {
console.log(`下载进度: ${progress.progress}%`);
},
);
console.log('下载完成:', filePath);
} catch (error) {
console.error('下载失败:', error);
}
}示例 4-3:综合文件管理(上传和下载)
import UniRequest from 'uniapp-request-sdk';
const request = new UniRequest({
baseUrl: 'https://api.example.com',
uploadTimeout: 30000,
downloadTimeout: 30000,
});
// 文件管理服务
export const FileService = {
// 上传文件
uploadDocument: async (filePath: string, metadata: Record<string, any>) => {
try {
const result = await request.uploadFile(
'/documents/upload',
filePath,
metadata,
'file',
undefined,
(progress) => {
console.log(`上传进度: ${progress.progress}%`);
},
);
return result;
} catch (error) {
console.error('上传失败:', error);
throw error;
}
},
// 下载文件
downloadDocument: async (documentId: string, fileName: string) => {
try {
const filePath = await request.downloadFile(
`/documents/download/${documentId}`,
`downloads/${fileName}`,
{ 'X-Document-Id': documentId },
(progress) => {
console.log(`下载进度: ${progress.progress}%`);
},
);
return filePath;
} catch (error) {
console.error('下载失败:', error);
throw error;
}
},
// 列出文档
listDocuments: async (payload: Record<string, any>) => {
return request.post('/documents/list', payload);
},
// 删除文档
deleteDocument: async (documentId: string) => {
return request.delete(`/documents/${documentId}`);
},
};
// 在组件中使用
export default {
data() {
return {
uploadProgress: 0,
downloadProgress: 0,
documents: [],
};
},
methods: {
// 选择并上传文件
async selectAndUpload() {
try {
const res = await uni.chooseFile({
type: 'file',
count: 1,
});
const result = await FileService.uploadDocument(res.tempFilePaths[0], {
description: '重要文档',
category: 'reports',
});
uni.showToast({
title: '上传成功',
icon: 'success',
});
// 刷新列表
await this.loadDocuments();
} catch (error) {
uni.showToast({
title: '上传失败',
icon: 'error',
});
}
},
// 下载文件
async downloadDocument(documentId: string, fileName: string) {
try {
const filePath = await FileService.downloadDocument(documentId, fileName);
// 打开文件
uni.openDocument({
filePath: filePath,
showMenu: true,
});
} catch (error) {
uni.showToast({
title: '下载失败',
icon: 'error',
});
}
},
// 加载文档列表
async loadDocuments() {
try {
this.documents = await FileService.listDocuments({
page: 1,
limit: 20,
});
} catch (error) {
console.error('加载失败:', error);
}
},
},
mounted() {
this.loadDocuments();
},
};示例 5:实时生成签名
import UniRequest from 'uniapp-request-sdk';
import CryptoJS from 'crypto-js';
// 创建带有实时签名的请求实例
const request = new UniRequest({
baseUrl: 'https://api.example.com',
headerProcessor: async (header) => {
// 每次请求都生成新签名
const timestamp = Date.now().toString();
const appKey = 'your-app-key';
// 根据 timestamp + appKey 生成签名
const sign = CryptoJS.SHA256(timestamp + appKey).toString();
return {
'X-Timestamp': timestamp,
'X-Sign': sign,
};
},
});响应格式
库默认假定服务器返回的响应格式如下:
{
errno: 0, // 错误码(0 表示成功)
data: { // 实际数据
// ... 业务数据
}
}其中:
errno === 0表示请求成功errno !== 0表示业务错误,会触发 reject
错误处理
自动处理的错误
| 错误类型 | 处理方式 | | -------- | ------------------------------------- | | HTTP 403 | 自动获取新 token 并重试 | | HTTP 401 | 触发 logout 事件,通知 APP 返回登录页 | | 网络错误 | 自动重试(最多 maxRetryCount 次) | | 超时错误 | 自动重试(最多 maxRetryCount 次) | | 业务错误 | 直接 reject,业务处理 |
全局错误处理
const request = new UniRequest({
baseUrl: 'https://api.example.com',
onErrorHandler: (error) => {
// 这里处理所有请求错误
if (error.statusCode === 401) {
// 处理权限错误
console.log('权限不足');
} else if (error.errno !== undefined) {
// 处理业务错误
console.log('业务错误:', error.errno);
} else {
// 处理网络错误
console.log('网络错误:', error.errMsg);
}
},
});局部错误处理
try {
const data = await request.get('/users');
} catch (error) {
// 处理该请求的错误
console.error('请求失败:', error);
}平台差异处理
H5 平台
在 H5 平台上,请勿在请求头中设置 cookie,因为出于安全考虑,H5 中自动删除了手动设置的 cookie。
// ❌ 错误:H5 中不生效
const request = new UniRequest({
tokenHeader: 'cookie',
});
// ✅ 正确:使用 Authorization header
const request = new UniRequest({
tokenHeader: 'Authorization',
});小程序平台
在小程序中,token 需要通过 sendNativeEvent 从 APP 中获取。
const request = new UniRequest({
tokenEventName: 'getToken', // APP 端对应事件名
});配置参数详解
| 参数 | 类型 | 默认值 | 必填 | 说明 | | --------------- | -------- | ------------- | ---- | -------------------------- | | baseUrl | string | - | ✅ | API 基础地址,支持代理路径 | | timeout | number | 10000 | - | 普通请求超时(毫秒) | | uploadTimeout | number | 5000 | - | 文件上传超时(毫秒) | | maxRetryCount | number | 3 | - | 失败重试次数 | | retryDelay | number | 3000 | - | 重试延迟时间(毫秒) | | header | object | - | - | 全局请求头 | | headerProcessor | function | - | - | 请求头预处理函数(新增) | | token | string | - | - | 初始 token | | tokenHeader | string | Authorization | - | token 所在 header 字段 | | tokenPrefix | string | 'Bearer ' | - | token 前缀 | | tokenEventName | string | getToken | - | APP 获取 token 的事件名 | | getTokenFun | function | - | - | 自定义 token 获取函数 | | username | string | - | - | 用户名(用于日志) | | onErrorHandler | function | console.error | - | 全局错误处理函数 |
请求头预处理器(新增功能)
请求头预处理器允许你在每次请求前动态生成或修改请求头,特别适合需要:
- 动态生成签名 - 基于时间戳生成请求签名
- 权限认证 - 根据用户权限添加特殊 header
- 版本标记 - 添加 API 版本或协议版本
- 业务数据 - 从业务接口获取数据并添加到 header
使用场景
场景 1:生成请求签名
const request = new UniRequest({
baseUrl: 'https://api.example.com',
headerProcessor: async (header) => {
const timestamp = Date.now();
const appSecret = 'your-app-secret';
// 生成签名
const sign = generateHMAC(timestamp + appSecret);
return {
'X-Timestamp': timestamp.toString(),
'X-Sign': sign,
};
},
});场景 2:从业务接口获取 header 数据
const request = new UniRequest({
baseUrl: 'https://api.example.com',
headerProcessor: async (header) => {
// 从缓存或接口获取业务数据
const businessData = await getBusinessDataFromStorage();
return {
'X-Business-Id': businessData.id,
'X-Business-Version': businessData.version,
};
},
});场景 3:同步生成请求 ID
import { v4 as uuidv4 } from 'uuid';
const request = new UniRequest({
baseUrl: 'https://api.example.com',
headerProcessor: (header) => {
// 同步生成请求 ID
return {
'X-Request-Id': uuidv4(),
};
},
});预处理器的执行时机
- 执行点:在设置 token 后、发送请求前
- 执行频率:每次业务请求执行一次,自动重试时复用第一次处理后的 header;若中途刷新 token,则会基于新 token 重新执行一次
- 异常处理:如果预处理器异常,请求会直接 reject,不会发送
预处理器的签名
// 同步预处理器
type HeaderProcessor = (header: Record<string, string>) => Record<string, string>;
// 异步预处理器
type HeaderProcessor = (header: Record<string, string>) => Promise<Record<string, string>>;最佳实践
✅ 推荐做法
- 在应用初始化时创建单一实例
// app.ts 或 main.ts
import UniRequest from 'uniapp-request-sdk';
export const request = new UniRequest({
baseUrl: process.env.VUE_APP_API_URL,
token: initialToken,
username: getUserName(),
});- 为不同的业务模块创建单独的方法
// api/user.ts
export function getUserList(page: number) {
return request.get('/users', { page });
}
export function createUser(data: User) {
return request.post('/users', data);
}- 统一处理所有错误
const request = new UniRequest({
onErrorHandler: (error) => {
// 上报到日志系统
logService.error(error);
// 根据错误类型显示提示
if (error.errno) {
showErrorToast(error.errno);
}
},
});❌ 避免做法
- 不要在每个请求时创建新实例
// ❌ 错误
async function getUsers() {
const request = new UniRequest({ baseUrl: '...' });
return request.get('/users');
}
// ✅ 正确
async function getUsers() {
return request.get('/users');
}- 不要在预处理器中执行耗时操作
// ❌ 错误:每次请求都会等待 5 秒
headerProcessor: async () => {
await sleep(5000); // 不必要的延迟
return {};
};
// ✅ 正确:提前准备数据
headerProcessor: async () => {
const cachedData = await cache.get('businessData');
return { 'X-Business-Id': cachedData.id };
};- 不要在头预处理器中设置 cookie(H5 平台)
// ❌ 错误:H5 中不生效
headerProcessor: async () => {
return { cookie: 'sessionid=xxx' };
};
// ✅ 正确:使用其他方式
headerProcessor: async () => {
return { 'X-Session-Id': 'xxx' };
};企业级实战案例
真实案例:OA 流程审批系统
以下是基于 getui-oa-process-uniapp 项目的实战案例,展示如何在真实企业系统中使用本库。
项目背景
一个 uni-app 小程序 OA 系统,用于流程审批、预算查询等企业应用。该项目使用本库作为核心请求库。
1. 请求实例初始化
// src/requests/http/index.ts
import UniRequest from 'uniapp-request-sdk';
// 显示 Toast 提示(处理 iOS 冷启动吞掉 toast 的问题)
function showToast(title: string) {
setTimeout(() => {
uni.showToast({
title,
icon: 'none',
});
}, 500);
}
// 全局错误处理函数
function onErrorHandler(resData: any) {
if (resData?.data?.errno === 2002) {
// 业务逻辑:用户无权限
showToast(resData.data.errmsg ?? '用户无权限');
setTimeout(() => {
// 关闭小程序
uni.sendNativeEvent('closeMiniProgram', {}, () => {});
}, 1000);
} else if (resData?.statusCode === 403) {
// 业务逻辑:token 失效或权限不足
if (uni.getStorageSync('isDebug')) {
// 调试模式跳转到 mock 页面
uni.navigateTo({
url: '/pages/mock/index',
});
} else {
// 生产环境提示重新进入
showToast('用户登录信息失效,请退出后重试');
}
} else {
// 默认错误处理
showToast(resData?.data?.errmsg ?? '网络异常,请退出重新进入');
}
}
// 主请求实例(用于常规业务请求)
export const requestInstance = new UniRequest({
timeout: 4000, // 请求超时 4 秒
maxRetryCount: 2, // 重试 2 次
uploadTimeout: 1000 * 60 * 2, // 文件上传超时 2 分钟
onErrorHandler, // 使用统一错误处理
});
// AI 请求实例(用于 AI 接口请求)
export const aiRequestInstance = new UniRequest({
uploadTimeout: 1000 * 20, // 上传超时 20 秒
maxRetryCount: 0, // 不重试(AI 接口可能耗时较长)
onErrorHandler,
});2. 创建业务 API 层
// src/requests/severs/process.ts
import { requestInstance, aiRequestInstance } from '../http';
export const ProcessServes = {
// 获取流程详情
getProcessDetailes: (payload: { businessId: string }) =>
requestInstance.post<{ data: any }>('/process/application/business/detail', payload),
// 标记流程已读
readProcessMessage: (payload: { businessId: string }) =>
requestInstance.post<{ data: any }>('/process/application/business/readProcessMessage', payload),
// 查询申请人职位
getById: (payload: { id: string }) => requestInstance.post('/usercenter/oaUser/getById', payload),
// 撤回流程
processRecall: (payload: { businessId: string; processCategory: string; comment: string }) =>
requestInstance.post('/process/application/process/recall', payload),
// 获取流程事项列表
getProcessMatterList: (payload: any) =>
requestInstance.post<{ data: any[] }>('/process/application/matter/list', payload),
// 获取事项信息
getMatterInfo: (payload: any) => requestInstance.post('/process/application/matter/matterInfo', payload),
// 流程审批
processApprove: (payload: {
businessId: string;
processCategory: string;
ccUser: string[];
comment: string;
pass: boolean;
attachment: Array<{ name: string; url: string }>;
}) => requestInstance.post('/process/application/process/approve', payload),
// 保存并审批
processSaveAndApprove: (payload: any) => requestInstance.post('/process/application/process/saveAndApprove', payload),
// 保存流程
processSave: (payload: {
applicantDepartmentId: string;
processDepartmentId: string;
applicantId: string;
businessData: Record<string, any>;
processId: string;
sponsorId: string;
sponsorDepartmentId: string;
processCategory: string;
businessId?: string;
}) => requestInstance.post('/process/application/business/save', payload),
// 申请流程
processApply: (payload: { businessId: string; ccUser: any[]; processCategory: string }) =>
requestInstance.post('/process/application/process/apply', payload),
// 获取流程评论
getProcessComments: (payload: { businessId: string }) =>
requestInstance.post('/process/application/business/commentList', payload),
// 获取流程表单信息
getProcessFormInfo: (processCategory: string) =>
requestInstance.post('/process/application/business/formInfo', { processCategory }),
// 获取流程申请人列表
getApplyUsers: (payload: { processCategory: string; userId: string }) =>
requestInstance.post('/flow/assistant/getApplyUsers', {
business: payload.processCategory,
id: payload.userId,
}),
// 检查流程版本号
checkVersion: (payload: { processCategory: string; processId: string }) =>
requestInstance.post('/process/application/process/checkVersion', payload),
// 获取预算包使用进度
getBudgetInventoryProgress: (payload: {
processCategory: string;
departmentId: string;
subjects: string[];
businessLineUuids: string[];
}) => requestInstance.post('/financial/budgetInventory/budgetInventoryProgress', payload),
// 获取业务线列表
getBusinessLineList: () => requestInstance.post('financial/businessLine/businessLineList'),
// 获取合同详情
getContractDetail: (payload: { contractNumber: string; businessId: string }) =>
requestInstance.post('/financial/contract/processContractDetail', payload),
// 获取流程文件列表
getBusinessProcessFileList: (businessId: string) =>
requestInstance.post<any[]>('/process/application/business/businessProcessFileList', {
businessId,
fileTypes: ['pdf', 'docx', 'doc'],
}),
// 上传音频文件并转换为流程数据(使用 AI 实例,支持长超时)
uploadAudioToProcessData: ({ filePath, data }: any) => {
return aiRequestInstance.uploadFile('/ds/trans/process/', filePath, data);
},
};3. 公共 API 服务
// src/requests/severs/common.ts
import { requestInstance } from '../http';
export const CommmonServes = {
// 获取用户信息
getUserInfo: async (payload: { userIds: string[] }) => {
const data = await requestInstance.post<any>('/usercenter/oaUser/getUserInfoByIds', payload);
return data?.data;
},
// 获取部门信息
getDepartmentIdInfo: async (payload: { departmentId: string; type?: string }) => {
const data = await requestInstance.post<any>('/usercenter/oaDepartment/getParentById', payload);
return data?.data ?? data;
},
// GHR 认证
ghrAuthentication: async (payload: {}) => {
const data = await requestInstance.post<any>('/usercenter/ghr/authentication', payload);
return data?.data ?? data;
},
// 查询关联客户
getAssociatedCustomers: async (payload: {}) => {
const data = await requestInstance.post<any>('/gcrm/third/relevantCust/checkContains', payload);
return data?.data ?? data;
},
};4. 在 Vue 组件中使用
// 在组件中使用 API 服务
import { ProcessServes } from '@/requests/severs/process';
export default {
data() {
return {
processData: null,
loading: false,
};
},
async mounted() {
await this.loadProcessDetail();
},
methods: {
async loadProcessDetail() {
this.loading = true;
try {
// 使用 API 服务获取数据
const response = await ProcessServes.getProcessDetailes({
businessId: this.businessId,
});
this.processData = response;
} catch (error) {
// 错误处理已由全局 onErrorHandler 处理
console.error('加载流程详情失败:', error);
} finally {
this.loading = false;
}
},
async submitApproval() {
try {
// 提交审批
await ProcessServes.processApprove({
businessId: this.businessId,
processCategory: this.processCategory,
ccUser: this.ccUsers,
comment: this.comment,
pass: true,
attachment: this.attachments,
});
uni.showToast({
title: '审批成功',
icon: 'success',
});
} catch (error) {
console.error('审批失败:', error);
}
},
async uploadAudioFile() {
try {
// 选择音频文件
const res = await uni.chooseFile({
type: 'file',
accept: 'audio/*',
});
// 上传音频并转换
const result = await ProcessServes.uploadAudioToProcessData({
filePath: res.tempFilePaths[0],
data: {
processCategory: this.processCategory,
},
});
// 使用 AI 转换后的数据
this.processFormData = result;
} catch (error) {
console.error('上传音频失败:', error);
}
},
},
};5. 关键设计要点
多实例策略
项目使用两个不同的请求实例:
requestInstance: 用于常规业务请求
- 超时时间:4 秒
- 重试次数:2 次
- 适合快速响应的 API
aiRequestInstance: 用于 AI 接口请求
- 超时时间:20 秒(足够长时间处理 AI 计算)
- 重试次数:0 次(避免重复处理)
- 适合长时间处理的 API
统一错误处理
所有错误都通过 onErrorHandler 函数处理:
- 业务错误(errno = 2002):显示错误信息,关闭小程序
- 权限错误(statusCode = 403):引导用户重新登录
- 其他错误:显示通用错误信息
API 层封装
将所有 API 调用封装在专门的服务文件中:
- 统一的错误处理
- 类型安全的请求和响应
- 易于测试和维护
- 便于在多个组件间复用
6. 实战总结
这个案例展示了如何在生产环境中使用本库:
✅ 多实例管理 - 根据需求创建不同配置的实例 ✅ 统一错误处理 - 全局处理所有错误,避免重复代码 ✅ API 层封装 - 将请求逻辑与业务逻辑分离 ✅ 类型安全 - 充分利用 TypeScript 的泛型机制 ✅ 易于扩展 - 新 API 添加无需修改现有代码
常见问题
Q: 如何处理跨域问题?
A: 在 baseUrl 中包含代理路径,让后端或开发服务器代理请求。
const request = new UniRequest({
baseUrl: 'https://localhost:8080/api/proxy/', // 包含代理路径
});Q: 如何上传多个文件?
A: 多个文件需要多次调用 uploadFile,或者一个文件一个请求。
// 方式 1:循环上传
for (const filePath of filePaths) {
await request.uploadFile('/upload', filePath);
}
// 方式 2:并行上传(不推荐,可能导致超时)
await Promise.all(filePaths.map((filePath) => request.uploadFile('/upload', filePath)));Q: 如何下载多个文件?
A: 需要多次调用 downloadFile,建议使用串行下载避免超时:
// 方式 1:顺序下载(推荐)
const filePaths = [];
for (const fileUrl of fileUrls) {
const path = await request.downloadFile(fileUrl);
filePaths.push(path);
}
// 方式 2:并行下载(需谨慎,可能导致超时)
const filePaths = await Promise.all(fileUrls.map((fileUrl) => request.downloadFile(fileUrl)));Q: 下载的文件如何持久化?
A: 使用 uni.saveFile() 将临时文件保存到永久目录:
const tempFilePath = await request.downloadFile('/file.pdf');
// 保存到永久位置
uni.saveFile({
tempFilePath: tempFilePath,
success: (res) => {
console.log('文件已保存:', res.savedFilePath);
},
});Q: 下载大文件时超时怎么办?
A: 增加 downloadTimeout 的值:
const request = new UniRequest({
baseUrl: 'https://api.example.com',
downloadTimeout: 60000, // 60 秒,适合大文件
});
// 或在运行时修改
request.setParams({
downloadTimeout: 120000, // 120 秒
});Q: 如何取消正在进行的下载?
A: 目前库返回的是 Promise,无法直接获取 DownloadTask 来调用 abort。建议通过以下方式实现:
// 方式 1:修改库的源码返回 DownloadTask(高级用法)
// 方式 2:使用超时机制让请求自动中止
// 方式 3:在下载进度回调中检查用户是否点击了取消
// 推荐:在进度回调中检查状态
let shouldCancel = false;
await request.downloadFile('/file.zip', undefined, undefined, (progress) => {
if (shouldCancel) {
// 无法直接中止,但可以停止处理
return;
}
console.log(`下载进度: ${progress.progress}%`);
});
// 用户点击取消按钮时
function cancelDownload() {
shouldCancel = true;
}Q: 下载的文件在哪里?
A: 下载的文件保存在以下位置:
- H5: 浏览器默认下载目录
- 小程序: 临时目录(
wx.env.USER_DATA_PATH下),或指定的filePath - App: 应用的缓存目录
建议下载后立即使用或保存,避免应用被清理时文件丢失。
Q: 如何处理 token 过期?
A: 库会自动在收到 403 状态码时获取新 token 并重试。如需自定义逻辑:
const request = new UniRequest({
getTokenFun: async () => {
// 自定义获取 token 的逻辑
const token = await refreshTokenFromServer();
return token;
},
});Q: 为什么第一次请求返回 403?
A: 可能是因为初始化时没有设置 token,或 token 已过期。库会在收到 403 时自动获取新 token 并重试。
Q: 如何禁用自动重试?
A: 设置 maxRetryCount 为 0。
const request = new UniRequest({
maxRetryCount: 0, // 禁用重试
});Q: headerProcessor 每次都会执行吗?
A: 每次调用 request / get / post 等接口时都会执行一次;如果该次请求触发自动重试,会复用第一次处理后的 header,不会重复执行。这可以保证同一次业务请求在重试时复用同一个幂等 header。唯一例外是 403 刷新 token 成功后,SDK 会基于新 token 重新生成一次 header,再立即重试。
版本历史
v1.6.2(最新)
✨ 新增 文件下载功能 (
downloadFile方法)- 支持下载文件到本地目录或临时目录
- 支持实时下载进度监听
- 支持自定义请求头
- 自动继承 token、重试等企业级特性
- 默认超时时间 30 秒(可配置)
📝 文档改进 完整的 JSDoc 注释
- 所有公开/私有方法添加详细 JSDoc
- 所有参数和返回值有清晰说明
- 关键方法包含使用示例
- 100% 注释覆盖率
📚 README 更新
- 新增《文件下载》文档章节
- 新增文件下载的详细使用示例(示例 4-2、4-3)
- 更新目录索引
- 完善参数和平台兼容性说明
🔧 技术改进
- 优化 TypeScript 类型定义
- 完善错误处理机制
- 增强跨平台兼容性检查
v1.4.11
- ✨ 新增 请求头预处理器功能 (
headerProcessor) - 🐛 修复 原有逻辑兼容性
- 📚 文档 完整的 README 文档
v1.4.10 及之前
- 基础请求功能
- Token 管理
- 自动重试
- 错误处理
许可证
ISC License
作者
yaojun
贡献
欢迎提交 Issue 和 Pull Request!
获取帮助
如有问题,请:
- 查看 常见问题
- 提交 GitHub Issue
- 联系 [email protected]
