资讯动态

uni-app接口调用实战:从基础封装到跨端兼容,一篇讲透

发布时间:2026/10/14 9:45:01 来源:尧图企业网站定制
1. 从一次真实需求说起为什么要专门写一篇接口调用的文章上个月接手一个跨端管理系统的改造业务方要求同一个产品覆盖微信小程序、iOS App 和 H5 三个入口。技术选型没有悬念直接用 uniapp 一套代码三端复用。骨架搭起来很快页面 UI 也顺利但到了联调接口这一步团队里几个人陆续卡住了有的在浏览器里调试得好好的一跑到小程序就报url not in domain list有的人把 axios 的习惯直接搬进来发现promise风格拿不到返回值还有人被后端反问“你传的到底是不是 form 表单”愣在当场说不出话。这个场景想必很多搞 uniapp 的都经历过。uniapp 调取接口的方法表面上看就是uni.request一个 API但真正上手你会发现这里面牵扯到请求封装、跨端差异、Content-Type 设置、证书域名校验、超时重试机制等等一系列细节。任何一个环节没处理好轻则多花半天排查时间重则线上版本直接翻车。这篇文章就从实际开发的角度出发把 uniapp 调取接口的完整链路捋一遍从最基础的uni.request写法到业务层封装思路再到 H5、小程序、App 三端的差异化处理最后附上我实际踩坑记录和排查清单。不管你是拿 uniapp 写过两三个页面的新手还是已经在做跨端项目的中级开发者这里都有你能直接抄走的方案。2. 原生请求的十八般武艺uni.request 的完整用法拆解2.1 最基础的 GET 和 POST 调用先把看家本领拿出来。uniapp 内置了uni.request这就是默认的 HTTP 客户端调用方式和微信小程序的wx.request几乎一致——如果你写过小程序上手基本零成本。一个最普通的 GET 请求长这样uni.request({ url: https://api.example.com/v1/user/info, method: GET, data: { userId: 10086 }, success: (res) { // 注意这里是 res.data不是 res console.log(请求成功, res.data) }, fail: (err) { // 网络异常、超时、域名不存在等情况走这里 console.log(请求失败, err.errMsg) } })POST 请求的差异只在两个地方method改成POST同时显式声明请求头里的Content-Typeuni.request({ url: https://api.example.com/v1/login, method: POST, header: { Content-Type: application/json }, data: { account: admin, password: 123456 }, success: (res) { if (res.statusCode 200) { console.log(登录成功, res.data) } } })这里有一个新接触 uniapp 的人最容易搞混的点success回调里拿到的res是完整响应对象里面包含了statusCode、header、data、errtMsg等字段。你要的业务数据在res.data里面而不是res本身。后端返回了什么res.data就是什么——如果后端返回的是 JSON 字符串res.data甚至可能是一个字符串需要你再JSON.parse一次这跟浏览器里的fetch、axios自动解析 JSON 的行为不一样。2.2 method、data、header、timeout 等关键参数逐个说uni.request可配置的参数常用的没那么多但每个都有注意点。url是请求地址开发的时候你可能图省事写成相对路径/api/loginH5 端配合 devServer 代理没问题但打包到小程序和 App 上必挂。因为小程序运行环境里根本没有“当前站点host”这种概念必须写完整的https://地址。我建议从第一天写项目起所有接口地址都拼成一个完整的绝对 URL后面省心很多。method的可选值有GET、POST、PUT、DELETE、OPTIONS、HEAD等和 HTTP 方法一一对应。需要注意uni.request默认值就是GET所以很多初学者写了 POST 逻辑却忘记改 method请求发了后端拿到却是一脸懵。每次写完请求先检查 method 再提交代码。data是发送到后端的参数类型可以是对象也可以直接给字符串。当Content-Type是application/json时对象会被序列化成 JSON 字符串当Content-Type是application/x-www-form-urlencoded时对象会被序列化成keyvaluekey2value2的格式。这块后面第三节再展开因为它是跨端联调里的大坑。header是个普通对象塞请求头用的。除了Content-Type最常见的还有Authorization和token——有的后端要Authorization: Bearer xxx有的后端自定义了一个token字段按对方文档填就行。timeout单位是毫秒默认 60000小程序端一部分基础库版本默认是 60000H5 端略不同。像上传图片这类长任务你可以加到 30000但登录、列表这种接口建议设成 8000 或 10000不然后端假死的时候用户会对着页面发呆十秒钟。responseType默认是text如果需要后端返回二进制把它设成arraybuffer。做文件下载、图片流渲染的时候才会用到日常接口不用管。success、fail、complete三个回调覆盖了三种情况业务请求成功、网络层失败、无论成功失败都要执行的逻辑。比如请求时要先uni.showLoading那就在complete里uni.hideLoading不管结果是成功还是失败都能关掉弹窗避免页面卡死一个转圈。2.3 返回值最大痛点uni.request 本身不支持 Promise用习惯了fetch、axios的人上手uni.request会很难受它没有 promise不支持async/await只能在success回调里一层一层往下写。两个接口有依赖关系就得嵌套回调三四个接口串起来代码就是“回调地狱”。我们做个简单统计一个列表页可能先要拿到用户信息根据用户信息里的权限码判断请求哪个数据接口再拿数据渲染。用原生的写法uni.request({ url: https://api.example.com/user/info, method: GET, success: (userRes) { const permissions userRes.data.permissions let apiUrl https://api.example.com/list/default if (permissions.includes(admin)) { apiUrl https://api.example.com/list/admin } uni.request({ url: apiUrl, method: GET, success: (listRes) { // 到这里代码已经是两层嵌套了 this.list listRes.data } }) } })这还只是两层实际项目里加上 loading、错误处理、token 刷新代码会越来越乱。解决方案是手动包一层 Promise把uni.request变成可await的版本。这也是后面讲请求封装的最基础一步。3. 进阶实战把请求模块化代码瞬间清爽3.1 动手封装一个 Promise 版本的基础请求函数这一节直接给能落地的代码。我们构建一个request.js文件先做最基础的一层封装// utils/request.js const request (options) { return new Promise((resolve, reject) { uni.request({ url: options.url, method: options.method || GET, data: options.data || {}, header: options.header || {}, timeout: options.timeout || 10000, success: (res) { // HTTP 状态码 2xx 视为成功其他交给 reject if (res.statusCode 200 res.statusCode 300) { resolve(res.data) } else { reject(res) } }, fail: (err) { reject(err) } }) }) } export default request封装完之后原来的嵌套代码可以改写成这样import request from /utils/request.js async function getList() { const userInfo await request({ url: https://api.example.com/user/info, method: GET }) const permissions userInfo.permissions const apiUrl permissions.includes(admin) ? https://api.example.com/list/admin : https://api.example.com/list/default const listData await request({ url: apiUrl, method: GET }) this.list listData }逻辑清晰多了吧。这里有个微妙的点resolve、reject之后调用方通过await拿到的返回值到底是什么取决于你在封装函数里怎么处理。我上面的写法是直接resolve(res.data)把后端真正的数据抛出去业务层拿到就直接用不需要再剥一层壳。如果你在后端约定的结构里还有code、data、message三层也可以在这一层先做一个统一判断// 后端返回结构约定{ code: 0, data: {...}, message: ok } success: (res) { if (res.statusCode 200 res.data.code 0) { resolve(res.data.data) } else { reject(res.data) } }这样业务侧连code 0都不用判断永远只拿业务数据。如果后端报错直接走catch错误对象里带message可以直接弹给用户。3.2 全局拦截器和认证信息注入的几种常规方案项目一旦上了规模每个接口都要带 token每个 401 都要统一跳登录页。把这些逻辑全部塞进unk层其实是错的。最合理的做法是把拦截逻辑写进封装函数里而不是让每个页面去处理。改造一下request.js——增加请求前处理、响应后处理两个钩子// utils/request.js // 读缓存中的登录态 const getToken () { const token uni.getStorageSync(token) return token ? Bearer ${token} : } const request (options) { // 请求前统一处理比如加 token const header { Content-Type: application/json, ...(options.header || {}) } const token getToken() if (token) { header.Authorization token } return new Promise((resolve, reject) { uni.request({ url: options.url, method: options.method || GET, data: options.data || {}, header, timeout: options.timeout || 10000, success: (res) { if (res.statusCode 401) { // 登录态失效统一清理缓存并跳转登录页 uni.removeStorageSync(token) uni.navigateTo({ url: /pages/login/login }) reject({ code: 401, message: 登录已过期 }) return } if (res.statusCode 200 res.statusCode 300) { if (res.data.code 0) { resolve(res.data.data) } else { // 业务错误统一弹 toast页面不需要再加判断 uni.showToast({ title: res.data.message || 请求失败, icon: none }) reject(res.data) } } else { reject(res) } }, fail: (err) { // 统一处理网络异常 uni.showToast({ title: 网络异常请检查网络设置, icon: none }) reject(err) } }) }) }这个版本的好处是业务开发者不需要在任何页面里写uni.showToast接口失败自动提示不需要在每个页面判断登录态401 自动跳登录页也不需要手动塞 token统一在header注入。当然如果你想给某些接口单独跳过 token可以在 options 里加一个标记位灵活控制这里不展开。要提醒一点uni.addInterceptor也能拦截uni.request从官方能力上看更“正统”但我实测下来在部分安卓真机上对于uni.request的拦截存在兼容问题表现为 promise 链有时不执行。如果你的项目一定要用务必在真机上多测几个版本。我的建议是优先采用“包一层函数”的方案它对所有端都是天然兼容的因为本质上你就是在调用一个普通函数。3.3 按业务模块拆分的 API 层设计封装好request.js之后再往前走一步不要在每个页面里直接写request({ url: ... })而是把接口按业务模块拆分到独立文件里形成“一个模块一个 js”的结构。举个例子用户相关的接口放进api/user.js// api/user.js import request from /utils/request.js export const login (account, password) { return request({ url: https://api.example.com/v1/login, method: POST, data: { account, password } }) } export const getUserInfo () { return request({ url: https://api.example.com/v1/user/info, method: GET }) } export const updateUserProfile (data) { return request({ url: https://api.example.com/v1/user/profile, method: PUT, data }) }订单模块放在api/order.js商品模块放在api/product.js。页面里这么用import { login } from /api/user.js async function handleLogin() { try { const userInfo await login(admin, 123456) uni.setStorageSync(token, userInfo.token) } catch (e) { // 错误已经被统一 toast 过了这里只需要处理业务逻辑 } }这样做的价值分成两层第一层接口路径不再散落在每个页面里后端换地址只改一个文件第二层页面代码只关心“登录”这个动作不关心它是怎么请求的维护成本大幅降低。团队里如果有人专门维护接口层对页面开发者来说就是透明的你只需要知道调用哪个函数、传什么参数、返回什么结构。4. 跨端环境下的隐性差异H5、小程序、App 各有各的脾气4.1 H5 端跨域问题从原理到解决方案H5 跑在浏览器里就绕不开浏览器的同源策略。你本地启动的 uniapp 开发服务器是localhost:5173后端接口在https://api.example.com协议不同、端口不同就是跨域。浏览器看到跨域请求会先发一个OPTIONS预检请求后端如果不回Access-Control-Allow-Origin真实请求就不会发出。你打开浏览器开发者工具的 Network 面板会看到一堆CORS error。最省事的开发期方案是在 uniapp 项目根目录的manifest.json里配置 H5 的 devServer 代理。找到h5.devServer字段加上 proxy 配置{ h5: { devServer: { proxy: { /api: { target: https://api.example.com, pathRewrite: { ^/api: /v1 } } } } } }这样页面里的url写成/api/login请求经过 devServer 转发到目标后端浏览器感知不到跨域开发期调试非常顺手。需要注意/api和/v1的路径映射要跟后端实际路由对齐否则你看到的就是 404。生产环境上线 H5 时devServer 不再生效常见做法是让运维在 nginx 里配反向代理把/api/前缀的请求全部转发到后端服务。后端开 CORS 也是一种方案但如果你有多个前端应用消费同一套接口统一走 nginx 反代反而更可控。顺带一提如果你在小程序端的代码里用的是绝对 URLH5 端跨域问题被 nginx 代理解决后前端基本不需要做任何按端区分的条件编译这也是我把“所有请求统一写绝对地址”视为最佳实践的原因。4.2 小程序端的域名校验和开发工具配置小程序是异类中的异类。它不存在浏览器的 CORS 概念请求直接由客户端发出但微信强约束了请求域名所有接口域名必须是 HTTPS 且配置在小程序后台的“request 合法域名”列表里。大部分新手在小程序开发者工具里跑通了一个 HTTP 接口真机预览却发现请求全军覆没报错信息自带xxx is not in domain list的字眼。原因就是工具自带“不校验合法域名”开关真机上这个开关不存在了。我处理此类问题的优先级是这样在开发阶段开发者工具右上角“详情 → 本地设置”里勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”保证开发效率和真机调试提审前把线上接口域名提前配置到微信公众平台的“开发管理 → 服务器域名 → request 合法域名”里保存后一般一分钟内生效。如果你的域名是新的还没备案或没配 HTTPS 证书那后端要先把这一套补齐否则没法上小程序。另一件小事是 TLS 版本。微信要求 TLS 1.2 及以上如果你后端服务器用的是比较老的配置客户端请求可能直接握手失败。在浏览器上看接口是好的但在小程序里errMsg显示request:fail这种情况优先检查服务器 TLS 配置而不是前端代码。4.3 App 端的网络权限和证书细节App 端跑的是原生网络栈在 Android 上要注意AndroidManifest.xml里有没有INTERNET权限。uniapp 默认打包模板一般会带上但如果你是用离线打包或者自定义的 manifest漏掉INTERNET权限的后果就是所有的网络请求全部失败而且fail回调里的错误信息经常让人摸不着头脑提示的是“网络异常”而不是“权限缺失”。iOS 端更折磨人的是 ATSApp Transport Security。iOS 默认禁止不安全的 HTTP 连接你的接口如果是 HTTP 明文在 iOS 的真机上会被拦下来。开发阶段你可以临时在manifest.json配置允许任意加载NSAllowsArbitraryLoads设为 true但这是治标不治本的方案——App Store 审核时如果看到你开了全局 HTTP 豁免可能会以“违规收集用户信息”、“不安全传输”的理由打回。过审版本还是要上 HTTPS 证书实在有特殊场景必须 HTTP也要限定在特定域名而不是全局放开。这部分的调试经验是App 短真机联调时如果请求一直失败用 PC 上的抓包软件先确认请求到底有没有发出去、后端到底有没有响应。如果根本没发出去大概率是权限或证书问题如果发出去了但没有响应那是后端或网络环境问题。不要一上来就在前端代码里盲目加header或者改data格式。5. Content-Type 引发的惨案json 还是 form跨端各有讲究5.1 application/json 与 x-www-form-urlencoded 的区别这是接口联调时最容易“死得不明不白”的地方。后端工程师习惯用 Postman 调试他们可能测通了 JSON 格式也可能测通的是表单格式但前端一旦传错格式后端的参数解析逻辑就会拿不到值报错信息往往是“参数缺失”或者Required String parameter xxx is not present。application/json对应的传参方式是把参数序列化成 JSON 字符串放入请求体。现在的主流后端框架只要控制器方法用RequestBody接收对象就是这个格式。application/x-www-form-urlencoded则是传统的表单提交格式参数长这样titletestcontenthello。后端用RequestParam或request.getParameter接收。uni.request的默认行为需要明确一下如果data传的是一个对象且header没设置Content-Typeuni-app 在不同平台的默认值可能不同。H5 端默认可能是application/json小程序端也是 json但我碰到过个别第三方组件或自定义基座改变了默认值的场景。为了不出幺蛾子跨端项目里必须在header里显式写明Content-Type。当后端要求x-www-form-urlencoded时如果data传了对象uni-app 一般会帮我们序列化成查询字符串但我更推荐自己拼好字符串// 方式一传对象依赖框架序列化 uni.request({ url: https://api.example.com/v1/form, method: POST, header: { Content-Type: application/x-www-form-urlencoded }, data: { title: hello, content: world } }) // 方式二手动拼接字符串 const body title${encodeURIComponent(hello)}content${encodeURIComponent(world)} uni.request({ url: https://api.example.com/v1/form, method: POST, header: { Content-Type: application/x-www-form-urlencoded }, data: body })方式二看起来麻烦但它绕开不同端对对象序列化处理不一致的问题。我在部分安卓机型上确实碰到过对象传表单格式时字段顺序被打乱、中文编码出错的现象手动拼字符串后世界清净了。5.2 常见的“明明传了却拿不到”的排查思路遇到后端说“我没收到参数”不要急着跟后端争论先按以下顺序自查。第一步打开开发者工具或抓包工具看请求体里的具体内容。如果是 json 格式确认花括号里的字段名跟后端RequestBody的实体字段严格一致注意大小写和下划线分割差异。很多后端是 Java 风格字段叫userName前端传了username看起来差不多实际对不上。第二步确认Content-Type和后端接收方式匹配。后端如果是 key-value 方式接收你传 json 就识别不了后端如果是RequestBody接收你传表单也直接报错。这一步对不齐后面全是白搭。第三步检查数据有没有被二次处理。比如data里套了一层{ data: {...} }后端实体没这层结构自然解析失败。或者是data传了FormData对象跨端兼容不一致部分端直接变成[object Object]。第三点特别想多说一句uniapp 里FormData在 App 端和 H5 端的表现差异极大如果你是为了传文件而用FormData建议直接改用uni.uploadFile那是官方封装好的上传组件能处理multipart/form-data比手动构造FormData可靠得多。6. 高频问题排查与避坑经验速查6.1 典型报错与解决方案对照把我在项目里碰到过的高频问题整理成了一份速查表方便你直接定位问题报错现象可能的根因解决动作request:fail网络断开、域名无法解析、证书错误抓包确认请求是否到达服务器检查 WiFi 和 4G 切换测试url not in domain list小程序 request 合法域名未配置去小程序后台添加域名开发工具勾选“不校验合法域名”跨域 CORS 报错H5 端同源策略拦截开发期配置 devServer proxy上线用 nginx 反代或后端开 CORS后端收不到参数Content-Type 不匹配核对后端接收方式是 json 还是表单显式声明 header返回数据是字符串responseType是 text如果需要 JSON建议在封装层统一JSON.parse并做容错请求超时后端响应慢或 timeout 设置过短区分接口类型设置合理 timeout排查后端慢查询App 端请求全部失败Android 缺网络权限或 iOS ATS 限制 HTTP检查 manifest 配置的权限iOS 开发期临时允许 HTTP线上必须 HTTPS401 循环跳转登录401 拦截逻辑没有区分单次请求和并发请求加一个标记位防止多个请求同时 401 时重复跳转6.2 五个最容易踩但网上不太有人提的坑坑一success回调里拿statusCode判错位置。有人把业务成功的判断写成了if (res.data.statusCode 200)但res.data是后端返回的数据后端的业务结构里大概率没有statusCode于是永远进不了成功分支。正确写法是res.statusCode。坑二请求地址里带空格和中文没编码。URL 里有中文参数在 H5 端浏览器会自动编码但是在小程序端不同版本的编码行为不一致最稳妥的方式是自己提前encodeURIComponent。我建议所有 query 参数都手动编一次不要依赖框架。坑三并发请求的 loading 处理。如果页面上同时发出三个请求每个请求complete里都执行uni.hideLoading第一个请求回来就把 loading 关了后面的请求还在飞用户体验像抽搐一样。解决方案是加一个计数器请求发起时结束时--只有归零才隐藏 loading。这也解释了为什么很多成熟项目都会在封装层做并发控制。坑四token 过期后并发请求进来全部 401结果同时发起了多次“重新登录”跳转。处理办法是第一次遇到 401 时设置一个全局标记后续请求直接排队等待登录完成再统一重放。这个逻辑在封装函数里实现要花点心思但效果直接决定体验是否正常。坑五uni.request的url长度限制在不同端不一样。小程序端 URL 长度上限有限如果你把一长串参数全拼在 query 里到了真机可能被截断后端就收不到完整参数。凡是参数多的接口优先走 POST body而不是 GET query。6.3 一键产出可用代码我的最终版 request.js 模板前面讲了很多细节这里直接把一个适合大多数中后台项目的request.js完整版贴出来你复制过去改下 baseURL 就能用// utils/request.js const BASE_URL https://api.example.com const getToken () uni.getStorageSync(token) || const request (options) { let url options.url // 传相对路径时自动拼接完整地址兼容多端 if (!/^https?:\/\//.test(url)) { url BASE_URL url } const header { Content-Type: application/json, ...(options.header || {}) } const token getToken() if (token) { header.Authorization token } return new Promise((resolve, reject) { uni.request({ url, method: options.method || GET, data: options.data || {}, header, timeout: options.timeout || 15000, success: (res) { if (res.statusCode 401) { uni.removeStorageSync(token) uni.navigateTo({ url: /pages/login/login }) reject({ code: 401, message: 登录已过期 }) return } if (res.statusCode 200 res.statusCode 300) { // 约定后端成功码为 0 if (res.data res.data.code 0) { resolve(res.data.data) } else { uni.showToast({ title: (res.data res.data.message) || 请求失败, icon: none }) reject(res.data || res) } } else { uni.showToast({ title: 服务异常[${res.statusCode}], icon: none }) reject(res) } }, fail: (err) { uni.showToast({ title: 网络异常请稍后重试, icon: none }) reject(err) } }) }) } export default request要注意如果你接入的后端成功码不是0比如是200或者是success把res.data.code 0那个判断改成对应规则即可。另外这个模板里Content-Type默认写了 json遇到需要表单提交的接口可以在调用侧覆盖request({ url: /user/login, method: POST, header: { Content-Type: application/x-www-form-urlencoded }, data: accountadminpassword123456 })header 里的同名键会覆盖默认值这一层灵活度是封装后最实用的部分之一。7. 写在最后我对 uniapp 接口调用的几点体会用 uniapp 调取接口说难不难说简单也不简单。核心的 API 就一个uni.request但它背后绑定了跨端兼容、请求规范、业务分层等一系列工程化问题。我个人做了几个跨端项目后的体会是尽早把请求层封装统一起来比再多的“页面里临时调一下”都重要。封装的好处不在于少写几行代码而在于你能在一次改动里修复所有页面共性的问题——比如统一加 token、统一处理 401、统一规范错误提示。这些习惯到项目后期会产生复利效应维护成本差距非常明显。最后再分享一个小技巧调试接口时不要只在浏览器里验证定期用小程序开发者工具、App 自定义基座跑一遍真实环境。跨端项目最大的风险永远是“某个端单独出问题”而这个问题往往要到真机联调阶段才暴露。把这种多端验证变成每天的例行操作你会少熬很多个深夜。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