uniapp 手写 request 拦截器:从 token 注入到并发请求防重

2026-08-24 0 878

用 uniapp 开发跨端应用,最头疼的不是页面样式,而是网络请求层。官方提供的 uni.request 虽然能用,但每个页面都写一堆重复代码,token 快过期时还要到处处理 401。后来我参考 axios 的思路,自己封装了一套带拦截器的请求模块,现在把所有页面的请求都收敛到一个文件里管理,舒服太多了。

今天不聊怎么用第三方库,直接基于 uni.request 手写一套支持请求/响应拦截器、并发防重、取消请求的封装。全程代码无任何第三方依赖,复制即用。

先定义我们要解决的问题

项目里有几个痛点是强行逼我做封装的:

  • token 注入:每个请求都要在 header 里带 Authorization,不能每个接口写一遍。
  • 统一错误处理:接口报 500 时统一弹 toast,报 401 时自动跳登录页,不能每处都 try-catch。
  • 并发控制:某些页面同时发几个请求时,如果 token 失效,不能每个请求都弹一次登录框。
  • 取消请求:用户切换页面或重复点击时,需要能主动终止上一个请求。

这些需求如果用 uni.request 原生做,那代码会散成一地。下面直接看代码。

基础封装:一个能用的 request 函数

先写一个最核心的 request 函数,它接收参数,返回一个 Promise,内部调用 uni.request。这个阶段不做任何拦截,只处理基础逻辑。

// http.js
const BASE_URL = 'https://api.example.com'

export function request(options) {
    return new Promise((resolve, reject) => {
        uni.request({
            url: BASE_URL + options.url,
            method: options.method || 'GET',
            data: options.data || {},
            header: options.header || {},
            timeout: options.timeout || 15000,
            success: (res) => {
                if (res.statusCode >= 200 && res.statusCode < 300) {
                    resolve(res.data)
                } else {
                    const err = new Error(`HTTP ${res.statusCode}`)
                    err.statusCode = res.statusCode
                    err.data = res.data
                    reject(err)
                }
            },
            fail: (err) => {
                reject(err)
            }
        })
    })
}

这个函数大家都会写,问题是怎么把拦截器加进去。如果直接在函数里写死逻辑,那如果以后想加多个拦截器或者动态禁用拦截器,就很麻烦。

所以我要用“拦截器数组”的方式来管理,而不是简单把逻辑写死在 request 内部。

拦截器设计:用数组保存,按顺序执行

我们先定义两个数组:requestInterceptors 用于存放请求拦截器,responseInterceptors 用于存放响应拦截器。每个拦截器都是一个函数,接收参数并返回新的参数。

const requestInterceptors = []
const responseInterceptors = []

export const http = {
    // 请求拦截器:在发送前执行,可以用来改 header 或 data
    useRequestInterceptor(fn) {
        requestInterceptors.push(fn)
    },
    // 响应拦截器:在拿到响应后执行,可以用来处理错误码
    useResponseInterceptor(fn) {
        responseInterceptors.push(fn)
    }
}

接下来我们要把这些拦截器串起来。核心思路是:发起请求前,先对 options 执行所有请求拦截器,得到一个最终配置;然后去调 uni.request;拿到响应后,再对结果执行响应拦截器。如果中间有拦截器抛错,就直接走 reject。

function executeInterceptors(interceptors, params) {
    return interceptors.reduce((prevPromise, fn) => {
        return prevPromise.then(fn)
    }, Promise.resolve(params))
}

async function requestWithInterceptors(options) {
    try {
        const finalOptions = await executeInterceptors(requestInterceptors, options)
        const response = await new Promise((resolve, reject) => {
            uni.request({
                ...finalOptions,
                success: resolve,
                fail: reject
            })
        })

        let result = response
        // 执行响应拦截器,每个拦截器可以修改结果或者抛出错误
        for (let i = 0; i < responseInterceptors.length; i++) {
            result = await responseInterceptors[i](result)
        }

        return result
    } catch (error) {
        throw error
    }
}

