更新记录

2.0.0(2025-09-26) 下载此版本

新增无感刷新token,优化示例项目,下载即会使用

1.1.2(2025-06-24) 下载此版本

优化method

1.1.1(2025-04-08) 下载此版本

去掉loading,添加示例项目

查看更多

平台兼容性

uni-app x(4.76)

Chrome Safari Android iOS 鸿蒙 微信小程序
- 5.0

yq-request-uts

uniappx 封装 request 支持 Promise 和 async/await 写法,支持 GET、POST、PUT、DELETE、HEAD、OPTIONS、PATCH 方法,内置无感刷新 Token 机制。

主要特性

  • ✅ 支持 Promise 和 async/await
  • ✅ 支持 GET、POST、PUT、DELETE、HEAD、OPTIONS、PATCH 方法
  • ✅ 内置无感刷新 Token 机制
  • ✅ 自定义请求头,更加灵活
  • ✅ 自定义业务层错误处理函数
  • ✅ 支持外部接口调用
  • ✅ 同时支持 HTTP 状态码 401 和业务错误码 401 触发刷新

安装和使用

1. App.uvue 配置

<script lang="uts">
    export default {
        globalData: {
            baseUrl: '',
            devUrl: 'https://restapi.amap.com',
            proUrl: 'http://pro.com',

            // Token 刷新相关
            isRefreshingToken: false,
            requestQueue: [] as UTSJSONObject[],

            // Token 存储 
            accessTokenValue: '',
            refreshTokenValue: '',

            httpSuccessCodes: [200, 201, 202, 203, 204, 205, 206],

            http401() {
                console.log('HTTP 401 错误');
            },

            systemError(res : UTSJSONObject) : any {
                // 返回三种可能的值:
                // - false: 没有错误
                // - 'TOKEN_EXPIRED': Token 过期,需要刷新
                // - true: 其他业务错误

                // 检查业务错误码 401(Token 过期)
                if (res['code'] == 401) {
                    return 'TOKEN_EXPIRED';
                }
                // 检查 HTTP 状态码(某些接口可能在 data 中返回状态码)
                else if (res['status'] == 401) {
                    return 'TOKEN_EXPIRED';
                }
                // 其他常见的 Token 过期标识
                else if (res['error'] == 'invalid_token' || res['error'] == 'access_denied') {
                    return 'TOKEN_EXPIRED';
                }
                // 业务错误
                else if (res['code'] == 500) {
                    uni.showToast({
                        title: res['message'] as string,
                        icon: 'none',
                        duration: 2500
                    });
                    return true;
                }
                // 没有错误
                else {
                    return false;
                }
            },

            setHeader(url : string, auth : boolean, formData : boolean) : UTSJSONObject {
                let header = {
                    'content-type': 'application/json'
                };

                if (auth && this.accessTokenValue != "") {
                    header['Authorization'] = 'Bearer ' + this.accessTokenValue;
                }

                if (formData) {
                    header['content-type'] = 'application/x-www-form-urlencoded';
                }

                return header;
            },

            /**
             * 刷新 Token 的方法 - 真实请求
             */
            refreshToken() : void {
                console.log('开始刷新 Token');

                if (this.refreshTokenValue == "") {
                    console.log('refreshToken 为空,刷新失败');
                    this.handleRefreshFailure();
                    return;
                }

                // 真实的刷新 Token 请求
                uni.request<UTSJSONObject>({
                    url: this.baseUrl + '/api/auth/refresh', // 请替换为您的实际刷新接口
                    method: 'POST',
                    header: {
                        'content-type': 'application/json',
                        'Authorization': 'Bearer ' + this.refreshTokenValue
                    },
                    data: {
                        refresh_token: this.refreshTokenValue
                    },
                    success: (res : RequestSuccess<UTSJSONObject>) => {
                        console.log('刷新Token请求成功:', res);

                        // 检查HTTP状态码
                        if (res.statusCode == 200) {
                            const data = res.data as UTSJSONObject;

                            // 检查业务错误
                            const errorCheck = this.systemError(data);
                            if (errorCheck === false) {
                                // 刷新成功,获取新的token
                                const newAccessToken = data['access_token'] as string;
                                const newRefreshToken = data['refresh_token'] as string;
                                const finalAccessToken = newAccessToken != null ? newAccessToken : "";
                                const finalRefreshToken = newRefreshToken != null ? newRefreshToken : this.refreshTokenValue;

                                if (finalAccessToken != "") {
                                    console.log('Token刷新成功');
                                    this.handleRefreshSuccess(finalAccessToken, finalRefreshToken);
                                } else {
                                    console.log('刷新接口返回的access_token为空');
                                    this.handleRefreshFailure();
                                }
                            } else if (errorCheck === 'TOKEN_EXPIRED') {
                                console.log('刷新Token已过期');
                                this.handleRefreshFailure();
                            } else {
                                console.log('刷新Token接口返回业务错误');
                                this.handleRefreshFailure();
                            }
                        } else {
                            console.log('刷新Token请求失败,状态码:', res.statusCode);
                            this.handleRefreshFailure();
                        }
                    },
                    fail: (error : any) => {
                        console.log('刷新Token网络请求失败:', error);
                        this.handleRefreshFailure();
                    }
                });
            },

            /**
             * Token 刷新成功处理
             */
            handleRefreshSuccess(accessToken : string, refreshToken : string) : void {
                console.log('处理刷新成功');

                this.accessTokenValue = accessToken;

                if (refreshToken != "") {
                    this.refreshTokenValue = refreshToken;
                    uni.setStorageSync('refreshToken', refreshToken);
                }

                uni.setStorageSync('accessToken', accessToken);
                console.log('当前请求队列长度:', this.requestQueue.length);
                this.executeQueuedRequests();
                this.isRefreshingToken = false;
                console.log('Token 刷新流程完成');
            },

            /**
             * Token 刷新失败处理
             */
            handleRefreshFailure() : void {
                console.log('处理刷新失败');

                this.accessTokenValue = "";
                this.refreshTokenValue = "";
                uni.removeStorageSync('accessToken');
                uni.removeStorageSync('refreshToken');

                uni.showToast({
                    title: '登录已过期,请重新登录',
                    icon: 'none'
                });

                setTimeout(() => {
                    uni.reLaunch({
                        url: '/pages/login/login'
                    });
                }, 1500);

                this.rejectQueuedRequests();
                this.isRefreshingToken = false;
            },

            /**
             * 执行队列中的请求
             */
            executeQueuedRequests() : void {
                console.log('执行队列中的请求,数量:', this.requestQueue.length);

                const queue = this.requestQueue;
                this.requestQueue = [];

                for (let i = 0; i < queue.length; i++) {
                    const item = queue[i];
                    try {
                        console.log('执行第', i + 1, '个重试请求');
                        const executeFunc = item['execute'] as () => void;
                        executeFunc();
                    } catch (error) {
                        console.error('重试请求失败:' + error);
                    }
                }
            },

            /**
             * 拒绝队列中的请求
             */
            rejectQueuedRequests() : void {
                console.log('拒绝队列中的请求,数量:', this.requestQueue.length);

                const queue = this.requestQueue;
                this.requestQueue = [];

                for (let i = 0; i < queue.length; i++) {
                    const item = queue[i];
                    try {
                        const rejectFunc = item['reject'] as (reason : string) => void;
                        rejectFunc('Token刷新失败,请重新登录');
                    } catch (error) {
                        console.error('拒绝请求失败:' + error);
                    }
                }
            },

            /**
             * 初始化 Token
             */
            initToken() : void {
                const accessToken = uni.getStorageSync('accessToken') as string;
                const refreshToken = uni.getStorageSync('refreshToken') as string;

                if (accessToken != "") {
                    this.accessTokenValue = accessToken;
                }
                if (refreshToken != "") {
                    this.refreshTokenValue = refreshToken;
                }

                console.log('Token 初始化完成');
            }
        },

        onLaunch: function () {
            console.log('App Launch');
            this.setBaseUrl();
            this.globalData.initToken();
        },

        methods: {
            setBaseUrl() : void {
                if (process.env.NODE_ENV == 'development') {
                    // #ifdef H5
                    this.globalData.baseUrl = location.origin;
                    // #endif
                    // #ifndef H5
                    this.globalData.baseUrl = this.globalData.devUrl;
                    // #endif
                } else {
                    this.globalData.baseUrl = this.globalData.proUrl;
                }
                console.log('Base URL 设置为:', this.globalData.baseUrl);
            }
        }
    }
