前端请求三部曲:Ajax / Fetch / Axios 演进与 Vue 工程化封装

从 Ajax → Fetch → Axios:前端网络请求演进史与工程化封装

前言

本篇是 Vue项目实战三板斧系列第一篇,专门聊聊前端最基础的网络请求。

不少同学上来就用 axios,会写但不太明白它到底是怎么来的。 这篇我就带大家简单走一遍进化路线:从最原始的 Ajax,到原生 Fetch,再到我们现在常用的 Axios,一步步看清它们的优缺点,最后一起封装一套简洁、好维护的工程化请求方案。 不求花里胡哨,只求看完能真正理解"我们为什么要这么写请求"。

一、最原始的网络请求:原生 XMLHttpRequest

要说网络请求,老祖宗必须是 XMLHttpRequest,也就是我们常说的 Ajax。 它实现了页面不刷新就能拿数据,在当年简直是黑科技。

1.1 原生手写 Ajax(最底层写法)

js 复制代码
// 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

原生写法太啰嗦,我们简单封装一版,方便复用。

js 复制代码
// 封装一个自己的 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请求。代码会层层嵌套、缩进不断加深,可读性极差,后期根本无法维护和修改,这就是典型的回调地狱问题。

js 复制代码
  
// 回调地狱示例
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 请求(带参数拼接)

js 复制代码
// 定义参数
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 请求

js 复制代码
// 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 语法糖更香

js 复制代码
// 用 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

js 复制代码
// 封装一个通用的 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 ,否则会把错误当正常数据处理,导致页面报错。

js 复制代码
// 不写这句,404/500 不会进 catch
if (!res.ok) throw new Error("请求失败")
硬伤二:没有请求、响应拦截器,所有逻辑必须手写重复

详细解释: Fetch 原生不支持拦截器。如果你想给所有接口加 token、加请求头、统一处理返回值、统一报错,每个 fetch 都要写一遍,无法像 axios 那样全局配置一次到处生效。

js 复制代码
// 每个请求都要重复写一遍
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 完全不处理,你必须手动:

js 复制代码
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 基础使用

js 复制代码
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

js 复制代码
// 简写 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 优雅版

js 复制代码
// 登录请求
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

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 按业务拆分文件

bash 复制代码
src/
└── api/
    ├── request.js   # axios 封装
    ├── user.js      # 用户相关接口
    ├── goods.js     # 商品相关接口
    └── order.js     # 订单相关接口

5.2 user.js 示例

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 组件中使用

js 复制代码
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

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 实现登录鉴权。

相关推荐
泯泷4 小时前
那段文字是谁删的?Yjs 14 正式版之前,一套删除归属方案的实现与边界
前端·javascript·算法
野槐7 小时前
前端项目优化(有了我,会发生...)
前端
aa小小7 小时前
【无标题】
前端
计算机魔术师8 小时前
还在单线程跟 AI 聊天?Claude Code 并行多会话让你一个人干三个人的活
前端
志尊宝8 小时前
Vue3 零基础每日笔记(038):亲手封装 useDebounceFn 与 useThrottleFn——高频事件的流量阀
前端·javascript·vue·html·vue3
JavaGuide8 小时前
对标 MinIO!全新一代分布式文件系统正式发布
前端·后端
风骏时光牛马8 小时前
AI驱动自动化业务工作流搭建实践
前端
码云之上9 小时前
Skill 里的脚本终于能跑了,星悟接 CubeSandbox 的纪实
前端·人工智能·前端框架
IT_陈寒9 小时前
Vue computed属性这个坑,我居然踩了三次才爬出来
前端·人工智能·后端
烈风逍遥9 小时前
第三篇:组件化实践,SpTable 通用表格组件设计
前端·架构