书接上篇,今天我们来聊一个很多UniApp开发者都问过的问题:能在UniApp中使用axios吗?如果能,怎么用?怎么封装成企业级请求库?附带实现思路和所有源码示例,以及常见避坑指南和开发实践。
此系列文章将带领你从移动端跨平台开发入门到精通,如果你也喜欢关注APP、小程序、公众号、H5等等应用的开发,可以持续关注后续更新,避免错过宝贵的知识分享。
你可能会想:“UniApp不是有自己的uni.request吗?为什么还要用axios?”因为axios有更优雅的拦截器、更丰富的生态、更符合前端开发者的使用习惯。但是,axios默认依赖XMLHttpRequest,在微信小程序等环境中不可用。不过,axios提供了一个强大的扩展机制——自定义适配器(adapter)。通过编写适配器,让axios底层调用uni.request,我们就可以在UniApp所有平台(H5、小程序、App)中无缝使用axios!
一、为什么需要适配器?理解axios的适配器机制
在深入代码之前,我们先来理解一个核心概念:axios的适配器到底是什么?
1.1 适配器模式的本质
适配器模式是一种设计模式,它就像一个翻译官,让两个“语言不通”的系统能够协同工作。
打个比方:你有一个通用的笔记本电脑(axios),它有一个标准的USB接口。但是小程序环境(微信小程序)只提供了一种特殊的Type-C接口(uni.request)。直接插是插不进去的。适配器就像一个USB转Type-C的转接头,一头插入笔记本电脑的USB口,另一头连接Type-C设备,让两者可以正常通信。
1.2 axios适配器的工作流程
当你在浏览器中调用axios.get()时,axios内部会执行以下流程:
用户调用 → 请求拦截器 → 适配器(adapter)→ 实际发送请求 → 响应拦截器 → 返回结果适配器是axios内部真正执行网络请求的地方。默认情况下,axios有两个内置适配器:
浏览器环境:使用
XMLHttpRequest(xhr适配器)Node.js环境:使用
http模块(http适配器)
当我们把代码运行到微信小程序时,axios默认找不到合适的适配器,就会报错:adapter is not a function。这就是为什么我们需要手动设置适配器——告诉axios:“嘿,在小程序里用这个适配器来发请求!”
1.3 适配器的统一接口规范
所有适配器都遵循统一的规范:接收一个config配置对象,返回一个Promise对象。适配器需要将uni.request的响应格式转换为axios期望的AxiosResponse格式。
// 适配器的类型定义typeAxiosAdapter= (config: AxiosRequestConfig) =>Promise<AxiosResponse>// AxiosResponse的期望结构interfaceAxiosResponse {data: any; // 响应的数据内容status: number; // HTTP状态码(如200、404)statusText: string; // 状态文本headers: any; // 响应头config: any; // 请求配置(原样返回)request?: any; // 请求对象}
理解了这个统一规范,我们就能明白:不管是自己写适配器,还是使用第三方库,核心工作就是把uni.request的响应“翻译”成axios能认得的格式。
二、方案一:自己动手实现适配器(理解原理)
我们先从手动实现一个适配器开始。这不仅能让你理解适配器的原理,还能在需要深度定制时派上用场。亲手造一遍轮子,是理解原理最好的方式。
2.1 适配器要实现哪些功能?
一个完整的适配器需要处理以下几件事:
URL构建:将
baseURL和url拼接成完整路径参数序列化:将
params对象拼接到URL查询字符串发起请求:调用
uni.request响应转换:将
uni.request的响应转换为axios期望的格式错误处理:统一处理请求失败的情况
超时控制:处理请求超时
2.2 从零开始手写适配器(带详细注释)
创建utils/axios-adapter-mp.ts:
// utils/axios-adapter-mp.tsimporttype { AxiosRequestConfig, AxiosResponse } from'axios'// 【步骤1】构建完整的URL(处理baseURL + url)functionbuildFullPath(baseURL?: string, url?: string): string {if (!baseURL||/^(https?:)?\/\//.test(url||'')) {// 如果url已经是完整URL(以http://或https://开头),直接返回returnurl||'' }// 否则拼接baseURL和urlreturn (baseURL.replace(/\/+$/, '') +'/'+ (url||'').replace(/^\/+/, ''))}// 【步骤2】将params对象拼接到URL查询字符串functionbuildURL(url: string, params?: any): string {if (!params||Object.keys(params).length===0) returnurlconstsearchParams=newURLSearchParams()for (constkeyinparams) {if (params[key] !==undefined&¶ms[key] !==null) {searchParams.append(key, String(params[key])) } }constqueryString=searchParams.toString()returnqueryString?url+ (url.includes('?') ?'&' : '?') +queryString : url}// 【步骤3】将uni.request的响应转换为AxiosResponse格式functiontransformResponse(res: UniApp.RequestSuccessCallbackResult,config: AxiosRequestConfig): AxiosResponse {return {data: res.data, // 响应数据status: res.statusCode, // HTTP状态码statusText: '', // 状态文本(小程序不提供,留空)headers: res.header|| {},// 响应头config: config, // 原样返回config,供拦截器使用request: {} // 请求对象(小程序不需要) }}// 【步骤4】将axios的config转换为uni.request的参数functionbuildUniRequestConfig(config: AxiosRequestConfig): UniApp.RequestOptions {// 4.1 构建完整URL(baseURL + url)letfullUrl=buildFullPath(config.baseURL, config.url)// 4.2 将params拼接到URL查询字符串fullUrl=buildURL(fullUrl, config.params)return {url: fullUrl,method: (config.method?.toUpperCase() ||'GET') asany,data: config.data,header: config.headers,timeout: config.timeout, }}// 【步骤5】主适配器函数exportdefaultfunctionuniAdapter(config: AxiosRequestConfig): Promise<AxiosResponse> {returnnewPromise((resolve, reject) => {// 5.1 构建uni.request需要的参数constuniConfig=buildUniRequestConfig(config)// 5.2 发起真正的网络请求uni.request({...uniConfig,success: (res) => {// 5.3 成功时,将uni的响应格式转换为axios格式resolve(transformResponse(res, config)) },fail: (err) => {// 5.4 失败时,创建符合axios规范的错误对象// 注意:这里创建的是普通Error,实际生产中应调用axios的createErrorconsterror=newError(err.errMsg||'Network Error')reject(error) } }) })}
2.3 理解每段代码的意图
上面每段代码都有清晰注释,但我再强调几个关键点:
buildFullPath:处理
baseURL和url的拼接。比如baseURL='https://api.com'和url='/user',拼接成https://api.com/user。注意要去掉重复的斜杠。buildURL:将
params对象拼接到URL上。{ id: 1, name: '张三' }会被拼接成?id=1&name=张三。transformResponse:这是最关键的“翻译”环节。
uni.request返回的响应结构是{ statusCode, header, data },而axios期望的是{ status, headers, data },所以我们做了字段映射,让上层拦截器能按照axios的习惯来处理响应。错误处理:适配器必须返回Promise,成功时resolve,失败时reject。这样axios的响应拦截器才能正确捕获。
三、方案二:使用第三方组件 axios-miniprogram-adapter(开箱即用)
自己动手写适配器虽然能理解原理,但生产环境我更推荐使用成熟的第三方库。axios-miniprogram-adapter 就是专门为小程序设计的适配器,它已经处理好了各种边界情况,开箱即用,省时省力。
3.1 安装依赖
npm install axios@0.26.0 axios-miniprogram-adapter注意:必须锁定axios版本到0.26.0。因为新版axios引入了
form-data包,它依赖的combined-stream包不支持小程序环境,构建后会报错。这是一个已经被广泛验证的兼容性问题。
3.2 配置与使用
在utils/request.ts中初始化:
// utils/request.tsimportaxiosfrom'axios'importmpAdapterfrom'axios-miniprogram-adapter'// 【关键】设置适配器(必须在创建实例前设置)axios.defaults.adapter=mpAdapter// 创建axios实例consthttp=axios.create({baseURL: 'https://api.example.com',timeout: 10000,headers: { 'Content-Type': 'application/json' }})// 请求拦截器http.interceptors.request.use( (config) => {consttoken=uni.getStorageSync('token')if (token) {config.headers.Authorization=`Bearer ${token}` }returnconfig }, (error) =>Promise.reject(error))// 响应拦截器http.interceptors.response.use( (response) => {// 根据业务码处理const { code, data, message } =response.dataif (code===200) returndatauni.showToast({ title: message||'请求失败', icon: 'none' })returnPromise.reject(newError(message)) }, (error) => {uni.showToast({ title: error.message||'网络错误', icon: 'none' })returnPromise.reject(error) })exportdefaulthttp
3.3 为什么优先推荐这个方案?
省时省力:不需要自己处理URL拼接、参数序列化、响应格式转换等细节,这些都由库内部完成。
多平台兼容:支持微信小程序、支付宝小程序、钉钉小程序、百度小程序等。
经过验证:这个库已经在大量生产项目中稳定运行,踩过的坑已经有人帮你填平了。
TypeScript支持:自带类型定义,开发体验好。
一句话总结:自己实现适配器是理解原理的最佳方式,但生产环境直接使用axios-miniprogram-adapter更高效、更可靠。
四、跨平台适配:H5 vs 小程序/App
有了适配器,我们还需要根据不同平台动态选择适配器,否则在H5中也会走小程序适配器,可能导致问题。
4.1 条件编译区分平台
importaxiosfrom'axios'// #ifdef H5// H5环境:保持默认适配器(xhr),什么都不用做// #endif// #ifdef MP-WEIXIN || APP-PLUS// 小程序/App环境:使用uni适配器importmpAdapterfrom'axios-miniprogram-adapter'axios.defaults.adapter=mpAdapter// #endifconsthttp=axios.create({baseURL: 'https://api.example.com',timeout: 10000})
4.2 方案对比与选型建议
| 对比维度 | 自己实现适配器 | axios-miniprogram-adapter | 直接使用uni.request |
|---|---|---|---|
| 实现难度 | 高(需要处理各种边界) | 极低(一行代码配置) | 低 |
| 代码量 | 多(需自己写转化逻辑) | 少 | 少 |
| 功能完整性 | 取决于实现质量 | 完整(经过验证) | 基础 |
| 拦截器支持 | 依赖axios | 完整支持axios拦截器 | 需自己实现 |
| 适用场景 | 理解原理、深度定制 | 生产环境首选 | 简单项目 |
我的建议:
新项目:直接用
axios-miniprogram-adapter,省心省力。理解原理:自己手写一遍适配器,但不要在生产中使用。
追求极简:直接使用
uni.request,但会失去axios的拦截器生态。
五、企业级请求封装:从方案到完整代码
有了适配器,我们就可以在axios的基础上进行企业级封装了。下面我们按照功能模块逐一实现。
5.1 项目结构
src/├── utils/│ ├── request.ts # 核心请求封装(对外导出)│ └── interceptors/ # 拦截器模块│ ├── systemParams.ts # 系统参数填充│ ├── token.ts # token管理│ ├── sign.ts # 请求签名│ ├── encrypt.ts # 加解密│ ├── repeat.ts # 防重复提交│ ├── i18n.ts # 国际化│ ├── loading.ts # 加载遮罩│ └── errorHandler.ts # 全局异常处理
5.2 系统参数填充拦截器
自动添加时间戳、设备ID、应用版本等公共参数。
// interceptors/systemParams.tsimport { getDeviceId, getAppVersion, getPlatform } from'@/utils/helper'exportfunctionsystemParamsRequestInterceptor(config: any) {consttimestamp=Date.now()constdeviceId=getDeviceId()constappVersion=getAppVersion()constplatform=getPlatform()constcommonParams= { timestamp, deviceId, appVersion, platform }// GET请求的参数放在params中,POST放在data中if (config.method==='get') {config.params= { ...config.params, ...commonParams } } else {config.data= { ...config.data, ...commonParams } }returnconfig}
设计思路:timestamp用于防重放攻击,deviceId用于设备追踪,appVersion用于版本兼容判断。这些参数由拦截器统一添加,业务层完全无感知。
5.3 Token管理拦截器
// interceptors/token.tsexportfunctiontokenRequestInterceptor(config: any) {consttoken=uni.getStorageSync('token')if (token) {config.headers=config.headers|| {}config.headers['Authorization'] =`Bearer ${token}` }returnconfig}exportfunctiontokenResponseInterceptor(response: any) {// 401代表token过期或无效if (response.status===401) {uni.removeStorageSync('token')uni.showToast({ title: '登录已过期,请重新登录', icon: 'none' })setTimeout(() => {uni.navigateTo({ url: '/pages/login/login' }) }, 1500)returnPromise.reject(newError('登录过期')) }returnresponse}
设计思路:请求拦截器自动注入token,响应拦截器自动处理401状态码跳转登录。这样业务代码中不需要重复写这些逻辑。
5.4 请求签名拦截器
// interceptors/sign.tsimportmd5from'md5'functionsortParams(obj: any): string {constkeys=Object.keys(obj).sort()letstr=''for (constkeyofkeys) {if (obj[key] !==undefined&&obj[key] !==null) {str+=`${key}=${obj[key]}&` } }returnstr.slice(0, -1)}exportfunctionsignRequestInterceptor(config: any) {// 允许通过needSign: false跳过签名if (config.needSign===false) returnconfigconstsecret='your-secret-key'letparams: any= {}if (config.method==='get') {params= { ...config.params } } else {params= { ...config.data } }constsortedStr=sortParams(params)constsign=md5(sortedStr+secret)config.headers=config.headers|| {}config.headers['X-Sign'] =signreturnconfig}
设计思路:签名的作用是防止请求参数被篡改。签名算法:将所有参数按键名排序后拼接,再加上密钥,计算MD5值。这样即使请求被拦截,攻击者也无法在不破坏签名的情况下修改参数。
5.5 加解密拦截器(按需开启)
// interceptors/encrypt.tsimportCryptoJSfrom'crypto-js'constSECRET_KEY='your-16-char-key'exportfunctionencryptRequestInterceptor(config: any) {if (config.encrypt&&config.data) {constjsonStr=JSON.stringify(config.data)constencrypted=CryptoJS.AES.encrypt(jsonStr, SECRET_KEY).toString()config.data= { encrypted } }returnconfig}exportfunctiondecryptResponseInterceptor(response: any) {if (response.config.decrypt&&response.data?.encrypted) {constbytes=CryptoJS.AES.decrypt(response.data.encrypted, SECRET_KEY)constdecrypted=bytes.toString(CryptoJS.encUtf8)response.data=JSON.parse(decrypted) }returnresponse}
设计思路:敏感数据(如手机号、身份证号)在传输过程中加密,可以有效防止中间人攻击。使用encrypt和decrypt参数控制,按需开启,避免不必要的性能损耗。
5.6 防重复提交拦截器
// interceptors/repeat.tsconstpendingRequests=newMap()functiongenerateKey(config: any): string {const { method, url, params, data } =configconstrequestData=method==='get'?params : datareturn`${method}&${url}&${JSON.stringify(requestData)}`}exportfunctionrepeatRequestInterceptor(config: any) {if (config.noRepeat===false) returnconfigconstkey=generateKey(config)if (pendingRequests.has(key)) {// 相同的请求正在执行中,直接返回已存在的PromisereturnpendingRequests.get(key) }letresolveFn: any, rejectFn: anyconstpromise=newPromise((resolve, reject) => {resolveFn=resolverejectFn=reject })pendingRequests.set(key, promise)config._repeatKey=keyconfig._repeatResolve=resolveFnconfig._repeatReject=rejectFnreturnconfig}exportfunctionrepeatResponseInterceptor(response: any) {constkey=response.config._repeatKeyif (key&&pendingRequests.has(key)) {const { _repeatResolve, _repeatReject } =response.configif (responseinstanceofError) {_repeatReject(response) } else {_repeatResolve(response) }pendingRequests.delete(key) }returnresponse}
设计思路:防止用户在短时间内重复点击按钮,导致重复发送相同请求。用请求的method+url+参数作为唯一标识,记录正在进行的请求,后续相同请求直接复用。
5.7 国际化拦截器
// interceptors/i18n.tsexportfunctioni18nRequestInterceptor(config: any) {constlang=uni.getStorageSync('language') ||'zh-CN'config.headers=config.headers|| {}config.headers['Accept-Language'] =langreturnconfig}
设计思路:根据用户选择的语言或系统语言,自动在请求头中添加Accept-Language,后端可以根据这个头返回对应语言的提示信息。
5.8 加载遮罩拦截器(并发控制)
// interceptors/loading.tsletrequestCount=0exportfunctionloadingRequestInterceptor(config: any) {if (config.showLoading!==false) {requestCount++if (requestCount===1) {uni.showLoading({ title: config.loadingText||'加载中...', mask: true }) } }returnconfig}exportfunctionloadingResponseInterceptor(response: any) {if (response.config.showLoading!==false) {requestCount--if (requestCount===0) {uni.hideLoading() } }returnresponse}
设计思路:使用计数器管理多个并发请求,只有第一个请求触发时显示loading,最后一个请求完成时关闭loading,避免多个loading重叠导致的UI闪烁。
5.9 组装完整的请求实例
// utils/request.tsimportaxiosfrom'axios'import { systemParamsRequestInterceptor } from'./interceptors/systemParams'import { tokenRequestInterceptor, tokenResponseInterceptor } from'./interceptors/token'import { signRequestInterceptor } from'./interceptors/sign'import { encryptRequestInterceptor, decryptResponseInterceptor } from'./interceptors/encrypt'import { repeatRequestInterceptor, repeatResponseInterceptor } from'./interceptors/repeat'import { i18nRequestInterceptor } from'./interceptors/i18n'import { loadingRequestInterceptor, loadingResponseInterceptor } from'./interceptors/loading'import { errorResponseInterceptor } from'./interceptors/errorHandler'// 创建axios实例consthttp=axios.create({baseURL: import.meta.env.VITE_API_BASE_URL||'https://api.example.com',timeout: 10000,headers: { 'Content-Type': 'application/json' }})// 条件编译:小程序/App环境下使用适配器// #ifdef MP-WEIXIN || APP-PLUSimportmpAdapterfrom'axios-miniprogram-adapter'http.defaults.adapter=mpAdapter// #endif// 注册请求拦截器(按顺序执行)http.interceptors.request.use(systemParamsRequestInterceptor)http.interceptors.request.use(tokenRequestInterceptor)http.interceptors.request.use(i18nRequestInterceptor)http.interceptors.request.use(signRequestInterceptor)http.interceptors.request.use(encryptRequestInterceptor)http.interceptors.request.use(repeatRequestInterceptor)http.interceptors.request.use(loadingRequestInterceptor)// 注册响应拦截器http.interceptors.response.use(repeatResponseInterceptor)http.interceptors.response.use(decryptResponseInterceptor)http.interceptors.response.use(tokenResponseInterceptor)http.interceptors.response.use(loadingResponseInterceptor)http.interceptors.response.use(response=>response, errorResponseInterceptor)exportdefaulthttp
5.10 使用示例
// 在页面中使用importhttpfrom'@/utils/request'// 基础GET请求(自动添加token、签名、loading)constuserInfo=awaithttp.get('/user/info', { params: { id: 1 } })// POST请求,加密敏感数据constorderRes=awaithttp.post('/order/create', { goodsId: 123, quantity: 2, phone: '13800138000' }, { encrypt: true, loadingText: '提交中...' })// 跳过签名(如登录接口)constloginRes=awaithttp.post('/public/login', { username, password }, { needSign: false })// 禁用防重复检测(如频繁上报位置)constlocationRes=awaithttp.post('/location/report', { lat, lng }, { noRepeat: true })
六、核心难点与注意点
6.1 axios版本锁定
为什么必须锁定axios@0.26.0?新版axios引入了form-data包,它所依赖的combined-stream包包含了Node.js特定的模块,在小程序环境中无法运行,构建后会报错。这是一个已经被广泛验证的兼容性问题。
6.2 适配器的挂载时机
适配器必须在创建axios实例之前挂载到axios.defaults,或者直接在axios.create()中传入adapter选项。如果先创建实例再挂载,已创建的实例不会继承新设置的适配器。
// 正确顺序axios.defaults.adapter=mpAdapterconsthttp=axios.create({ ... })// 或consthttp=axios.create({ adapter: mpAdapter, ... })
6.3 loading并发控制的重要性
如果不做并发控制,多个请求同时发起时会显示多个loading,或者第一个请求结束后就把loading关闭了,但后面的请求还在进行中,用户体验极差。用计数器管理是最常见的解决方案。
6.4 签名与加解密的性能影响
签名和加密操作会消耗CPU时间。对于高频请求(如轮询),建议关闭这些功能,或者只在关键接口上开启。
6.5 防重复提交的有效性
防重复提交只对完全相同的请求有效(相同URL、相同方法、相同参数)。不同参数的请求被视为不同,不会被拦截。
七、常见错误与解决方案
❌ 错误1:打包到小程序时报错 "adapter is not a function"
现象:小程序中调用接口时报错。
原因:未设置适配器,或适配器设置顺序错误,或axios版本不兼容。
解决方案:
确保安装了
axios@0.26.0和axios-miniprogram-adapter在创建axios实例之前设置
axios.defaults.adapter = mpAdapter使用条件编译确保只在需要适配器的平台设置
❌ 错误2:请求参数后端收不到
现象:前端传了参数,后端收到的却是空对象。
原因:GET和POST的参数传法搞混了。GET用params,POST用data。
解决方案:
// GET请求http.get('/user', { params: { id: 1 } })// POST请求http.post('/user', { name: '张三' })
❌ 错误3:签名验证失败
现象:接口返回签名错误。
原因:前后端排序规则不一致,或参数中包含特殊字符未处理,或空值处理方式不同。
解决方案:与后端同学确认签名规则,统一使用encodeURIComponent对参数值编码后再签名。
❌ 错误4:loading一直不消失
现象:请求完成后loading还在转。
原因:并发请求的计数器没有正确减少,可能是某些请求在失败时没有触发计数减1。
解决方案:确保在错误拦截器中也减少计数。
exportfunctionloadingErrorInterceptor(error: any) {if (error.config?.showLoading!==false) {requestCount--if (requestCount===0) {uni.hideLoading() } }returnPromise.reject(error)}
❌ 错误5:H5跨域问题
现象:H5环境下请求被CORS策略阻止。
原因:前端请求的域名与后端不同,且后端未开启CORS。
解决方案:在vite.config.js中配置代理,或后端配置Access-Control-Allow-Origin。
八、总结
今天我们全面学习了UniApp中使用axios的方法:
理解适配器原理:知道了为什么需要适配器、适配器的工作流程、统一接口规范,这是解决所有跨平台请求问题的理论基础。
两种适配方案:手写适配器理解原理,使用
axios-miniprogram-adapter高效落地,两种方案相辅相成。企业级封装:通过拦截器实现了系统参数、token管理、签名、加解密、防重复、国际化、loading、错误处理等生产级功能。
最后送你三句话:
理解原理是最佳实践:自己动手写一遍适配器,能让你在遇到问题时知道从哪里下手排查。
生产环境用成熟方案:
axios-miniprogram-adapter已经帮你处理好了各种边界情况,直接用它更高效。好的封装不是代码行数多,而是让业务代码变得简洁、让团队协作更顺畅。
希望这篇教程能帮你在UniApp项目中用好axios。如果在实践中遇到问题,欢迎带着你的代码来找我。
加油,未来的全栈大佬!💪如果你也对移动端跨端开发感兴趣,关注我,后续还有更多优质文章分享!

往期相关文章推荐
夜雨聆风