1. Axios 核心概览

1.1 Axios 是什么

在网页开发中,我们需要从服务器获取数据。Axios 就是连接前端页面后端服务器的那座“桥梁”。

(1) 身份

Axios 是一个第三方库,某团队的一个开源项目,主流框架(如Vue,React)都推荐使用这个。

(2) 特征

Axios 是异步的。它在请求数据时,不会前端网页,用户依然可以进行其他操作。

1.2 为什么选它

(1) 技术对比

在 Axios 出现之前,还有 XMLHttpRequest、jQuery、Fetch API 等

阶段 工具 缺点)
1. 原始社会 XHR 代码太长、嵌套地狱
2. 封建时代 jQuery 捆绑销售(为了个请求得下载整个库)
3. 现代雏形 Fetch 半成品(不报错、得手动转格式)
4. 现代文明 Axios 没缺点:自动转格式、报错准、功能全
(2) 学习理由
  • 行业标准: 虽然有 Fetch(原生)和 TanStack Query(进阶管理),但 Axios 仍是目前企业级项目中使用最广、最稳的基础库。
  • 一通百通: Axios 的设计理念(拦截器、Promise 链)是网络请求的“通用模板”,学会它,换任何工具都能秒上手。
  • 跨端神技: 一套代码在浏览器(前端)和 Node.js(后端)都能跑,性价比极高。

2. 快速上手

2.1 安装与集成

(1) 引入

引入 Axios 这类热门的前端库,基本上都有两种方式

a. CDN 引入(个人练习)

<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

b. 直接下载到本地项目中(正式项目))

// (npm为例)
npm install axios
(2) 无需在 main.ts 中引入

第三方库有两种:插件普通库,普通库可以在各种框架使用,插件和某一个框架强绑定,插件都需要在main.ts引入。

像 Pinia、Router 等属于 Vue 的插件,Axios 属于普通第三方库,Element Plus 既能当插件也能当普通库。

维度 Vue 插件 (Pinia/Router/Element) 普通库 (Axios/Lodash/Element)
是否依赖 Vue 必须依赖,离开 Vue 跑不通 独立存在,哪里都能跑
main.ts 注册 必须 (app.use) 不需要
使用方式 全局可用,或通过 Vue 钩子调用 哪里用到,哪里 import
类比 房子的中央空调系统,与房子绑定 房间里的一台电风扇,可以拿到别的房子用

2.2 基础请求方法

(1) 请求的结构

请求包含三部分:请求行、请求头、请求体

a. 请求行:
  • Method (方法如GET)
  • URL (路径如 /api/v1/user/profile)
  • **Protocol **(协议通常是 HTTP/1.1HTTP/2)