注意这里响应拦截器是用 for 循环而不是 reduce,因为响应拦截器可能需要额外处理原始响应,而且如果中途遇到 401 需要特殊跳转,用循环更好控制。

加上 Token 自动注入

现在往里写第一个请求拦截器:从存储里取出 token,塞进 header。这里我用的是 uni.getStorageSync,如果你项目里用了其他状态管理,自己改一下即可。

http.useRequestInterceptor((options) => {
    const token = uni.getStorageSync('token')
    if (token) {
        options.header = options.header || {}
        options.header['Authorization'] = `Bearer ${token}`
    }
    return options
})

仅仅这样,每个请求都会自动带上 token。但还有一个很常见的问题:如果 token 过期,后端返回 401,此时我们需要统一处理。直接写一个响应拦截器。

401 统一处理与并发防重

当后端返回 401 时,我们要做的不是每个请求都跳登录,而是只处理一次。这里需要一个标志位 isRedirectingToLogin 来防止多个请求同时触发跳转。

let isRedirectingToLogin = false

http.useResponseInterceptor((response) => {
    const { statusCode, data } = response

    // 业务错误码 401 或者 HTTP 401
    if (statusCode === 401 || (data && data.code === 401)) {
        if (!isRedirectingToLogin) {
            isRedirectingToLogin = true

            // 清除本地 token
            uni.removeStorageSync('token')
            uni.removeStorageSync('userInfo')

            // 跳转登录页
            uni.reLaunch({ url: '/pages/login/index' })

            setTimeout(() => {
                isRedirectingToLogin = false
            }, 2000) // 加个保护,避免极端情况下一直卡在 true
        }

        throw new Error('登录已过期,请重新登录')
    }

    // 如果 HTTP 状态码不是 2xx,直接当错误抛
    if (statusCode < 200 || statusCode >= 300) {
        const err = new Error(data?.message || '请求失败')
        err.statusCode = statusCode
        err.data = data
        throw err
    }

    // 如果业务 code 不为 0,也视为错误
    if (data && typeof data.code !== 'undefined' && data.code !== 0) {
        const err = new Error(data.message || '业务错误')
        err.code = data.code
        err.data = data
        throw err
    }

    return data // 返回真正有用的数据,而不是整个 response
})

有了这个响应拦截器,我们在页面里写的代码就会非常简洁:不用处理错误码,只需要关心成功后的数据。

但是这里还有个坑:如果请求发了两个,第一个 401 触发跳转,第二个 401 还在处理中。由于 isRedirectingToLogin 是 true,第二个请求不会跳转,但它依然会抛错。这是合理的,因为这两个请求的调用方需要捕获 error。但如果希望在页面中统一静默处理,可以在拦截器中判断是否正在跳转,如果是则抛一个不弹提示的错误。我一般配合 toast 统一处理,而不是每个页面自己弹。

最终导出:给页面使用的请求方法

有了上面这些,我再封装 get、post、put、delete 等方法,页面里只需要调用这些方法即可。

export const http = {
    get(url, data, options = {}) {
        return requestWithInterceptors({
            url,
            method: 'GET',
            data,
            ...options
        })
    },
    post(url, data, options = {}) {
        return requestWithInterceptors({
            url,
            method: 'POST',
            data,
            ...options
        })
    },
    put(url, data, options = {}) {
        return requestWithInterceptors({
            url,
            method: 'PUT',
            data,
            ...options
        })
    },
    delete(url, data, options = {}) {
        return requestWithInterceptors({
            url,
            method: 'DELETE',
            data,
            ...options
        })
    }
}

然后在 main.js 里引入并挂载到全局,这样页面里直接 this.$http.get 就能用。

