前端请求三部曲:Ajax / Fetch / Axios 演进与 Vue 工程化封装
从 Ajax → Fetch → Axios:前端网络请求演进史与工程化封装
前言
本篇是 Vue项目实战三板斧系列第一篇,专门聊聊前端最基础的网络请求。
不少同学上来就用 axios,会写但不太明白它到底是怎么来的。 这篇我就带大家简单走一遍进化路线:从最原始的 Ajax,到原生 Fetch,再到我们现在常用的 Axios,一步步看清它们的优缺点,最后一起封装一套简洁、好维护的工程化请求方案。 不求花里胡哨,只求看完能真正理解“我们为什么要这么写请求”。
一、最原始的网络请求:原生 XMLHttpRequest
要说网络请求,老祖宗必须是 XMLHttpRequest,也就是我们常说的 Ajax。 它实现了页面不刷新就能拿数据,在当年简直是黑科技。
1.1 原生手写 Ajax(最底层写法)
// 1. 创建一个 ajax 实例
const xhr = new XMLHttpRequest();
// 2. 配置请求:请求方式、地址、异步(true)
xhr.open('GET','/api/data',true);
// 3. 监听请求状态变化(旧版常用写法)
xhr.onreadystatechange = function(){
// readyState === 4 表示请求完成
if(xhr.readyState === 4){
// status 200~299 代表请求成功
if(xhr.status >= 200 && xhr.status < 300){
// 把后端返回的 JSON 字符串转成对象
const result = JSON.parse(xhr.responseText);
console.log('请求成功',result)
}else{
console.log('请求失败',xhr.status);
}
}
}
// 网络异常、跨域失败时触发
xhr.onerror = function(){
console.log('网络异常或跨域错误')
}
// 4. 发送请求
xhr.send()
1.2 简单封装一下 Ajax
原生写法太啰嗦,我们简单封装一版,方便复用。
// 封装一个自己的 ajax 函数
function myajax(options) {
// 1. 创建请求实例
const xhr = new XMLHttpRequest()
// 2. 解构配置参数,给默认值
const {
method = 'GET', // 默认 GET 请求
url, // 请求地址
data = null, // 参数(这里演示无参)
success, // 成功回调
error // 失败回调
} = options
// 3. 初始化请求,转大写防止小写出错
xhr.open(method.toUpperCase(), url, true)
/*
旧写法:onreadystatechange 需要判断 readyState
新写法:onload 等价于 readyState=4,直接用更简单
*/
xhr.onload = function () {
// 判断 HTTP 状态码是否成功
if (xhr.status >= 200 && xhr.status < 300) {
// 解析后端返回的 JSON
const res = JSON.parse(xhr.responseText)
// 有成功回调就执行
success && success(res)
} else {
// 失败把状态码抛出去
error && error(xhr.status)
}
}
// 网络异常触发
xhr.onerror = function () {
error && error('网络异常或跨域')
}
// 发送请求(这里不传参数,避免 GET 报错)
xhr.send()
}
1.3 Ajax 的缺点(为啥我们不用它了)
缺点一:配置繁琐,全手动判断
详细解释:每发送一个请求,都要重复创建 XMLHttpRequest 实例、调用 open 配置请求、监听状态/错误、调用 send 发送请求,步骤多且冗余。而且要手动判断 readyState 请求状态、手动判断 status HTTP状态码、手动执行 JSON.parse 解析后端返回的字符串,没有任何自动处理逻辑,代码量极大,每写一个请求都要重复大量代码。
缺点二:回调一多直接回调地狱
详细解释: Ajax基于回调函数处理结果,一旦遇到连续多个依赖请求(比如先获取用户ID,再用ID获取详情,再用详情获取订单),就需要在success回调里嵌套下一个myajax请求。代码会层层嵌套、缩进不断加深,可读性极差,后期根本无法维护和修改,这就是典型的回调地狱问题。
// 回调地狱示例
myajax({
url:'/api/user',
success(res){
// 第一层回调
myajax({
url:`/api/detail?id=${res.id}`,
success(res){
// 第二层回调
myajax({
url:`/api/order?did=${res.detailId}`,
success(res){
// 第三层回调,代码彻底混乱
}
})
}
})
}
})
缺点三:没有拦截器、没有超时、没有统一处理
详细解释: 原生XHR没有全局请求/响应拦截机制,每个请求都要单独写错误处理、单独加请求头、单独处理返回结果。比如要给所有接口加token,必须在每个 xhr.open 之后,手动写 setRequestHeader ;想要设置请求超时,需要额外写定时器手动中断请求,无法做到一处配置、全局生效。
缺点四:不支持 Promise
详细解释: 原生Ajax不支持Promise语法,无法使用 async/await 、 then/catch 这种现代化异步写法,只能用传统回调函数。异步流程完全不可控,代码书写不优雅,也无法和现代前端的异步语法接轨,和后续的Fetch、Axios生态完全脱节。
总结:理解底层即可,真实项目没人直接写原生 Ajax。
二、现代浏览器原生:Fetch API
时代在进步,浏览器终于看不下去了,推出了Fetch。基于 Promise,告别回调,写法清爽多了。不用从头开始造,省时省力。
2.1 GET 请求(带参数拼接)
// 定义参数
const params = {
id: 123,
name: "text"
}
// 把对象转成 ?id=123&name=text 这种格式
const query = new URLSearchParams(params).toString();
// 发送请求
fetch(`/api/user?${query}`)
.then(res => {
// fetch 很坑:只有网络失败才 reject,404/500 依然走 then
if (!res.ok) throw new Error("请求失败:" + res.status)
// 解析 JSON
return res.json()
})
.then(data => {
console.log("获取数据成功", data)
})
.catch(err => {
console.error("请求异常", err)
})
2.2 POST 请求
// fetch 的 post 请求
fetch("/api/user", {
method: "POST",
headers: {
// 必须声明传递 JSON 格式
"Content-Type": "application/json"
},
// 对象转 JSON 字符串
body: JSON.stringify({
username: "admin",
password: "123456"
})
})
.then(res => {
if (!res.ok) throw new Error(res.status)
return res.json()
})
.then(data => {
console.log("请求成功", data)
})
.catch(err => {
console.error("请求失败", err)
})
2.3 async/await 语法糖更香
// 用 async/await 让代码看起来像同步
async function fetchData() {
try {
// 请求参数
const postData = {
username: "zhangsan",
password: "123456"
}
// 发送请求
const response = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(postData)
})
// 判断请求是否成功
if (!response.ok) {
throw new Error("请求失败,状态码:" + response.status)
}
// 解析数据
const result = await response.json()
console.log("请求成功", result)
} catch (error) {
// 统一捕获错误
console.error("请求错误", error)
}
}
// 执行
fetchData();
2.4 简单封装一版 Fetch
// 封装一个通用的 fetch 请求函数
async function request(url, options = {}) {
// 解构参数
const { method = 'GET', data, headers = {}, ...rest } = options;
// 方法转大写
const upperMethod = method.toUpperCase();
// 最终请求地址
let fetchUrl = url;
// 配置 fetch 参数
let fetchOptions = {
method,
headers,
...rest
}
// GET 请求:参数拼接到地址栏
if (upperMethod === 'GET' && data) {
const queryStr = new URLSearchParams(data).toString()
fetchUrl += `?${queryStr}`
}
// POST/PUT/DELETE 处理 JSON 格式
if (['POST', 'PUT', 'DELETE'].includes(upperMethod) && data) {
// 设置请求头
fetchOptions.headers['Content-Type'] = 'application/json';
// 转 JSON 字符串
fetchOptions.body = JSON.stringify(data)
}
try {
// 发送请求
const res = await fetch(fetchUrl, fetchOptions)
// 判断状态
if (!res.ok) { throw new Error(`请求错误:${res.status}`) }
// 解析并返回数据
return await res.json()
} catch (err) {
// 打印并抛出异常,外部可以继续 catch
console.error('请求失败', err)
throw err
}
}
2.5 Fetch 有哪些硬伤?
硬伤一:网络错误才 reject,404 / 500 依然走 then,必须手动判断
详细解释: Fetch 的“成功”只看网络是否发出去,只要浏览器收到了 HTTP 响应,哪怕是 401、404、500 错误, fetch 依然认为请求“成功”,会走进 then 而不是 catch 。 所以你必须每次手动判断 res.ok ,否则会把错误当正常数据处理,导致页面报错。
// 不写这句,404/500 不会进 catch
if (!res.ok) throw new Error("请求失败")
硬伤二:没有请求、响应拦截器,所有逻辑必须手写重复
详细解释: Fetch 原生不支持拦截器。如果你想给所有接口加 token、加请求头、统一处理返回值、统一报错,每个 fetch 都要写一遍,无法像 axios 那样全局配置一次到处生效。
// 每个请求都要重复写一遍
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token
}
硬伤三:无法取消请求,没有 abort 方案(必须额外用 AbortController)
详细解释: 原生 fetch 自身不支持取消请求。想要取消必须手动搭配 AbortController ,写一堆额外代码,切换页面、重复请求时无法自动中断,容易造成内存泄漏、重复请求、旧数据覆盖新数据等问题。
硬伤四:没有自带超时处理,超时要自己写定时器包装
详细解释: Axios 直接配置 timeout: 5000 就可以自动超时中断。Fetch 没有超时配置,想实现超时必须自己包一层 Promise.race + setTimeout ,每个请求都要重复造轮子,非常麻烦。
硬伤五:请求 body 不会自动处理,必须手动 JSON.stringify
详细解释: Axios 会自动帮你把对象转成 JSON、自动加 Content-Type: application/json 。 Fetch 完全不处理,你必须手动:
body: JSON.stringify(data)
headers: { "Content-Type": "application/json" }
少一句后端就收不到数据,非常容易漏写。
硬伤六:无法监听请求进度(上传/下载进度很难实现)
详细解释: Axios 自带 onUploadProgress 可以直接监听上传进度做进度条。 Fetch 原生不支持,只能通过 ReadableStream 自己手动解析流,实现复杂、成本极高,普通项目基本没法用。
结论:小 demo 能用,中大型项目顶不住。
三、项目主流方案:Axios 全面上手
前面我们了解了:
- 最底层:XMLHttpRequest,功能强但写起来巨麻烦
- 现代原生:Fetch,语法好看,但能力残缺
那有没有一个东西,既保留 XHR 的强大能力,又拥有 Fetch 的 Promise 优雅语法,还把所有坑都填了?
它就是我们现在前端项目的事实标准 —— Axios。
重点来了: Axios 并不是什么新底层技术,它本质上就是对原生 XMLHttpRequest 再次封装、增强、Promise 化之后的终极工具库。 相当于把我们刚才手写的简陋 myajax、简陋 fetch 封装,做到了工业级极致。
它解决了所有痛点:支持 Promise、自动处理 JSON、拦截器、取消请求、超时、进度监听…… 所以现在 Vue、React、小程序、Node 项目里,大家几乎都默认用 Axios。
3.1 基础使用
import axios from 'axios'
// 完整写法
axios({
method: 'get',
url: '/user',
params: { id: 10 }
})
.then(res => {
// axios 自动帮你解析了 JSON,直接拿 data
console.log(res.data)
})
.catch(err => {
console.log('请求失败', err)
})
3.2 简写 GET / POST
// 简写 GET
axios.get('/user', {
params: { id: 10 }
}).catch(err => {
console.log(err)
})
// 简写 POST(自动处理 JSON,不用自己 stringify)
axios.post('/login', {
username: 'admin',
password: '123456'
}).catch(err => {
console.log(err)
})
3.3 async/await 优雅版
// 登录请求
async function login() {
try {
const res = await axios.post('/login', {
username: 'admin',
password: '123456'
})
console.log(res.data)
} catch (err) {
// 请求失败、状态码错误都会进这里
console.log('请求失败', err)
}
}
四、工程化核心:Axios 二次封装(重点)
真实项目里,我们不会到处直接写 axios.get, 必须封装一次,统一处理:token、超时、状态码、错误提示。
4.1 封装 request.js
import axios from 'axios'
// 创建 axios 实例
const request = axios.create({
baseURL: '/api', // 统一接口前缀
timeout: 5000 // 超时时间 5 秒
})
// =================== 请求拦截器 ===================
request.interceptors.request.use(config => {
// 从本地拿到 token
const token = localStorage.getItem('token')
// 如果有 token,就加到请求头里
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
// 必须 return config
return config
})
// =================== 响应拦截器 ===================
request.interceptors.response.use(
res => {
// 直接返回后端数据,页面不用再 .data
return res.data
},
err => {
// 统一错误提示
console.log('请求出错', err)
// 抛出异常,让页面可以自己 catch 处理
return Promise.reject(err)
}
)
// 导出实例,页面引入使用
export default request
这一封装,好处直接拉满:
- 统一 baseURL,后期改地址只改一处
- 所有接口自动带 token,不用每个请求写
- 统一错误处理,不用每个接口 catch
- 响应直接返回 data,代码更干净
五、接口模块化管理(真正工程化)
封装完 axios 还不够,工程化必须接口模块化。
5.1 按业务拆分文件
src/
└── api/
├── request.js # axios 封装
├── user.js # 用户相关接口
├── goods.js # 商品相关接口
└── order.js # 订单相关接口
5.2 user.js 示例
import request from './request'
// 登录接口
export function loginApi(data) {
return request({
url: '/login',
method: 'post',
data
})
}
// 获取用户信息
export function getUserInfo() {
return request({
url: '/user/info',
method: 'get'
})
}
5.3 组件中使用
import { loginApi } from '@/api/user'
async function login() {
try {
const res = await loginApi({
username: 'admin',
password: '123456'
})
console.log('登录成功', res)
} catch (err) {
console.log('登录失败')
}
}
优点:
- 接口统一管理,便于维护
- 页面逻辑更干净
- 方便 mock、方便重复调用
六、在 Vue3 组件中实战使用
Vue
<template>
<div>
<button @click="getUser">获取用户信息</button>
</div>
</template>
<script setup>
import { getUserInfo } from '@/api/user'
import { ref } from 'vue'
const userInfo = ref({})
// 获取数据
const getUser = async () => {
try {
const res = await getUserInfo()
userInfo.value = res
} catch (err) {
console.log('请求失败')
}
}
</script>
可以看到,组件中已经完全看不到底层的 axios 调用,只需要调用封装好的接口方法即可完成数据请求。代码更加简洁清晰,职责更加单一,真体现了前端工程化低耦合、高复用、易维护的优势。
七、总结
这一篇我们完整走完了前端请求进化之路:
1. Ajax(XMLHttpRequest):底层基石,所有请求的根
2. Fetch:浏览器原生 Promise 方案,但能力有限
3. Axios:基于 XHR 深度封装,现代前端工程化最佳实践
工具一直在变,但核心思路没变:
从繁琐难用,到语法简化,再到功能完善。 Axios 之所以成为主流,正是因为它在原生 XHR 的基础上做了大量贴心封装,让我们不用再重复处理各种细节。
而封装和工程化的意义,也远不止“省事”这么简单:
统一的配置、统一的错误处理、按模块拆分接口,本质上都是为了让代码更简洁、更好维护、更容易协作。 一个项目是否规范,往往从请求层就能看出来。
搞懂这些来龙去脉,以后再写接口、做封装,就不再是机械复制代码,而是真正知道自己在做什么、为什么这么做。
这也是 Vue 项目工程化的第一步。 下一篇我们继续三板斧第二篇:VueRouter 路由与路由守卫,配合今天的 token 实现登录鉴权。