b. 请求头:
标题 (Header) 含义 举例
Host 请求的目标服务器地址 api.jimidou.com
Content-Type 【极重要】 告诉服务器发过去的数据是什么格式 application/json (JSON数据) 或 multipart/form-data (传文件)
Authorization 【最常用】 身份验证令牌(Token) Bearer eyJhbGciOiJIUzI1... (后端验证身份的唯一依据)
User-Agent 告诉服务器你用的是什么浏览器或设备 Mozilla/5.0 (Windows NT 10.0; Win64; x64)...
Accept 告诉服务器你希望接收什么格式的回包 application/json, text/plain, */*
c. 请求体:

GET 请求的请求体通常是空的。POST/PUT 请求装载着具体的业务数据。

{
  "username": "zhangsan",
  "password": "password123"
}
(2) 四种请求方法

Axios 的请求结构有默认的数据,需要配置的东西很少,比如MethodURLAuthorization请求体

a. GET:获取数据
import axios from 'axios'

const getList = async () => {
  // await 必须包在 async 函数里
  try {
    // 1. 等待请求结果,直接把货给 res
    const res = await axios.get(
      '/api/question/list', 
      {
        params: { page: 1 },
        headers: { 
          'Authorization': 'Bearer xxx',
          'Accept-Language': 'zh-CN'
        },
        timeout: 5000
      }
    )
    
    // 2. 只有成功了才会运行到这一行
    console.log('拿到了:', res.data)
    
  } catch (err) {
    // 3. 如果请求失败(比如 404 或超时),会直接跳到这里
    console.error('出错了:', err)
  }
}
b. POST:提交表单/JSON

POST 通常用于发送 JSON 对象。它的第二个参数就是 数据本身,第三个参数才是 配置项

const addQuestion = async () => {
  try {
    const res = await axios.post(
      '/api/question/add', 
      { 
        // 第二个参数:请求体 (Body)
        title: '如何学习 Axios?',
        content: '我觉得吃透请求结构很重要。' 
      }, 
      {
        // 第三个参数:配置项 (Config)
        headers: { 'Authorization': 'Bearer xxx' },
        timeout: 5000
      }
    );
    console.log('创建成功:', res.data);
  } catch (err) {
    console.error('提交失败:', err);
  }
}
c. PUT:更新

PUT 的结构和 POST 一模一样。它通常用于修改已经存在的数据(比如改个标题、换个头像)。

const updateQuestion = async (id) => {
  try {
    const res = await axios.put(
      `/api/question/update/${id}`, // 路径参数写法
      { title: '修改后的标题' },      // 请求体
      { headers: { 'Authorization': 'Bearer xxx' } } // 配置项
    );
    console.log('修改成功');
  } catch (err) {
    console.error('修改失败');
  }
}
d. DELETE:删除

流派 1:参数在路径里(最推荐,最常用)

const deleteById = async (id) => {
  try {
    // 像 GET 一样简单,只有两个参数:URL 和 Config
    await axios.delete(`/api/question/delete/${id}`, {
      headers: { 'Authorization': 'Bearer xxx' }
    });
    console.log('删除成功');
  } catch (err) {
    console.error('删除失败');
  }
}

流派 2:参数在请求体里(偶尔见,需注意) 如果后端非要你传个 data: { id: 1 } 才能删:

await axios.delete('/api/question/delete', {
  headers: { 'Authorization': 'Bearer xxx' },
  data: { id: 123 } // 必须写在 data 属性里!
});
(3) 两种 Axios 的写法
a. then 的方式(繁琐)
import axios from 'axios'
// 1. 发起请求
axios.get('/api/question/list', {
  params: { page: 1 },                // 对应 URL 里的查询参数
  headers: {
    'Authorization': 'Bearer xxx',    // 手动塞入 Token
    'Accept-Language': 'zh-CN'        // 手动指定语言,给后端看的用来返回不同数据库的数据,99%的网站没必要
  },
  timeout: 5000                       // 这一次请求如果 5 秒没回就掐断
})
.then(res => {
  // 2. 成功回执:res 是整个包裹,res.data 是后端给的货
  console.log('拿到了:', res.data)
})
.catch(err => {
  // 3. 失败回执:断网、超时、404 都会跑这里
  console.error('出错了:', err)
})

即使封装成方法,也要写then

// 定义方法
const getListByThen = () => {
  // 注意:这里必须 return,否则外部拿不到结果
  return axios.get('/api/list').then(res => {
    return res.data; // 返回处理后的数据
  });
}

// 调用方法
getListByThen().then(data => {
  console.log('拿到了数据:', data);
});

计算机执行代码的速度极快,而网络请求相对于 CPU 来说慢得像蜗牛。

  1. 第 0 毫秒:你调用了 getListByThen()
  2. 第 1 毫秒:Axios 把请求发向了互联网。
  3. 第 2 毫秒:重点来了!getListByThen 执行完毕。因为它是一个异步函数,它不会等网络结果,而是立刻返回了一个 Promise 对象。此时,你的 list 变量里装的就是这个对象。
  4. 第 500 毫秒:服务器的数据终于顺着网线回来了,触发了你函数内部的 .then()

结论:你在第 2 毫秒试图拿 list 去用,但数据在第 500 毫秒才到。你拿到的只能是那个“空支票”。

b. await 方式,简洁易懂
import axios from 'axios'

const getList = async () => {
  // await 必须包在 async 函数里
  try {
    // 1. 等待请求结果,直接把货给 res
    const res = await axios.get('/api/question/list', {
      params: { page: 1 },
      headers: { 'Authorization': 'Bearer xxx' },
      timeout: 5000
    })
    
    // 2. 只有成功了才会运行到这一行
    console.log('拿到了:', res.data)
    
  } catch (err) {
    // 3. 如果请求失败(比如 404 或超时),会直接跳到这里
    console.error('出错了:', err)
  }
}

这就是 await 的优势。它在底层帮你做了一件事:强制让当前代码行“停工”,等到那张“空支票”兑现成“现金”后,再把现金赋值给左边的变量。

3. 进阶实战

每次写 Axios 都写 headers、timeout等这么多内容过于复杂且臃肿,在项目中,通常需要对其进行封装。

http/
├── request.ts  # 拦截器等axios的第一层封装
├── api.ts      # axios的第二层封装(封装了4种方法)
├── modules/    # 封装各个具体的后端api方法
└── index.ts    # 暴露所有的api方法

3.1 request.ts

(1) 创建 Axios 实例

baseURL 本地开发中写在 vite.config.ts 里面,上线时写在 nginx.conf 里面,/api 的意义在于替换为真正的地址。

创建实例时可以放入一些固定的初始化数据,比如 baseURL,请求行、请求头的固定的数据都可以在这里修改,不写则是用默认值。

const instance: AxiosInstance = axios.create({
  baseURL: '/api',
  timeout: 5000,
});
(2) 请求拦截器

这部分的主要作用是把 Token 放入 请求头的 Authorization 中,因为每次请求的 Token 可能不一样,所以每次请求都要重新放入。

instance.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    const { accessToken } = useAuthStore()
    if (!accessToken) return config

    config.headers ??= {} as AxiosRequestHeaders
    config.headers.Authorization = `Bearer ${accessToken.trim()}`

    return config
  },
  (error: AxiosError) => {
    console.error('请求拦截器错误:', error)
    return Promise.reject(error)
  }
)
a. (error: AxiosError)作用
  • 传递信号:它是个“传声筒”。捕获的是上一个拦截器抛出的失败信号。
  • 打破死等:必须写 return Promise.reject(error)。这样如果前面有人取消了请求或拦截了请求,页面上的 await 才能立刻收到“失败”通知,而不是在那转圈死等。
  • 边界警告:它不负责抓取当前 config 逻辑里的 JS 崩溃(如 trim() 报错),那种错会直接导致程序炸裂。
b. 请求拦截器什么情况下会走(error: AxiosError)

基本上不会走的,直接理解成不用我们管,系统自己会处理

  • 一个页面有很多请求,某个请求(error: AxiosError)了,后续依赖此请求的请求全都(error: AxiosError)

    举例:只有请求2是建立在请求1的基础上,请求2的(error: AxiosError)才会依据请求1而不是自己去后端碰墙。针对并发的请求来说,即使请求2慢了两秒,即在请求1已经(error: AxiosError)了,因为二者没有依赖关系所以请求2依然会自己去后端撞墙?

  • 代码里写的 throw 或者 Promise.reject,比如 config 里面写 if (!window.navigator.onLine) { return Promise.reject(new Error('没网发个毛请求')); }

  • Axios 内部的“规则校验”

(3) 响应拦截器
// 标记是否正在刷新token
let isRefreshing = false

// 等待队列,存放等待刷新token的请求的 Promise 控制函数
type FailedQueueItem = {
  resolve: (token: string | PromiseLike<string>) => void
  reject: (error: any) => void
}
let failedQueue: FailedQueueItem[] = []

function processQueue(error: any, token: string | null = null) {
  failedQueue.forEach(({ resolve, reject }) => {
    if (error) {
      reject(error)
    } else if (token !== null) {
      resolve(token)
    } else {
      reject(new Error('No token to resolve'))
    }
  })
  failedQueue = []
}

/**
 * 响应拦截器
 * interceptors.response.use(successFn, errorFn)
 */