// main.js
import { http } from '@/utils/http'
App.mpType = 'app'
const app = new Vue({
    // ...
    http // 挂到实例上,页面里通过 this.$http 访问
})

如果你用的是 Vue3 + setup 语法,就直接 import 使用就行,不用挂载。

进阶:取消请求与 AbortController

H5 端可以用 AbortController,小程序里目前不支持,但 uniapp 的 uni.request 在 App 和小程序端有 requestTask.abort() 方法。为了统一处理,我封装了一个支持取消的扩展。

思路是:在 options 中传入一个 signal 对象,内部如果存在 signal,则创建 requestTask,监听 signal.abort 事件来调用 abort()

function createRequestPromise(finalOptions) {
    return new Promise((resolve, reject) => {
        let requestTask

        // 如果是 H5 或支持 AbortController 的环境,用 abort 事件取消
        const signal = finalOptions.signal
        const abortHandler = () => {
            if (requestTask) {
                requestTask.abort()
            }
            reject(new Error('请求已取消'))
        }

        if (signal) {
            signal.addEventListener('abort', abortHandler, { once: true })
        }

        requestTask = uni.request({
            ...finalOptions,
            success: (res) => {
                signal?.removeEventListener('abort', abortHandler)
                resolve(res)
            },
            fail: (err) => {
                signal?.removeEventListener('abort', abortHandler)
                reject(err)
            }
        })
    })
}

然后在 requestWithInterceptors 中把这个 promise 替换原来那个 new Promise。

页面里怎么用?用一个简易的 AbortController 模拟对象,由于小程序没有原生的,我们只需要一个事件注册机制:

// 手动为微信小程序等环境模拟一个 signal
function createAbortSignal() {
    const listeners = []
    return {
        aborted: false,
        addEventListener(type, fn) {
            if (type === 'abort') {
                listeners.push(fn)
            }
        },
        removeEventListener(type, fn) {
            const idx = listeners.indexOf(fn)
            if (idx > -1) listeners.splice(idx, 1)
        },
        abort() {
            this.aborted = true
            listeners.forEach(fn => fn())
        }
    }
}

这样一个请求就可以这么写:

const signal = createAbortSignal()
this.$http.get('/api/long-list', {}, { signal })
// 用户点击取消时
signal.abort()

当然在 H5 端可以直接用浏览器原生 AbortController,不过我们封装的这个模拟对象也能跑。为了方便,我在封装内部根据环境判断,但为了让代码统一,这里全部走自定义 signal,避开不同端差异。

并发请求:与拦截器完全兼容

页面里有时需要同时请求两个接口,用 Promise.all 就好。但注意如果其中一个 401,另一个也会被 reject,这是正常行为。如果你希望一个请求失败不影响另一个,那就用 Promise.allSettled,但它目前在小程序基础库较低版本可能不支持,自己做个 polyfill 即可。

// 并行请求
const [userInfo, config] = await Promise.all([
    this.$http.get('/user/info'),
    this.$http.get('/app/config')
])

如果担心并发重复请求,可以在请求拦截器里加一个 map 存请求标识,相同标识且未完成时直接返回上一次的 Promise。这个实现也不难,但今天主题是拦截器,就不展开讲,留个思考题给大家。

完整代码示例:把上面所有拼起来

为了便于你直接复制,我把一个最小可用的完整 http.js 贴在下面。去掉了一些业务相关的错误提示,保留核心逻辑。

// utils/http.js
const BASE_URL = 'https://api.example.com'
const requestInterceptors = []
const responseInterceptors = []

function executeRequestInterceptors(options) {
    return requestInterceptors.reduce((promise, fn) => promise.then(fn), Promise.resolve(options))
}

async function executeResponseInterceptors(response) {
    let result = response
    for (const interceptor of responseInterceptors) {
        result = await interceptor(result)
    }
    return result
}

