news 2026/10/18 6:08:47

Vue3+TypeScript实战:从零掌握Axios请求与拦截器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue3+TypeScript实战:从零掌握Axios请求与拦截器

前端新手问得最多的一个问题,十有八九是“请求到底怎么发”。我通常给出的答案里,都有一个固定成员:Axios。它不是黑科技,只是一个基于 Promise 的轻量 HTTP 客户端,但在Vue3 + TypeScript项目里,它几乎成了默认配置。这篇文章会从安装开始,带你发出自己的第一个请求,再逐个搞定 GET、POST、参数传递、拦截器和错误处理,最后给一个能直接落地改用的用户列表实战案例。目标是让刚接触前端请求的同学能在十分钟左右看完并跑起来,也让已经写了大量“散装请求代码”的人找到一点整理思路。

1. 先别急着敲命令:Axios到底帮你干了什么

很多新手都问过一个问题:“原生 fetch 也能发请求,为什么要学 Axios?”这句话本身没错,但你只要在真实项目里写过几个接口就会明白,业务请求从来不是“发出去”这么简单。

一次正常的请求背后,要处理 baseURL、超时时间、请求头、token、错误码、取消请求、重复点击、后端不同接口的拼接地址……如果每个页面都自己写一遍这些逻辑,代码会迅速膨胀成一团乱麻。Axios 把公共逻辑收敛到了一个统一的位置,让你在业务代码里只需要关注“要什么数据”和“拿到数据之后做什么”。

1.1 用一张表看懂 Axios 比 fetch 省事在哪

我用一张很朴素的表来对比,新手照着看会更直观:

需求原生 fetch 的写法Axios 的写法
解析 JSON 数据每次都要await res.json()响应对象默认带数据,直接访问response.data
设置超时需要手动配合AbortController做定时创建实例时写timeout: 10000即可
全局统一携带 token每个请求都要自己拼 header用请求拦截器统一加
后端返回 5xx默认不会进入 reject,你得自己判断响应状态Axios 默认将非 2xx 视为请求失败,能统一处理
取消请求手动构造 AbortController,逻辑分散同一个控制器也能传入 signal,代码更集中

这个表格不是要证明 fetch 一无是处。实际上浏览器都有 fetch,轻功能完全可用。但项目一旦上了规模,公共逻辑的一致性比“少引入一个库”重要得多。

1.2 Promise 看不懂没关系,记住一个生活比喻

Axios 发请求返回的是 Promise。很多新手一看到 Promise 就紧张,其实可以把它想象成“去餐厅点餐”:你下单之后不用一直站在柜台等,该干嘛干嘛,等厨师做完,服务员会喊你的号码。

代码里的await就是在等那个号码叫到你。写完await axios.get('/api/users'),程序就会暂停在这里等待结果,拿到响应后再继续往下走。对于新手来说,先把这个用法记熟,比死磕 Event Loop 更有实际意义。这也是我这篇教程里唯一需要你记住的异步基础。

2. 10分钟倒计时:初始化Vue3+TS项目并装好Axios

环境准备部分看起来无聊,但很多人的“10分钟教程”就栽在这里:项目没起来,后面全是空中楼阁。所以我尽量把每一步都写精确,你照着敲就行。

假设你已经有 Node 环境,打开终端:

npm create vite@latest demo-app -- --template vue-ts cd demo-app npm install npm install axios

npm create vite会生成一个带 TypeScript 基础的 Vue3 项目。如果你已经在用现成的 Vue3 项目,那只需要执行最后一句npm install axios,其他步骤可以跳过。

装完之后可以验证一下:

npm ls axios

能打印出版本号,就说明安装成功。此时package.json的 dependencies 里多了一行"axios": "^1.x.x"。

2.1 为什么要用 Vite 起步

Vite 是目前 Vue3 生态里最主流的开发服务器,启动快,配置简单,天然支持 TypeScript。我见过不少教程还在教 webpack 手工配置,这对新手来说负担太重。所以这系列就直接以 Vite 为基线,你的实际工作项目如果也是 Vite,那这套实践能无缝迁移。

2.2 开发环境跨域:在 Vite 配置文件里配代理

前端跑在http://localhost:5173,后端接口跑在http://localhost:3000。如果你直接发请求,页面控制台经常会有一条“跨域”报错。跨域是浏览器的一种安全策略,限制了不同源之间的请求读取。不要在代码里硬碰硬,我在本地开发时基本全靠 Vite 的 server.proxy 配置。