</script>

2. H5 跨域代理配置(可选)

在项目根目录创建 vite.config.ts

import { defineConfig } from 'vite';
import uni from '@dcloudio/vite-plugin-uni';
export default defineConfig({
    plugins: [
        uni(),
    ],
    server: {
      proxy: {
        '/api/': {
          target: 'http://192.168.1.110:8000',
          changeOrigin: true,
        }
      },
    },
});

3. 接口文件配置

在项目根目录创建 apis 文件夹(注意:不要使用 api 作为文件夹名,避免 H5 代理冲突),创建 index.uts

import { request, IParams } from '@/uni_modules/yq-request-uts/utssdk/index';

/**
 * 首页商品列表接口 - POST 表单形式
 */
export const goodsApi = (data: UTSJSONObject): Promise<UTSJSONObject> => {
    const requestData: IParams = {
        method: 'post', 
        api: '/api/goodsPage',
        header: {'X-Custom-Header': 'custom-value'}, // 支持自定义请求头
        data,
        formData: true, // 设置为表单格式
    };
    return request(requestData);
};

/**
 * 首页商品列表接口 - POST JSON形式
 */
export const goodsJsonApi = (data: UTSJSONObject): Promise<UTSJSONObject> => {
    const requestData: IParams = {
        method: 'post', 
        api: '/api/goodsPageJson',
        data, // 默认 JSON 格式
    };
    return request(requestData);
};

