Vue 3 + Vite 前端调用 WebApi 接口实战:从请求到页面渲染
最近有个朋友问我说自己在后端把接口都写好了但前端一直不知道怎么下手问我能不能写个最简单的例子用Vue把一个WebApi接口的数据拉下来放到页面上。这一问我倒是想起来了我刚开始做前后端分离的时候也是卡在这一步卡了很久——前后端单据、接口文档都齐全但就是不知道Vue项目里那几行请求代码往哪放、怎么写、数据怎么才能从接口流到页面上。所以今天这篇就干脆把这个过程从头到尾捋一遍以一个“用户列表”的WebApi接口为例手把手在Vue项目里实现数据拉取和页面展示。看完你就能明白Vue项目怎么建、接口请求怎么发、数据拿到了怎么放、页面怎么写才能把数据渲染出来以及最烦人的跨域问题怎么解决。这篇文章适合刚学完Vue基础语法、但没实际做过接口联调的前端新手也适合后端同学想了解前端怎么接你写的接口。1. 整体思路拆解一次完整的接口联调到底在调什么1.1 前后端分离到底是怎么个“分离”法在开始写代码之前咱们先把整个通信模型看清楚。前端的Vue项目跑在浏览器里后端的WebApi跑在一个服务器或者本地某个端口上两边是两个独立的进程。前端要拿到数据就得让浏览器向后端发起一次HTTP请求后端处理完把数据通过JSON格式返回前端再把JSON数据变成页面上能看见的表格、列表、卡片。这个流程听起来简单但新手最容易懵的地方在于写Vue组件的时候界面渲染和网络请求是两套完全不同的东西。界面的数据来自组件的变量data而网络请求是异步的你发出请求之后不能立刻拿到结果必须等后端响应回来再把返回值塞进变量里页面才会更新。这一整篇文章要做的其实就是一件事把接口返回的数据想办法塞进Vue组件的变量里。只要能理解这个核心目标后面的代码就都顺理成章了。1.2 我们需要哪些角色和技术栈这个例子里我会用到这几样东西Vue 3用Vite构建这是目前最主流的Vue项目搭建方式axios前端发HTTP请求的库比原生fetch更好用一个后端WebApi接口我用最简单的Node.js Express来模拟这样不需要额外装数据库把数据直接写在内存里就行Vite的代理配置解决开发环境的跨域问题如果你后端是Java的Spring Boot或者是C#的ASP.NET Core WebApi都没关系——接口返回的是JSON前端发起的是HTTP请求语言不影响联调逻辑你只需要把请求地址换成真实后端地址就行。1.3 我为什么用Vue 3 Vite而不是Vue 2现在网上还有大量教程在讲vue-cli那是因为写教程的时候Vue 2还是主流。但到今天Vue 3已经非常成熟了Vite的冷启动速度和热更新体验比webpack好一个量级。Vue 2在2023年12月31日已经正式停止维护新项目再学Vue 2是真的没必要。Vite的另一个好处是配置文件极其简单尤其是配代理这块Vue 2时代用webpack写proxy配置那叫一个折腾Vite这边几行就搞定了。做完这个例子之后你再看Vite项目里的vite.config.js会对这套机制有很直观的感受。2. 环境准备把Vue项目先跑起来2.1 Node.js环境检查Vue项目依赖Node.js环境这是绕不开的第一步。打开终端先执行node -v npm -v如果提示找不到命令说明电脑上还没装Node.js。直接去Node.js官网下载LTS版本长期支持版一路下一步安装完重新打开终端再跑一次上面的命令能看到版本号就说明装好了。Node.js版本建议14.18以上因为Vite 4以上的版本对Node版本有要求。我建议直接装最新的LTS比如18或20省得后面各种依赖装不上。2.2 用Vite创建Vue 3项目这一步在终端执行npm create vuelatest注意这里我用的不是npm create vite而是Vue官方提供的脚手架create-vue。它会帮你把Vue项目的基础结构、路由、状态管理如果选了都搭好省去一堆手动配置。执行之后它会问你几个交互式问题比如是否安装路由、Pinia、是否需要ESLint这些东西对新手来说先都选No等理解了再加也不迟。项目创建成功后cd vue-webapi-demo npm install npm run dev浏览器里打开终端提示的地址默认是http://localhost:5173看到Vue的欢迎页面项目就通了。2.3 项目结构怎么看新手刚创建完项目面对一堆文件会懵。你只需要关心这几个src/main.js整个应用的入口new Vue实例的对象带挂载src/App.vue根组件最底层的页面框架src/components/存放自定义组件src/views/存放页面级组件创建项目时如果选了Vue Router就会自动生成vite.config.jsVite配置待会儿改代理就在这改我们这次把代码主要写在App.vue或者src/views/HomeView.vue里先用一个单组件搞定整个例子不需要拆太细。3. 后端WebApi接口这边怎么准备3.1 没有真实后端的时候怎么先自测很多前端同学在学习的时候根本没有可用的后端接口或者后端文档对不上。这里我提供一个最简单可行的方案用Node.js的Express写一个临时接口。不需要数据库把数据硬编码在内存里先保证前端联调链路是通的。我的做法是单独建一个文件夹初始化一个最简单的后端项目mkdir fake-backend cd fake-backend npm init -y npm install express cors然后新建一个server.jsconst express require(express); const cors require(cors); const app express(); const port 3000; app.use(cors()); // 模拟用户数据 const users [ { id: 1, name: 张三, age: 25, email: zhangsanexample.com }, { id: 2, name: 李四, age: 30, email: lisiexample.com }, { id: 3, name: 王五, age: 28, email: wangwuexample.com } ]; // 用户列表接口 app.get(/api/users, (req, res) { res.json({ code: 200, message: success, data: users }); }); app.listen(port, () { console.log(接口已启动: http://localhost:${port}/api/users); });启动后端node server.js浏览器访问http://localhost:3000/api/users能看到JSON数据就说明接口通了。3.2 接口返回格式设计的讲究上面这个接口我故意把返回格式设计成了{ code: 200, message: success, data: [...] }没直接返回一个数组这是有原因的。实际开发中后端接口往往还需要返回错误提示、分页信息、业务状态码等如果直接返回数组前端一旦需要判断“有没有数据”“出没出错”就很被动。正常的接口约定应该是这样code业务状态码200表示成功message描述信息出错时告诉前端原因data真正的数据可以是对象、数组、也可以为空前端拿到这个结构之后先判断code是否为200再取数据这样逻辑是清晰的。如果你和同事对接接口建议也约好这么一套结构后面大家省心。3.3 接口联调前要确认的三件事拿到后端接口文档之后先别急着写代码先把三件事问清楚请求方式是什么GET还是POST路径是/api/users还是/api/getUsers参数怎么传查询参数query、路径参数path、请求体body还是请求头header返回数据格式是什么JSON还是XML字段名大小写是什么很多联调翻车都翻在“我以为”上。比如后端返回的字段名是user_name你前端写的是username页面能渲染出个鬼。所以第一步一定是以接口文档为准别想当然。4. 在Vue里对接接口核心代码实操4.1 用axios还是fetchVue里发请求有两个常见选择axios和原生fetch。fetch是浏览器自带的不需要安装依赖但用起来比较繁琐尤其做错误处理、请求拦截的时候。axios是一套封装好的库用起来简洁还自动帮你在浏览器里处理JSON转换问题所以它成了Vue生态里最常用的请求库。我推荐直接用axios理由就是省心。安装npm install axios4.2 一个简单但够用的axios请求写法在src/views/HomeView.vue或者src/App.vue里我把逻辑写完整template div classuser-list h1用户列表/h1 !-- 加载中提示 -- p v-ifloading正在加载数据.../p !-- 错误提示 -- p v-else-iferror classerror{{ error }}/p !-- 数据列表 -- table v-else thead tr thID/th th姓名/th th年龄/th th邮箱/th /tr /thead tbody tr v-foruser in users :keyuser.id td{{ user.id }}/td td{{ user.name }}/td td{{ user.age }}/td td{{ user.email }}/td /tr /tbody /table /div /template script setup import { ref, onMounted } from vue; import axios from axios; const users ref([]); // 用户列表数据 const loading ref(true); // 是否加载中 const error ref(); // 错误信息 // 获取用户列表 const fetchUsers async () { loading.value true; error.value ; try { const response await axios.get(http://localhost:3000/api/users); // 前面约定过的返回格式{ code, message, data } if (response.data.code 200) { users.value response.data.data; } else { error.value response.data.message || 接口返回异常; } } catch (err) { error.value 请求失败 err.message; } finally { loading.value false; } }; // 页面初始化时自动执行 onMounted(() { fetchUsers(); }); /script这段代码的核心思路是四步定义响应式变量users、loading、error用axios.get发请求等接口返回后把数据赋值给users页面通过v-for遍历users并且渲染表格4.3 为什么是onMounted而不是createdVue组件从创建到挂载要经历一系列生命周期其中created在实例创建后立刻执行onMounted在组件挂载到DOM之后执行。对于请求数据这件事建议放在onMounted里。原因很简单onMounted执行的时候页面已经渲染完成此时更新数据不会导致模板重新渲染时出现“数据还没准备好”的闪烁问题而且DOM访问也更安全。如果放在created里理论上也可以因为数据请求本身不依赖DOM但为了规范和可维护性推荐统一用onMounted。你打开页面的时候会先看到“正在加载数据...”等接口返回后就变成表格。这个交互是用户感知最自然的。4.4 ref和reactive到底用哪个Vue 3的响应式API有两个很相似的玩意儿ref和reactive。ref用来声明一个值类型字符串、数字、布尔值或者需要整体替换的对象访问时写作users.valuereactive用来声明一个嵌套对象访问时直接写users.xxx不需要.value我这个例子里用ref是因为从接口拿回来的数据是一个数组我后面需要整组替换它ref直接users.value ...非常爽。用reactive的话整体替换数组反而要注意Object.assign之类的操作麻烦很多。新手建议记住一条大多数场景直接用ref简单直观不用纠结什么时候该用reactive。4.5 手动测试确认数据渲染成功代码写完后页面会自动刷新如果一切正常你会看到表格里出现张三、李四、王五三行数据。如果你在浏览器里没看到数据按F12打开开发者工具切到Network网络面板刷新一下页面找/api/users这个请求请求状态是不是200响应内容是不是正确的JSON有没有报跨域错误这一步很重要它能帮你判断问题出在前端代码还是后端接口。5. 前后端分离绕不过去的坎跨域问题5.1 用最简单的类比理解跨域跨域这个话题我第一次接触的时候也是头大。用生活类比来说明浏览器像一个安保很严的小区小区住户前端页面住在localhost:5173这栋楼后端口服务器WebApi住在localhost:3000这栋楼。小区物业规定一栋楼的住户想访问另一栋楼的资源必须要有“通行证”CORS响应头否则安保直接拦截。问题在于开发阶段前端跑在5173端口后端跑在3000端口两者端口不一致浏览器就会判定这是跨域直接把请求结果拦下来。你明明看到接口是有返回数据的但浏览器里的Vue就是拿不到就是这个原因。5.2 开发环境最实用的解法Vite代理开发环境下最优雅的办法不是后端开CORS而是通过Vite配置代理让浏览器以为请求是发给自己的前端服务器再由前端服务器转发给后端。打开vite.config.jsimport { fileURLToPath, URL } from node:url; import { defineConfig } from vite; import vue from vitejs/plugin-vue; export default defineConfig({ plugins: [vue()], resolve: { alias: { : fileURLToPath(new URL(./src, import.meta.url)) } }, server: { port: 5173, proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } } });配置完之后前端请求代码改成const response await axios.get(/api/users);注意请求地址从http://localhost:3000/api/users变成了/api/users。这样浏览器发请求的目标变成了http://localhost:5173/api/users同源不会跨域Vite dev server收到这个请求后发现以/api开头自动转发到http://localhost:3000/api/users拿到结果再返回给浏览器。配置完需要重启一下npm run dev才能生效这是很多新手踩过的坑。5.3 生产环境怎么办后端CORS开发环境的代理在生产环境不生效因为生产环境你的前端代码是打包成静态文件部署的没有Vite dev server帮你转发。所以生产环境要么用Nginx做反向代理Nginx把/api请求转发到后端要么后端开启CORS。当初我用Express模拟后端的时候已经加了cors()中间件app.use(cors());Spring Boot后端可以加Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOrigin(*); config.addAllowedMethod(*); config.addAllowedHeader(*); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }我的建议是开发环境用Vite代理生产环境用Nginx反向代理后端CORS作为兜底方案。既不要在开发环境依赖后端CORS也不要只靠前端代理打天下两边配合才最稳。5.4 我踩过的跨域坑第一次做前后端分离项目的时候我在前端折腾了半天代理结果发现后端代码里也写了CORS配置两边配置交叉作用就差没把浏览器搞出精神分裂。后来我总结出的经验是前端代理和后端CORS不要同时开同一套接口的前后配置容易产生“前端代理转发一次、后端CORS放行一次、浏览器搞不清楚”的奇怪现象排查跨域问题的时候先看Network面板里失败的请求浏览器会明确告诉你“CORS policy”错误按提示对症下药如果你把前端部署在Nginx上优先用Nginx代理这是最接近生产实践的方案配置也简单6. 常见问题与排查技巧实录6.1 接口请求404如果Network面板里看到请求状态是404先确认后端接口地址是不是对的。这里有一个容易忽略的点启动后端的时候Express输出的地址是http://localhost:3000/api/users但你前端如果是用Vite代理访问那么你写的应该是/api/users而不是带上了http://localhost:3000的全称。如果你已经写了全称地址、但网络面板还是显示了http://localhost:5173/api/users说明代理没配成功检查vite.config.js的修改是否保存、以及是否重启了npm run dev。还有一种情况是后端根本没启动或者启动的端口不是3000先去终端确认node server.js有没有报错再访问一次接口地址看能不能打开。6.2 数据返回了但页面空白这个问题出现频率极高后台确认接口返回了JSON浏览器也能看到响应体但页面就是什么都不渲染。常见原因一字段名对不上。后端返回user_name你模板里写{{ user.username }}当然空白。打开响应体一个个对照字段。常见原因二数据结构层级不对。比如后端返回的data是一个对象{ list: [...] }你直接users.value response.data.data赋了一个对象然后在模板里v-for遍历一个对象浏览器可能不报错但渲染不出来。最好console.log打印一下response.data看数据结构再写取值逻辑。常见原因三请求确实成功了users里也有值了但视图没更新。这种情况在Vue 3的ref里比较少见因为ref的响应式追踪是很灵敏的。但如果你用reactive并且直接整体替换了数组就有可能出现视图不更新的情况所以在前面的建议里我一直推荐ref。6.3 打包之后请求地址不对本地开发一切正常npm run build之后部署到服务器结果页面能打开数据请求全404。原因很可能是你开发环境用的代理只对dev server生效打包后的静态文件发请求还是按你代码里的路径来的。如果你写的是相对路径/api/users那么它会请求部署服务器的/api/users如果你写的是全称http://localhost:3000/api/users打包后依然会请求这个地址那当然只有你本机后端还开着才会通。打包后的解决方案我一般推荐两种Nginx配置反向代理把/api路径转发到真正的后端地址使用环境变量管理接口地址区分开发和生产环境第一种是生产环境最常用的。第二种适合在不同环境部署同一套前端代码的情况用.env.development和.env.production分别配置VITE_API_BASE_URL代码里读取这个变量拼接请求地址。6.4 请求状态一直pending最后超时接口地址没问题、页面代码没问题但请求一直pending过一会儿报超时。这种情况先别怀疑前端多半是后端没有启动或者后端启动后卡在某个环节比如等待数据库连接根本没有返回响应。你可以在终端确认后端启动日志也可以用Postman或者浏览器直接访问后端地址看接口单独访问是不是正常的。如果单独访问也超时那问题一定在后端不是前端代码。6.5 一个完整的兜底排查清单这一步是精华我把自己日常排接口问题的顺序整理成了清单你照着查基本能解决80%的问题排查步骤检查内容常见问题1后端接口能不能独立访问后端没启动、端口不对、路由写错2请求地址和方式GET/POST写错、URL拼错、query参数没传3Network面板里的请求状态404、500、跨域报错分别处理4响应里的数据结构字段名大小写、嵌套层级、JSON格式5控制台有没有JavaScript报错模板里调用了不存在的方法或属性6数据赋值后视图是否更新ref/reactive用错、overwrite整体赋值排查的时候记住一个原则从网络层到数据层再到视图层一层层往下追不要一上来就怀疑模板写错。7. 把代码做点小优化加载状态、错误提示和封装7.1 为什么不能只有成功路径我见过很多新手写的接口请求代码是“裸奔”的——只写了成功请求没有loading没有错误处理接口一挂页面直接空白。这在真实的项目里是不可接受的。我的习惯是每个接口请求都要配套状态管理加载中、成功、失败三种状态分别渲染不同内容。前端请求本来就是不可靠的网络抖动、后端报错、参数错误都可能发生至少要在页面上让用户知道发生了什么而不是默默失败。在4.2节代码里我已经展示了完整的loading、error处理。实际项目中这个模式可以复用很多次。7.2 axios封装给每个请求加同样的配置页面多了之后每个组件里都写一遍axios.get也够烦的而且不利于统一维护接口地址和token。一个最基础的request.js封装长这样// src/utils/request.js import axios from axios; const request axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || /api, timeout: 10000 }); // 请求拦截器每发起一个请求自动携带token request.interceptors.request.use( (config) { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }, (error) Promise.reject(error) ); // 响应拦截器统一处理业务错误码 request.interceptors.response.use( (response) { const res response.data; if (res.code ! 200) { alert(res.message || 接口错误); return Promise.reject(new Error(res.message)); } return res; }, (error) { alert(网络异常请稍后重试); return Promise.reject(error); } ); export default request;封装之后页面里请求就清爽多了import request from /utils/request; const fetchUsers async () { loading.value true; try { const res await request.get(/users); users.value res.data; } catch (err) { error.value err.message; } finally { loading.value false; } };注意baseURL用了import.meta.env.VITE_API_BASE_URL这是Vite的环境变量机制。你在项目根目录新建.env.developmentVITE_API_BASE_URL/api再建一个.env.productionVITE_API_BASE_URLhttps://api.example.com这样开发环境走Vite代理、生产环境走真实域名代码不用改构建的时候自动读对应用环境变量。7.3 这个例子的下一步扩展方向当你把这个用户列表跑通之后可以顺着这个思路继续折腾添加一个详情页点击表格行跳到对应用户的详情用Vue Router传参把“新增/编辑/删除”操作也接上体会POST、PUT、DELETE请求的写法给表格加分页后端返回{ total, list }结构前端做页码切换把这个列表封装成公共组件数据通过props传入实现组件复用这些都基于今天这套“发请求、存数据、渲染页面”的基础逻辑只是业务变复杂了核心不会变。8. 最后分享一点我个人的体会接口联调做得多了以后你会发现真正难的从来不是语法而是调试的思路。以前我写代码遇到数据没出来第一反应是去翻模板代码后来被逼着养成了一个习惯先开浏览器开发者工具看Network面板里接口到底通没通再看响应体里的数据长什么样最后才回头看自己的代码逻辑。这个顺序一旦定下来排查问题快得不是一星半点。另外还要多说一句前后端联调是两个人的事接口文档约定得越细后面吵架越少。返回码、字段命名、错误格式这些在一开始就约定好后面大家都能省很多事。后端同学如果方便的话尽量把项目跑起来让前端直连测试或者提供一个可访问的测试环境你一个人闷头写完接口直接丢给前端对方真的会头大。这套流程我前前后后带过好几个新人只要照着走一遍基本就能独立负责一个模块的联调了。现在趁着你后端接口在手赶紧把代码敲一遍遇到问题的时候再回头看看这篇文章的排查清单应该就能自己解决大部分问题了。