打开项目根目录的vite.config.ts,改成下面这样:

import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true } } } })

这段配置的意思是:当浏览器端请求以/api开头的地址时,Vite 的开发服务器会把它转发到http://localhost:3000。浏览器看到的始终是同一个开发服务器,所以不会触发跨域。

注意一个细节:改完vite.config.ts后,必须重启一次开发服务器,配置才会生效。我看到太多人改完配置不重启,然后花半小时找跨域问题,最后发现只是个低级失误。

3. 第一次请求:GET、POST、query和body的取舍

环境通了,接下来就可以真正发出一个请求。我先从最简单的 GET 开始,再讲 POST,最后把参数传法说清楚。

3.1 第一个 GET 请求:先看清 response 里面装了什么

在任意组件里写一个异步函数:

import axios from 'axios' async function getUsers() { const response = await axios.get('/api/users') console.log(response.data) }

很多新手第一次打印response,会以为自己拿到的就是后端返回的数据。其实不是。Axios 返回的是一个响应对象,里面包含data、status、statusText、headers、config这些字段。真正的接口数据在response.data里。

所以口诀很简单:发请求用 axios,取数据用 .data。

如果后端返回的是这样一个 JSON:

[ { "id": 1, "name": "张三" }, { "id": 2, "name": "李四" } ]

那response.data就是一个数组,可以直接在 Vue 页面里v-for遍历。

3.2 POST 请求:默认就是 JSON 格式

接下来是提交数据,最常见的是登录和三方创建操作:

import axios from 'axios' async function login() { const response = await axios.post('/api/login', { username: 'demo', password: '123456' }) console.log(response.data) }

Axios 在post的第二个参数传入对象时,会自动把对象序列化为 JSON,并设置Content-Type: application/json。所以只要后端文档里写着“接收 JSON”,你就放心用这个写法。

如果后端要求的是表单格式,直接用对象传参就不行了。此时可以改用URLSearchParams来构造表单数据:

const form = new URLSearchParams() form.append('username', 'demo') form.append('password', '123456') await axios.post('/api/login', form)

这种场景在文件上传、老项目接口里比较常见,新项目一般还是 JSON 为主。知道有这么个切换方式就够了。

3.3 query 参数和 body 参数不要混淆

有的接口需要带查询参数,比如分页。很多人会直接拼字符串:

axios.get('/api/users?page=1&size=20')

这种方式能跑,但参数一多就不好维护。更推荐用params配置:

axios.get('/api/users', { params: { page: 1, size: 20 } })

Axios 会帮你拼成/api/users?page=1&size=20。代码可读性好,而且参数是结构化对象,不容易拼错。

这里的关键区分是:GET 查询条件走params,POST 提交内容走请求体。前者出现在 URL 后面,后者出现在网络请求的 payload 里。对应到后端语言里,一个是 query string,一个是 body,不要混着来。

4. Vue3+TS实战案例:把用户列表从 mock 数据换成真实接口

基础语法讲完,我们来做一个真正能落地的实战改造。场景很简单:一个后台管理系统的用户列表页。

我在很多团队里看到的问题是,组件里直接写axios.get,页面一多,URL 又杂又乱,改一个后端地址都要全局搜索。所以实战案例里,我会把请求抽成一个独立的 API 层,让组件只负责 UI 和状态。

4.1 项目目录规划

先给你看一眼最终的目录结构:

src/ ├─ api/ │ ├─ http.ts │ └─ user.ts ├─ types/ │ └─ user.ts └─ views/ └─ UserList.vue

http.ts负责创建 Axios 实例和拦截器,user.ts负责用户相关的具体请求函数,types目录放类型定义,views里是页面组件。这样分层之后,组件不需要关心请求细节,API 层也不需要关心 UI 长什么样。

4.2 先写一个共享的 http 实例

创建src/api/http.ts:

import axios from 'axios' const http = axios.create({ baseURL: '/api', timeout: 10000 }) http.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.set('Authorization', `Bearer ${token}`) } return config }) http.interceptors.response.use( response => response, error => { const message = error.response?.data?.message || '请求失败,请稍后再试' console.error(message) return Promise.reject(error) } ) export default http