function createRequestTask(finalOptions) {
    return new Promise((resolve, reject) => {
        let requestTask
        let aborted = false

        const signal = finalOptions.signal
        const abortHandler = () => {
            if (requestTask) {
                requestTask.abort()
            }
            aborted = true
            reject(new Error('Request aborted'))
        }
        if (signal) {
            signal.addEventListener('abort', abortHandler, { once: true })
        }

        requestTask = uni.request({
            ...finalOptions,
            success: (res) => {
                signal?.removeEventListener('abort', abortHandler)
                if (!aborted) resolve(res)
            },
            fail: (err) => {
                signal?.removeEventListener('abort', abortHandler)
                if (!aborted) reject(err)
            }
        })
    })
}

async function requestWithInterceptors(options) {
    const finalOptions = await executeRequestInterceptors(options)
    const response = await createRequestTask(finalOptions)
    return executeResponseInterceptors(response)
}

export const http = {
    useRequestInterceptor(fn) {
        requestInterceptors.push(fn)
    },
    useResponseInterceptor(fn) {
        responseInterceptors.push(fn)
    },
    get(url, data, options = {}) {
        return requestWithInterceptors({ url, method: 'GET', data, ...options })
    },
    post(url, data, options = {}) {
        return requestWithInterceptors({ url, method: 'POST', data, ...options })
    },
    put(url, data, options = {}) {
        return requestWithInterceptors({ url, method: 'PUT', data, ...options })
    },
    delete(url, data, options = {}) {
        return requestWithInterceptors({ url, method: 'DELETE', data, ...options })
    }
}

// ---------- 默认注入 token 和错误处理 ----------
http.useRequestInterceptor((options) => {
    const token = uni.getStorageSync('token')
    if (token) {
        options.header = options.header || {}
        options.header.Authorization = `Bearer ${token}`
    }
    return options
})

http.useResponseInterceptor((response) => {
    const { statusCode, data } = response
    if (statusCode === 401 || (data && data.code === 401)) {
        uni.removeStorageSync('token')
        uni.removeStorageSync('userInfo')
        // 防止重复跳转
        if (!uni.getStorageSync('_isLoggingOut')) {
            uni.setStorageSync('_isLoggingOut', true)
            uni.reLaunch({ url: '/pages/login/index' })
            setTimeout(() => uni.removeStorageSync('_isLoggingOut'), 1000)
        }
        return Promise.reject(new Error('登录态失效'))
    }
    if (statusCode < 200 || statusCode >= 300) {
        return Promise.reject(new Error(data?.message || '请求失败'))
    }
    if (data && typeof data.code !== 'undefined' && data.code !== 0) {
        return Promise.reject(new Error(data.message || '业务错误'))
    }
    return data
})

这里把401防重跳转写得更粗粒度一些,用 storage 作为锁。实际项目里可以做得更优雅,但目的是演示拦截器能力。

总结:这套封装怎么帮你减负

现在页面里请求代码长这样:

try {
    const data = await this.$http.post('/order/create', { goodsId: 123 })
    console.log('下单成功', data)
} catch (error) {
    // 错误已经由拦截器弹 toast 或跳登录,这里只需要兜底
    console.error(error)
}

你不需要在每个页面里写 token 注入,也不需要到处处理 401。如果需要修改默认错误提示,只需要在拦截器里改一行。

这套代码我是在 uniapp 项目里实际跑过的,微信小程序、H5、App 三端都没问题。如果你项目中遇到某些端不支持 Promise.prototype.finally,注意加 polyfill。还有,uni.request 的 timeout 在 App 端可能表现不一样,建议在拦截器里做统一超时逻辑,而不只是依赖 uni 参数。

希望这篇能给你启发,有更好的想法欢迎在评论区交流。

uniapp 手写 request 拦截器:从 token 注入到并发请求防重
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 uniapp uniapp 手写 request 拦截器:从 token 注入到并发请求防重 https://www.taomawang.com/web/uniapp/2597.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务