instance.interceptors.response.use(
  (response: AxiosResponse) => {
    console.log('响应拦截器:', response)
    return response
  },
  async (error: AxiosError) => {
    console.error('响应拦截器错误:', error.response?.data)
    // 是否调用刷新 token 方法
    const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean }

    // 无响应处理
    if (!error.response) {
      ElNotification.error({
        title: '网络错误',
        message: '服务器无响应,请检查网络或稍后重试'
      })
      return Promise.reject(error)
    }

    const status = error.response.status

    // 401 且没有刷新过 token,尝试刷新 token
    if ((status === 401) && !originalRequest._retry) {
      originalRequest._retry = true

      // 正在刷新 token,将请求加入等待队列
      if (isRefreshing) {
        return new Promise<string>((resolve, reject) => {
          failedQueue.push({ resolve, reject })
        })
          .then(token => {
            if (!originalRequest.headers) {
              originalRequest.headers = {} as AxiosRequestHeaders
            }
            originalRequest.headers.Authorization = `Bearer ${token}`
            return instance(originalRequest)
          })
          .catch(err => Promise.reject(err))
      }

      // 正在刷新 token
      isRefreshing = true

      try {
        const { data } = await instance.post('/users/refresh-token')
        const newToken = data?.data?.accessToken

        if (!newToken) {
          throw new Error('刷新 token 失败,未返回新的 accessToken')

          // 弹出重新登陆提示框
        }

        const authStore = useAuthStore()
        authStore.setToken(newToken)
        // 队列中的请求换上新 token 继续执行
        processQueue(null, newToken)

        if (!originalRequest.headers) {
          originalRequest.headers = {} as AxiosRequestHeaders
        }
        originalRequest.headers.Authorization = `Bearer ${newToken}`
        return instance(originalRequest)
      } catch (refreshError) {
        processQueue(refreshError, null)

        const authStore = useAuthStore()
        authStore.logout()

        ElNotification.error({
          title: '身份验证失败',
          message: '登录状态已过期,请重新登录'
        })
        router.push('/')
        return Promise.reject(refreshError)
      } finally {
        isRefreshing = false
      }
    }

    // 其他错误统一提示
    let message = '请求失败,请稍后重试'
    if (error.response.data && typeof error.response.data === 'object') {
      // 尽量取服务端的 message
      if ('message' in error.response.data && typeof error.response.data.message === 'string') {
        message = error.response.data.message
      }
    } else if (error.response.statusText) {
      message = error.response.statusText
    }

    ElNotification.error({
      title: `错误 ${status}`,
      message
    })

    return Promise.reject(error)
  }
)
a. 响应拦截器什么情况下会走(error: AxiosError)
  • 只要 HTTP 响应状态码(response.status)不在 2xx 范围内,就会触发 error 回调。
  • 断网/DNS解析失败、跨域错误 (CORS)。这种情况error.responseundefined
  • 请求超时 (Timeout)
  • 主动取消请求
  • 在请求拦截器里写了 throw new Error('缺少必要参数') 或者 return Promise.reject()

业务 Code ≠ HTTP Status

  • success 回调:HTTP Status = 200。哪怕后端 JSON 里写了 { "code": 500, "msg": "余额不足" },对 Axios 来说,这也是“一次成功的通信”。
  • error 回调:HTTP Status = 401/500 等。此时后端不仅要改 JSON,还得通过 response.setStatus(401) 修改 HTTP 封面。这部分通常写在后端的拦截器中
b. 常见错误状态码
  • 401 (Unauthorized):没登录,或者 Token 过期了。
  • 403 (Forbidden):登录了,没权限看这个页面。
  • 404 (Not Found):请求的接口地址写错了。
  • 500 (Internal Server Error):后端代码写炸了,服务器冒烟了。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