这里的baseURL配合前面 Vite 的代理配置,所有接口地址就不用写全路径,统一写/users这种相对路径即可。timeout表示 10 秒内没响应就超时。请求拦截器负责把 token 塞进 header,这样业务代码里就不用每个请求都手动带一遍。响应拦截器在请求失败时统一打印错误信息,避免每个页面写一遍重复代码。

如果你觉得 token 这个逻辑短期内用不上,可以先去掉,优先把基础结构跑通。这个拦截器是一个很标准的模板,后面需要时往往都是直接把这套搬过去改。

4.3 给用户数据补充类型定义

创建src/types/user.ts:

export interface User { id: number name: string email: string }

为什么用 TypeScript 还要单独定义类型?因为组件里拿到User[]之后,编辑器就能帮你自动补全user.name、user.email,写错字段也会立刻标红。这个体验在项目大了之后非常重要。

4.4 写用户模块的请求函数

创建src/api/user.ts:

import http from './http' import type { User } from '../types/user' export const fetchUserList = () => http.get<User[]>('/users')

这里用到了 Axios 的泛型参数。http.get<User[]>的意思是:请求返回的data字段类型是User[]。于是组件里解构出来的数据不会是一个any,而是有完整类型提示的数组。

如果你还不熟悉泛型,可以先把它理解为“给请求结果打标签”:告诉 TypeScript,这个接口回来的是 User 类型数组,请用这个类型帮我做校验。

4.5 在 Vue3 组件中接入请求

创建src/views/UserList.vue:

<script setup lang="ts"> import { onMounted, ref } from 'vue' import { fetchUserList } from '../api/user' import type { User } from '../types/user' const users = ref<User[]>([]) const loading = ref(false) const errorMessage = ref('') async function loadUsers() { loading.value = true errorMessage.value = '' try { const { data } = await fetchUserList() users.value = data } catch { errorMessage.value = '数据加载失败,请稍后重试' } finally { loading.value = false } } onMounted(loadUsers) </script> <template> <section> <button :disabled="loading" @click="loadUsers">刷新</button> <p v-if="loading">加载中</p> <p v-else-if="errorMessage">{{ errorMessage }}</p> <ul v-else> <li v-for="user in users" :key="user.id"> {{ user.name }} - {{ user.email }} </li> </ul> </section> </template>

我来拆解这个组件的几个关键点。

ref<User[]>([])把用户列表声明成响应式数组,初始值是空数组,类型是User[]。在模板里,v-for循环时会有明确的 item 类型提示,不会像 JS 原型那样拿到一堆隐式 any。

const { data } = await fetchUserList()这一行的data,因为泛型标注,会被推断为User[],不需要再手动as User[]强转。这也是新手最容易忽略的好处之一:类型安全不是给别人看的,是给自己省debug时间的。

loading和errorMessage属于页面 UI 状态。请求开始之前设为true,结束后在finally里恢复;出错时在catch里写错误提示。这算是前端页面请求的一种标准姿态,你可以直接抄。

4.6 跑起来后的效果

浏览器访问页面,应该能看到列表逐条渲染出来,点击“刷新”按钮会重新请求一次接口。控制台干净,没有跨域报错,TypeScript 也没有红波浪线。到这一步,你已经把 Axios 完整接入到 Vue3+TS 项目里了。

5. 十个新手九个会踩的坑:状态码、业务码、超时、取消和代理

基础案例跑通之后,真正考验项目的其实是各种边界情况。我这里挑五个高频坑展开讲,每一个都值得你记到自己的“前端防翻车清单”里。

5.1 HTTP 状态码 200 不代表业务成功

后端接口有一种很常见的设计:不管操作成功还是失败,HTTP 状态码都返回 200,但会在响应体里放一个业务码:

{ "code": 500, "message": "库存不足", "data": null }

这种情况下,Axios 默认不会进入catch,因为在它眼里 200 就是成功。如果你只是简单await然后取data,页面会显示一个空数据,但根本不知道业务层面已经失败了。

针对这种设计,要么在响应拦截器里统一判断业务码,要么在请求函数里手动检查。我比较推荐后者,逻辑更直接:

const response = await http.post<ApiResult>('/api/order', payload) if (response.data.code !== 0) { throw new Error(response.data.message) }

这里的ApiResult是后端约定的响应包装结构,字段一般就是code、message、data这三件套。你和后端同事沟通时要先确认,到底用 HTTP 状态码表达结果,还是用业务码表达结果。两套机制混着用,前端代码里全是补丁判断。