/**
 * 首页公示公告列表接口 - GET
 */
export const newsApi = (data: UTSJSONObject): Promise<UTSJSONObject> => {
    const requestData: IParams = {
        method: 'get', 
        api: '/api/newsPage',
        data,
    };
    return request(requestData);
};

/**
 * 更新数据接口 - PUT
 */
export const updateApi = (id: string, data: UTSJSONObject): Promise<UTSJSONObject> => {
    const requestData: IParams = {
        method: 'put', 
        api: `/api/update/${id}`,
        data,
    };
    return request(requestData);
};

/**
 * 删除数据接口 - DELETE
 */
export const deleteApi = (id: string): Promise<UTSJSONObject> => {
    const requestData: IParams = {
        method: 'delete', 
        api: `/api/delete/${id}`,
    };
    return request(requestData);
};

/**
 * 外部接口 - 不需要鉴权(auth设置为false)
 */
export const baiduApi = (data: UTSJSONObject): Promise<UTSJSONObject> => {
    const requestData: IParams = {
        url: getApp().globalData.baiduUrl, // 使用外部 URL
        api: '/api/listIndex',
        auth: false, // 不需要鉴权
        data,
    };
    return request(requestData);
};

4. 在页面中使用

<template>
    <view>
        <view>
            <text>API 测试</text>
            <button @click="getWeather">get接口</button>
        </view>
        <view>
            <text style="font-size: 20px;">响应数据:</text>
            <view>
                <view>对象形式:</view>
                <text>Status: {{ data.status }}</text>
                <text>Info: {{ data.info }}</text>
                <text>Infocode: {{ data.infocode }}</text>
                <view style="margin-top: 20px;">列表形式:</view>
                <view v-for="(item, index) in data.lives" :key="index">
                      <text >地点:{{ item.province }}</text>
                      <text>天气:{{ item.weather }}</text>
                </view>
            </view>
        </view>
    </view>
</template>

<script setup lang="uts">
    import { weatherApi, } from '@/apis/index.uts';
    // 定义全局对象类型type
    type DataType = {
      status: string
      info: string
      infocode: string
      lives:UTSJSONObject[]
    }

    const data = reactive({
      status:"",
      info:"",
      infocode:"",
      lives:[]
    } as DataType)

    /**
     * 获取天气数据
     */
    const getWeather = async () => {
        try {
            const res = await weatherApi({});
            // 对象赋值
            data.status = res.status as string;
            data.info = res.info as string;
            data.infocode = res.infocode as string;
            // 列表赋值
            data.lives = res.lives as UTSJSONObject[]
            console.log('天气数据:', res);
        } catch (error) {
            console.error('获取天气失败:', error);
        }
    };

</script>

无感刷新 Token 机制

触发条件

以下情况会自动触发无感刷新 Token:

  1. HTTP 状态码 401:服务器返回 401 状态码
  2. 业务错误码 401:响应数据中 code 字段为 401
  3. 其他 Token 过期标识:如 status=401error=invalid_token

工作原理

  1. 当检测到 Token 过期时,将当前请求加入等待队列
  2. 触发 Token 刷新流程
  3. 刷新成功后,自动重试队列中的所有请求
  4. 刷新失败时,跳转到登录页面

参数说明

IParams 接口参数

参数名 类型 必填 默认值 说明
url string globalData.baseUrl 请求基础URL
api string - 接口路径
method string 'get' 请求方法(小写)
header UTSJSONObject - 自定义请求头
data any null 请求数据
formData boolean false 是否表单格式
timeout number 20000 请求超时时间
withCredentials boolean false 是否携带凭证
auth boolean true 是否需要鉴权

注意事项

  1. 方法名使用小写:如 'get''post',插件内部会自动转换为大写
  2. Token 存储:使用 accessTokenValuerefreshTokenValue 存储 Token
  3. H5 代理:接口文件夹名不要使用 api,建议使用 apis
  4. 错误处理:业务错误码 401 会触发无感刷新,其他错误码按业务逻辑处理

更新日志

v2.0.0

  • 新增无感刷新 Token 机制
  • 同时支持 HTTP 401 和业务错误码 401
  • 方法名改为小写传参
  • 修复属性名冲突问题
  • 优化错误处理逻辑

v1.0.0

  • 基础请求封装
  • 支持 Promise 和 async/await
  • 自定义请求头和错误处理

隐私、权限声明

1. 本插件需要申请的系统权限列表:

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

许可协议

MIT协议