ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Vue与后端交互实战:从axios封装到跨域与Token管理

Vue与后端交互实战:从axios封装到跨域与Token管理 1. 前后端交互这件事本质到底是什么很多同学学Vue学到组件、路由、状态管理都觉得挺顺一到“和后端联调”就开始犯怵。其实你想想Vue本身只是个视图层框架它管的是页面长什么样、数据怎么展示至于数据从哪来、怎么发给服务器它天然是不关心的。所以“Vue与后端交互”这个说法准确点讲是“在Vue项目里如何组织HTTP请求并把拿到的数据交给Vue去渲染”。我见过不少新手把交互想得很玄其实拆开看就三件事发请求、收响应、处理状态。发请求就是告诉后端“我要什么数据”或者“我要提交什么”收响应就是后端把结果丢回给你处理状态则是你在页面上根据请求进行中、成功、失败这三种情况来更新UI。这三件事放在一起才是完整的交互链路。如果你只是会用axios发个GET请求拿到数据渲染出来那不叫会交互那只是第一步。真正干活的时候你要考虑超时怎么办、Token过期怎么办、接口报错怎么提示用户、并发请求怎么处理、上传文件怎么带进度条……这些都是Vue与后端交互里的“隐藏关卡”。有一说一前端和后端之间打交道用的协议90%以上还是HTTP。哪怕你项目里用了WebSocket做实时推送也绕不开HTTP来做最开始的握手和鉴权。所以理解交互先理解HTTP请求的基本构成请求方法、URL、请求头、请求体、响应状态码、响应体。Vue这边的工作无非是把这些内容用一种更舒服的方式封装起来让你在组件里不要天天面对一堆底层细节。这里我多说一句很多人一上来就搜“Vue怎么调后端接口”然后照着教程写了一个axios.get就觉得自己会了。这种学习方式没问题但一定要往后多看一步——搞清楚axios帮你做了什么没帮你做什么。axios帮你序列化参数、解析响应、处理请求头但它不会替你处理业务错误也不会替你管理登录状态。那些事情得你自己设计。2. 交互之前先把环境跑通2.1 你确定你的开发服务器是真在跑吗在讲任何拦截器、Token、跨域之前我先泼一盆冷水我帮人排查“为什么Vue调不到后端接口”最后发现后端压根没启动的情况至少占三成。这不是开玩笑人在工位坐久了真的会眼瞎前端里报了500你在那儿反复改axios配置改了半天发现是后端服务挂了你不知道。所以第一步先确认几件事后端服务启动在哪个端口能不能在浏览器里直接访问接口文档里的路径是/api/user/list还是/user/list后端有没有要求特定的请求头。在Vue工程里我习惯把接口地址集中在src/api目录下管理每个模块一个文件比如user.js、order.js。里面export一个个函数组件里只管调用不关心URL拼接和参数格式。这种做法的好处是当后端改了路径你只改一个文件而不是全局搜“/api/user”然后一个个替换。2.2 用环境变量区分开发和生产另一件在交互前就该做的事是配置环境变量。因为开发环境你本地起的是webpack-dev-server或者Vite Dev Server后端在你本机的某个端口等部署上线接口地址又变成正式的域名。如果你把接口地址硬编码写在代码里每次打包上线都要改一遍基本属于自找麻烦。Vue CLI项目用.env.development和.env.production来区分Vite项目也一样。我一般这么配# .env.development VITE_API_BASE_URLhttp://localhost:8080/api# .env.production VITE_API_BASE_URLhttps://api.example.com/api然后在代码里取const baseURL import.meta.env.VITE_API_BASE_URL注意Vite项目里环境变量必须以VITE_开头才会暴露给前端代码Vue CLI里则是VUE_APP_开头。这个前缀规则经常有人踩坑——配了环境变量但页面里取到undefined十有八九是前缀不对。2.3 开发代理帮你瞒天过海解决了接口地址的问题接下来就是开发环境的跨域。你在本地localhost:5173跑Vue后端跑在localhost:8080直接发请求大概率会被浏览器拦下来报CORS错误。后端的同学如果没配跨域你前端这边再折腾也白搭。开发阶段最常见的解法是代理转发。Vite里配置server.proxy让前端开发服务器把/api开头的请求转发到后端的真实地址// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样你在代码里请求/api/user/listVite Dev Server会转发到http://localhost:8080/api/user/list对于浏览器来说你的请求是同源的不触发跨域限制。关于这个配置有两个细节值得你注意。第一个是changeOrigin它会改写请求头里的Host字段有些后端会校验这个字段如果不改就可能被后端拒绝。第二个是pathRewrite如果后端接口路径里没有/api这个前缀你需要把请求路径里的/api重写掉否则转发过去就变成404了。3. axios封装别让你的组件裸奔3.1 为什么要二次封装axiosaxios本身已经很好用了但如果你直接在每个组件里import axios然后axios.get(...)项目一大会出现几个问题。第一个是重复代码满天飞每个请求都要写baseURL、timeout、请求头改一处要改几十处。第二个是错误处理没法统一有的地方弹提示有的地方直接console.log用户看到的行为完全随机。第三个是你没法统一拦截请求和响应比如自动在请求头加Token、响应401时统一跳转登录页这些逻辑散落在各个组件里根本没法维护。所以我要表达的观点是axios这种底层HTTP库应该被封装成一个模块暴露出足够简单的接口给组件用。说句不好听的组件里不应该出现axios这个词它只知道“调用了一个api方法拿到Promise然后用.then或async/await处理结果”。3.2 我的标准封装长什么样我直接给你看我平时用的封装模板这个结构在Vue2、Vue3里都能跑// src/api/request.js import axios from axios import { ElMessage } from element-plus import router from /router import { getToken, removeToken } from /utils/auth const service axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 15000 }) // 请求拦截器 service.interceptors.request.use( config { const token getToken() if (token) { config.headers[Authorization] Bearer ${token} } return config }, error { return Promise.reject(error) } ) // 响应拦截器 service.interceptors.response.use( response { const res response.data // 假设后端统一返回 { code: 200, data: ..., message: ... } if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response error.response.status 401) { removeToken() router.push(/login) } else { ElMessage.error(error.message || 网络异常) } return Promise.reject(error) } ) export default service然后再做一个具体模块的接口文件// src/api/user.js import service from ./request export function getUserList(params) { return service({ url: /user/list, method: get, params }) } export function createUser(data) { return service({ url: /user/create, method: post, data }) }组件里调用的时候import { getUserList } from /api/user const list async () { try { const res await getUserList({ page: 1, size: 10 }) tableData.value res } catch (error) { console.log(请求失败) } }你仔细品一下这个结构组件不关心baseURL怎么来的不关心Token怎么加进去的不关心拿到数据之后外面包了几层。所有“脏活”都被封装在请求层里了这不只是代码整洁的问题更是项目可维护性的分水岭。3.3 响应数据到底该返回哪一层上面代码里有一个关键点很多人拿不准后端返回的结构统一是{ code, data, message }到底应该直接返回response本身还是返回res.data还是返回res.data.data我的经验是既然后端统一返回了code和data那么拦截器里就做一次解包把res.data返回给调用方。这样调用方拿到的直接就是业务数据它不需要关心HTTP协议层的东西。但是注意如果项目里有文件下载、Excel导出这类特殊接口它们返回的可能不是JSON而是blob这时候不能在拦截器里统一解包否则文件会损坏。处理办法可以是在响应拦截器里判断response.headers[content-type]如果是application/json就解包如果是二进制流就直接返回response。这是我刚开始写项目时踩过的坑导出Excel的功能上线后用户反馈下载的文件打不开查了很久发现是拦截器把blob当JSON解析了。从那以后我在封装axios时下载接口一律单独处理不走通用解包逻辑。4. 前端工程化里的“跨域”到底是什么鬼4.1 浏览器的一个安全策略别跟后端同学吵架先跟你说清楚一个概念跨域CORS错误提示的主体是浏览器不是后端拒绝了你也不是前端代码写错了。浏览器的同源策略规定一个页面里发起的AJAX请求只能访问“同源”的地址。什么算同源协议、域名、端口三个都相同才算同源。localhost:5173访问localhost:8080端口不同所以跨域。这个策略本质上是为了安全防止恶意网站偷偷调用你登录过的银行的接口。但对正常开发来说它就有点烦人——前后端分离的项目开发阶段几乎必然跨域。开发环境可以用代理解决前面已经讲过了。生产环境一般有两种方案要么让后端在网关或Nginx层配置CORS响应头要么前端和后端部署在同一个域名下用Nginx把/api路径转发到后端服务。第二种方案更常见部署上更干净不暴露后端的真实地址。4.2 生产环境的Nginx配置长什么样这里给一个最简单的Nginx配置片段前后端分离部署时用server { listen 80; server_name www.example.com; # 前端静态资源 location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; # 这行很关键Vue路由history模式需要 } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }我在这里明确告诉你try_files $uri $uri/ /index.html这行是给Vue Router的history模式用的少了它你刷新一个二级路由页面就会404。这个问题在Vue和后端交互的排障中经常被忽略——你接口通了页面也能访问但一刷新指定路由就白屏多半是Nginx没配置try_files。4.3 后端CORS配置也是绕不开的如果后端允许跨域访问它需要在响应头里加上Access-Control-Allow-Origin等字段。很多前端同学认为这是后端的事和自己没关系但在实际项目中如果后端不配合你前端再折腾也没用。这里有个很现实的问题如果后端是Java Spring Boot项目他们配置CORS的方式跟Node、Python Django完全不一样。你不需要背每一家的配置但至少要能看懂后端返回的响应头里有没有Access-Control-Allow-Origin以及浏览器控制台报CORS错误时能读懂是“响应头缺失”还是“预检请求失败”。如果你能清楚地告诉后端“你的接口缺什么响应头”配合起来顺畅得多。预检请求OPTIONS Preflight是另一个常见盲区。当你的请求不是简单请求比如带了Authorization请求头或者Content-Type是application/json浏览器会先发一个OPTIONS请求用来探测服务器是否允许实际请求。有些后端只处理了POST/GET没处理OPTIONS结果就是你用postman调接口没问题浏览器里一调就跨域报错。这类问题排查时打开浏览器开发者工具的Network面板如果看到OPTIONS请求返回401或404基本就是后端没处理预检。5. 登录态是怎么回事Token的前世今生5.1 Session还是JWT这个选择会影响你写代码Vue与后端交互里最绕不开的一个点是登录态管理。最早的传统方案是Session-Cookie用户登录后后端在服务器存一份Session同时下发一个Cookie给浏览器后续请求浏览器自动带上Cookie后端校验Session。这个方案在前后端不分离的时代非常好用但在前后端分离、多端复用App、小程序、PC的场景下Cookie的适配性就变差了。更主流的前后端分离方案是Token尤其JWT。流程大致是用户登录拿用户名密码换Token前端把Token存到localStorage或pinia里每次请求在拦截器里带上Authorization: Bearer token后端解析Token判断身份。因为JWT本身是自包含的后端不需要存Session天然适合分布式部署。我在项目里一般用localStoragepinia双写pinia存当前用户信息和Token用于内存状态localStorage存一份防止页面刷新后pinia重置导致Token丢失。刷新页面时在入口处判断localStorage有没有Token有就重新拉取用户信息、恢复登录态。5.2 Token过期了怎么办别让用户顿顿重新登录JWT是有过期时间的一般15分钟到2小时不等。过期之后前端再发请求后端会返回401。如果你只是简单地把用户踢回登录页体验会非常糟糕——用户填到一半的表单没了页面切了一下就要重新登录。成熟一点的做法是用双Token机制access_token短期比如15分钟和refresh_token长期比如7天。access_token过期后前端用refresh_token去换新的access_token这个过程对用户无感。简单实现思路是在响应拦截器里遇到401时不立刻跳登录而是先把请求“挂起”拿refresh_token换新Token换成功了重新发起原来的请求换失败了才跳登录页。伪代码大概这样// 用isRefreshing标记是否正在刷新Token let isRefreshing false let pendingQueue [] service.interceptors.response.use( response { /* ... */ }, error { const { config, response } error if (response.headers[x-token-expired] true !config._retry) { // 说明是Token过期 config._retry true if (!isRefreshing) { isRefreshing true return refreshToken().then(newToken { isRefreshing false return newToken }).then(token { pendingQueue.forEach(cb cb(token)) pendingQueue [] config.headers[Authorization] Bearer ${token} return service(config) }) } else { // 正在刷新Token期间把后续请求加入队列 return new Promise(resolve { pendingQueue.push(token { config.headers[Authorization] Bearer ${token} resolve(service(config)) }) }) } } // 真正失败 removeToken() router.push(/login) return Promise.reject(error) } )这里有一个坑多个请求同时遇到401如果你不去重就会发多个refreshToken请求后端可能因为refreshToken被重复使用而失效。所以我才用isRefreshing标记和pendingQueue队列保证同一时刻只有一个刷新Token的请求在跑其他请求排队等新Token。5.3 接口权限控制不只是隐藏按钮那么简单接入了Token之后你会遇到“按钮权限”“路由权限”这些东西。前端根据用户角色去隐藏某些页面和按钮这叫前端控制但它只是体验层面的优化。真正的权限必须由后端在每个接口上校验——前端隐藏按钮用户打开浏览器按F12直接调接口一样能访问到数据。所以权限体系的设计原则是前端管体验后端管安全。路由权限的常见实现是在路由守卫里判断用户有没有登录登录了再判断角色能不能访问当前路由。这块代码写在router.beforeEach里不要写在组件里保证任何路由跳转都会被统一拦截。router.beforeEach((to, from, next) { const token getToken() if (to.meta.requiresAuth !token) { next(/login) return } // 动态加权限路由逻辑省略... next() })6. 请求并发、取消与进度三个实操细节6.1 并发请求别手写Promise.all来硬抗有时候一个页面要同时请求两三个接口比如个人中心要同时拿用户信息、订单列表、消息数量。新手会在组件里写Promise.all([getUserInfo(), getOrders(), getMessages()])这个写法本身没有错但是当这三个接口都依赖同一个登录态、且可能在多个页面复用时你更应该在API层把它们组合成一个“合并接口”。合并接口是后端提供的一个聚合接口一次性返回你需要的所有数据前端只发一次请求。这个对用户体验和服务器压力都有好处。如果没有合并接口你在前端用Promise.all也能接受但要注意错误处理Promise.all只要有一个失败就整体失败如果希望单个失败不影响其他可以用Promise.allSettled。6.2 组件卸载了响应就不要回来更新了这是很多人忽视的问题用户在列表页发了个请求趁等待间隙点了返回按钮组件已经卸载了结果请求完成后回调里还去操作DOM或者给一个已销毁的响应式对象赋值控制台就会报Cannot read property of null。解决思路有几种用AbortController取消请求、在组件的onUnmounted里标记一个取消状态、或者用一个专门的Vue3的工具函数来判断组件是否仍然活跃。实际操作中如果是简单的列表页我建议用AbortController走正规取消import { onUnmounted } from vue let abortController new AbortController() const fetchData () { abortController.abort() abortController new AbortController() // 你和axios的集成方式 // axios.get(/api/user/list, { signal: abortController.signal }) } onUnmounted(() { abortController.abort() })注意axios从1.x版本开始取消请求的推荐方式已经从CancelToken换成了AbortSignal。如果你还在用new axios.CancelToken升级axios后可能要改代码。6.3 文件上传别只顾着发POST文件上传也是前后端交互里的高频场景。如果你的后端没有做特殊处理前端直接FormData加axios.post就能实现const formData new FormData() formData.append(file, file) formData.append(type, avatar) service.post(/upload, formData, { headers: { Content-Type: multipart/form-data }, onUploadProgress: (e) { if (e.total) { const percent Math.round((e.loaded / e.total) * 100) progress.value percent } } })这里有个性能点上传大文件时给onUploadProgress回调里更新进度条的逻辑加个节流否则每传一个字节就触发一次性能会很差。另外multipart/form-data的Content-Type其实不需要你手写浏览器会自动加上boundary你手动指定反而容易出错。上面这段代码里我写了是因为项目里有过特殊情况但正常情况下你可以不写让axios替你处理。7. 常见问题速查表与排障思路7.1 一张表看清楚高频坑位现象可能原因排查方向请求发出去了返回404后端路径不对 or 代理没命中看Network面板里请求的完整URL确认代理是否正确转发返回500后端崩溃或代码报错看后端日志别在前端死磕返回CORS错误后端没配响应头 or 预检请求失败看Response Headers里有没有Access-Control-Allow-Origin刷新页面路由404Nginx没配置try_files加try_files $uri $uri/ /index.html请求数据拿到了页面不显示数据层级解包错误确认拦截器返回的是res.data还是整个responseToken过期后一直重复请求未处理刷新Token并发用isRefreshing去重本地能访问打包后接口不通环境变量没配生产地址检查.env.production是否设置正确这张表不能覆盖所有情况但覆盖了我这些年被问得最多的问题。前端排障的思路永远是先定位再解决打开NetWork面板看请求有没有发出去、状态码是什么、响应体是什么、如果状态码是200但页面不对那就是数据解析或渲染的问题跟网络无关了。7.2 我常用的排障三步法第一步在浏览器开发者工具Network里看请求的全貌URL是什么、请求头有哪些、响应体长什么样。这一部能过滤掉90%的“我觉得应该没问题”的误判。第二步复制请求URL用postman或者直接curl跑一次。如果postman能通而浏览器不行基本是跨域或浏览器环境问题如果postman也不通问题在后端你先把截图甩给后端同事比干等强。第三步如果请求和响应都正常但页面没反应那问题就在前端代码逻辑拦截器是不是解包解错了返回的数据结构和你预期是不是不一致Vue的响应式数据有没有正确赋值这一步多打console.log比什么都好使。8. 实战收个尾一个小而美的请求层设计最后我给你一套可以“抄作业”的目录结构这个结构我用了很多个项目中小型项目完全够用src/ ├── api/ │ ├── request.js # axios实例 拦截器 │ ├── user.js # 用户相关接口 │ ├── order.js # 订单相关接口 │ └── upload.js # 上传相关接口 ├── utils/ │ ├── auth.js # getToken / setToken / removeToken │ └── storage.js # localStorage封装 ├── store/ │ └── user.js # Pinia用户状态 └── router/ └── index.js # 路由守卫很多培训机构的demo项目只有一两个接口用不上这么完整的结构。但如果你要做的项目会持续迭代两个月以上我劝你就按这个结构搭前期多花半小时后期能帮你每天省出一小时的排障时间。老人常说的“设计模式是为了对抗变化”请求层也是一样后端改一个路径、换一套Token策略、加了新的鉴权方式你都只需要动一个文件。拿我自己的经验来说只要把一个请求层设计好了后续加接口、加鉴权、加拦截都是往里填代码的事不会再出现“动一个地方炸一片”的情况。Vue与后端交互这件事说到底不是技术难点而是工程习惯。你把请求收口、把错误理顺、把状态管好剩下的就都是业务逻辑的堆砌了。最后再分享一个实操小技巧在request.js里除了业务请求我还会写一个downloadFile(url, params)的统一方法专门处理blob流文件下载。因为拦截器会把响应解包下载接口必须走另一条路我在这个函数里直接用axios原实例实现避免和通用拦截器打架。你可以试试这个做法多一个函数省掉好多次“文件下载下来是乱码”的麻烦。
返回列表