5.2 超时设置:别让用户一直转圈

在http.ts里设置的timeout: 10000,是指从请求发出开始,如果 10 秒内没有收到响应,Axios 就会抛错。它用起来很简单,但要注意:超时时间不是越长越好。设太久,用户卡住没反应;设太短,弱网环境反而误杀正常请求。普通后台管理系统设在 10 到 15 秒比较舒适,实时性要求高的场景可以单独再覆盖。

5.3 重复点击造成重复请求

按钮没有做加载状态保护时,用户快速点两下“刷新”,就会发出两次同样的请求。这既是资源浪费,也有可能造成页面上数据互相覆盖。

最直接的解决办法是在 UI 上禁用按钮,也就是实战案例里那个:disabled="loading"。但更稳妥的方案是在请求层支持取消:

let controller: AbortController | null = null async function loadUsers() { controller?.abort() controller = new AbortController() try { const { data } = await fetchUserList(controller.signal) users.value = data } catch (error) { if (axios.isCancel(error)) return errorMessage.value = '加载失败' } }

AbortController是浏览器原生的取消能力。把它的signal传给 Axios 请求,然后在发新请求之前调用controller.abort(),之前那个还没完成的请求就会被直接中断。abort引发的错误需要单独判断,否则每次切换路由时控制台都会刷一片红色错误信息,看着很慌,其实无害。

5.4 代理配置的常见死角

第一个死角是请求路径写了完整地址,比如axios.get('http://localhost:3000/api/users')。这么做浏览器会认为你在跨域请求,Vite 代理又因为路径没有/api前缀而无法接管。正确写法是让请求路径保持/api/users,其他交给代理。

第二个死角是忘记配changeOrigin: true。部分后端服务会对请求来源做域名校验,如果没有这个配置,服务端可能拒绝请求。我一般会默认打开这个选项,省得多想。

第三个死角是改了vite.config.ts没重启开发服务器。Vite 的配置文件不是热更新覆盖的,必须重启才能让新代理生效。每次改配置之后,我都习惯看一眼终端日志,如果能看到 Vite 自动重启信息,那才算真正生效。

5.5 后端返回的非 JSON 数据

请求一个接口时,如果后端返回的是纯文本、HTML 错误页或者网关报错,Axios 默认会尝试解析成 JSON,解析失败后会抛一个 “Unexpected token” 类型的错误。这种情况通常说明服务端没有正常返回数据,要优先检查接口地址、状态码和网关配置,而不是在浏览器里纠结 Axios 的解析逻辑。

6. 从“能跑”到“好用”:请求抽层的几条维护意见

案例已经跑通了,但你不可能以后每天都把它当成一个 Demo 看。当项目里有几十个接口时,请求层写的是否规范,会直接影响整个团队的开发效率。这里分享几条我在真实后台项目里沉淀下来的习惯,你可以按需吸收。

6.1 请求函数和组件分离,命名直接体现业务动作

不要在 Vue 组件里写一长串http.get('/users')。我习惯在api目录下建模块,并按业务动作命名函数:

export const fetchUserList = () => http.get<User[]>('/users') export const createUser = (data: UserPayload) => http.post<User>('/users', data) export const updateUser = (id: number, data: UserPayload) => http.put(`/users/${id}`, data) export const deleteUser = (id: number) => http.delete(`/users/${id}`)

组件里只出现await fetchUserList()、await updateUser(id, form),看起来就像在读一句话。后端如果改了接口地址,也只需要改api/user.ts一处,全项目自动生效。这个习惯对维护者极其友好。

6.2 和后端约定统一响应格式

我给项目里推荐过很多次下面的泛型结构,它能让 TypeScript 的类型约束贯穿整个请求链:

export interface ApiResult<T> { code: number message: string data: T } export const fetchUserList = () => http.get<ApiResult<User[]>>('/users')

拿到结果之后,业务代码再处理response.data.data。有人会觉得这层多出来的包裹很麻烦,但统一格式的好处是,你可以在一个地方处理所有业务码,也可以在组件里精确知道每一层数据的类型。如果团队正在从纯 JS 切换到 TS,这个约束建议越早定越好。

6.3 错误提示统一收口,别散落在页面里

页面里最常见的坏味道是这样的:每个组件都对错误状态做一套自己的alert,五六个页面就有五六个不同文案。我更建议在相关请求函数或拦截器里抛出一个标准化错误,组件只负责接收一个错误状态并决定是否展示。

