npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

uniapp-request-sdk

v1.7.1

Published

用于uniapp小程序的请求库的sdk

Readme

uniapp-request-sdk

一个为 uni-app 小程序框架设计的高效、易用的 HTTP 请求库。支持自动重试、动态 token 管理、请求头预处理等企业级特性。

npm version license

目录

适用场景

这个 SDK 适合以下场景:

  • uni-app 项目中统一封装 GETPOSTPUTDELETEuploadFiledownloadFile
  • 需要自动处理 403 后重新获取 token 再重试的业务场景
  • 需要为所有请求统一追加公共请求头、日志字段、签名字段
  • 需要在 App、小程序、H5 等多端尽量复用一套网络层逻辑
  • 需要在上传文件时监听进度并驱动页面上的进度条展示
  • 需要在下载文件时监听进度并驱动页面上的下载进度条展示

如果你的服务端返回结构不是本文档中的默认格式,也可以基于当前 SDK 继续二次封装。

核心能力

  • 统一的请求实例管理,支持运行时动态更新参数
  • 支持全局公共请求头与单次请求头合并
  • 支持同步或异步的请求头预处理器 headerProcessor
  • 支持 401403、网络异常、超时等通用场景处理
  • 支持文件上传、上传超时控制和上传进度监听
  • 支持文件下载、下载超时控制和下载进度监听
  • 默认兼容相对路径与完整 URL 两种调用方式

特性

  • 自动重试机制 - 请求失败自动重试,可配置重试次数和延迟
  • 动态 Token 管理 - 支持自动获取和更新 token,兼容 APP 原生交互
  • 请求头预处理 - 支持同步/异步预处理器,动态生成或修改请求头
  • 完整的异常处理 - 统一的错误处理、HTTP 状态码处理、权限管理
  • 文件上传支持 - 专门优化的文件上传接口,独立超时控制,进度监听
  • 文件下载支持 - 专门优化的文件下载接口,独立超时控制,进度监听
  • 平台兼容 - 支持 iOS App、Android App、H5 等多平台
  • TypeScript 支持 - 完整的类型定义和 JSDoc 注释,更好的开发体验
  • 完全向后兼容 - 不破坏现有代码,渐进式增强

安装

npm 安装

npm install uniapp-request-sdk

yarn 安装

yarn add uniapp-request-sdk

5 分钟接入

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>>;

最佳实践

✅ 推荐做法

  1. 在应用初始化时创建单一实例
// 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(),
});
  1. 为不同的业务模块创建单独的方法
// api/user.ts
export function getUserList(page: number) {
  return request.get('/users', { page });
}

export function createUser(data: User) {
  return request.post('/users', data);
}
  1. 统一处理所有错误
const request = new UniRequest({
  onErrorHandler: (error) => {
    // 上报到日志系统
    logService.error(error);

    // 根据错误类型显示提示
    if (error.errno) {
      showErrorToast(error.errno);
    }
  },
});

❌ 避免做法

  1. 不要在每个请求时创建新实例
// ❌ 错误
async function getUsers() {
  const request = new UniRequest({ baseUrl: '...' });
  return request.get('/users');
}

// ✅ 正确
async function getUsers() {
  return request.get('/users');
}
  1. 不要在预处理器中执行耗时操作
// ❌ 错误:每次请求都会等待 5 秒
headerProcessor: async () => {
  await sleep(5000); // 不必要的延迟
  return {};
};

// ✅ 正确:提前准备数据
headerProcessor: async () => {
  const cachedData = await cache.get('businessData');
  return { 'X-Business-Id': cachedData.id };
};
  1. 不要在头预处理器中设置 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!

获取帮助

如有问题,请:

  1. 查看 常见问题
  2. 提交 GitHub Issue
  3. 联系 [email protected]