当然,统一不等于一刀切。登录页面可能希望弱化报错细节,后台管理页面可能希望把完整的技术信息打到控制台。这都属于业务决策,但只要请求层收口了,后续改起来就非常简单。

6.4 给请求层留一个追踪入口

我在实际开发中发现,线上问题最难查的就是“用户点了某个按钮,但后端说没收到请求”。这种问题往往是网络层丢包、代理过期、重复点击等复杂原因叠加导致的。在发出请求的钩子里打印一条日志,带上请求方法和地址,排查问题时就能直接给后端同事一个可检索的 key,而不是双方各猜各的。

这段日志代码一般写在请求拦截器里,加上环境判断,只在本地开发时开启。虽然这逻辑只有几行,但它几乎是我每次搭请求层的必备动作,碰到的疑难问题越多,越觉得它有用。

Axios 在 Vue3 + TypeScript 项目里,属于那种“学会一次就能用很多年”的基础能力。我最后还想分享一个个人习惯:拿到任何一份接口文档,先不要急着写页面,先把api目录里的请求函数全部建好,再继续做 UI。这个顺序看起来绕了一点,但项目越到后面,越能体会到请求层稳定的价值。希望这篇教程不只是帮你发出第一个请求,更能让你往后维护代码时少被请求相关的问题折腾。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 6:07:59

Genkit Dart 1.0 发布:Flutter 原生 AI Agent 编排框架详解

这两年做 Flutter 的开发者想接大模型&#xff0c;应该都有过一种拧巴的感觉&#xff1a;在 Dart 里裸调 HTTP 拼 JSON&#xff0c;请求倒是发出去了&#xff0c;但模型返回的文本怎么清洗、怎么重试、怎么让模型自己决定要不要“调用工具”&#xff0c;全得自己造轮子。社区里…

作者头像 李华
网站建设 2026/10/18 6:06:06

【STM32C5教程】(9)DAC 模拟量信号输出实验

欢迎关注 youcansxidian【嵌入式软件AI编程】专栏 【STM32C5教程】&#xff08;4&#xff09;GPIO 外部中断实验 【STM32C5教程】&#xff08;5&#xff09;定时器周期中断实验 【STM32C5教程】&#xff08;6&#xff09;定时器 PWM 输出实验 【STM32C5教程】&#xff08;7&…

作者头像 李华
网站建设 2026/10/18 6:05:19

page_alloc free_reserved_area

free_reserved_area() 是内核启动后期用于将保留内存&#xff08;reserved memory&#xff09;释放回伙伴系统的核心函数。它通常服务于 free_initmem()、free_initrd_mem() 等场景&#xff0c;把初始化代码段、initrd 等不再需要的物理内存归还给分配器。核心作用与设计意图它…

作者头像 李华
网站建设 2026/10/18 6:03:20

Matlab自动驾驶决策规划控制实战:从状态机到纯跟踪完整代码解析

自动驾驶决策规划控制Matlab代码实战&#xff0c;听起来又长又劝退&#xff0c;但我实际跑通之后发现&#xff0c;这其实是进入自动驾驶领域性价比最高的一条路。最近不少朋友来问我怎么入门&#xff0c;我的建议是别一上来就啃C、Linux那一套&#xff0c;先用Matlab把决策、规…

作者头像 李华
网站建设 2026/10/18 6:02:49

Playwright Trace Viewer:自动化测试的完整证据链

前两天帮一位校招阶段的朋友复盘自动化测试项目&#xff0c;代码写得挺干净&#xff0c;断言也覆盖了核心路径&#xff0c;但面试的时候被问住了一句话&#xff1a;跑挂了之后&#xff0c;你怎么证明不是环境问题&#xff1f;他想了想说&#xff0c;有截图。截图当然有&#xf…

作者头像 李华
网站建设 2026/10/18 6:02:17

大O表示法面试全攻略:从复杂度分析到数据结构性能优化

1. 为什么面试官总爱问大O表示法我在做技术面试官的时候&#xff0c;几乎每场技术面都会问候选人关于算法复杂度的问题。倒不是故意刁难&#xff0c;而是这一题能非常高效地把候选人的基础功底测出来。你想想&#xff0c;一个写了两三年业务代码的人&#xff0c;天天增删改查&a…

作者头像 李华