Vue学习笔记-Axios 全局封装(自动带 Token、统一响应处理)

📝 作者:一名 Vue 的学习者
🕒 记录时间:2025年11月
💡 目标:使用axios处理网络请求,从后端获取token,F12可以看到我们的网络请求:


写到这里,我已经有了登录页面、有了布局、有了菜单,接下来要做的就是让整个项目“跑起来”。
而这一步的关键就是:把 Axios 封装好

为什么要封装 Axios?因为:

  • 每个请求都要带 Token,不封装会让每个 API 都写一遍;
  • 错误需要统一处理,不可能 everywhere 都 try/catch;
  • 后端的成功/失败格式不一致时,前端需要自己规范化数据,避免代码满天飞;
  • 未来要做“自动刷新 Token”,也必须先封装。

所以,这篇就是——让项目具备“成熟 API 调用能力”的核心篇章


1、创建目录结构

我习惯在 src 下加一个 utils/request.js

src/
├── api/
│   └── user.js
├── utils/
│   └── request.js
└── main.js
  • request.js → Axios 封装
  • api/** → 所有业务接口

2、安装 Axios

如果项目还没安装,则需要执行以下命令:

npm install axios

3、开始封装 request.js

这是本篇的核心内容,我会一步步写,不会用网上那种巨复杂模板,保证看得懂。


📌(3.1)创建 axios 实例

// 创建 axios 实例
const service = axios.create({
    baseURL: '/api', // 基础URL
    timeout: 10000, // 超时时间
    withCredentials: true, // 跨域请求时发送 cookies
})

📌(3.2)请求拦截器:自动携带 Token

我们要从 localStorage(或 pinia/store)里读取 token:

// 请求拦截器
service.interceptors.request.use(
    (config) => {
        // 请求开始时间(用于计算请求耗时)
        config.metadata = { startTime: new Date() }

        // 如果配置了 skipAuth 为 true,则不添加 token
        if (!config.skipAuth) {
            const token = localStorage.getItem('token')
            if (token) {
                config.headers.Authorization = `Bearer ${token}`
            }
        }

        // 处理 Content-Type
        // 如果手动设置了 Content-Type,则使用手动设置的值
        // 否则根据 data 类型自动设置
        if (!config.headers['Content-Type']) {
            if (config.data) {
                if (config.data instanceof FormData) {
                    config.headers['Content-Type'] = 'multipart/form-data'
                } else if (typeof config.data === 'string') {
                    config.headers['Content-Type'] = 'application/x-www-form-urlencoded'
                } else {
                    config.headers['Content-Type'] = 'application/json'
                }
            }
        }

        // 合并额外的 headers
        if (config.extraHeaders) {
            config.headers = {
                ...config.headers,
                ...config.extraHeaders
            }
        }

        return config
    },
    (error) => {
        console.error('Request Error:', error)
        return Promise.reject(error)
    }
)

只要登录时把 token 存起来,这里就会自动带上。


📌(3.3)响应拦截器:统一处理错误

假设后端返回格式如下:

{
  "code": 0,
  "msg": "ok",
  "data": {...}
}

我们把它规范化,让调用方更轻松:

// 响应拦截器(保持不变)
service.interceptors.response.use(
    (response) => {
        // 计算请求耗时
        const endTime = new Date()
        const startTime = response.config.metadata?.startTime
        if (startTime) {
            const duration = endTime - startTime
            console.log(`请求 ${response.config.url} 完成,耗时: ${duration}ms`)
        }

        // 根据后端返回的数据结构调整
        if (response.data && typeof response.data === 'object') {
            const { code, message } = response.data

            // 如果 code 不是成功状态码,统一处理错误
            if (code && code !== 200 && code !== 0) {
                ElMessage.error(message || '请求失败')
                return Promise.reject(new Error(message || '请求失败'))
            }

            // 返回实际数据
            return response.data.data !== undefined ? response.data.data : response.data
        }

        return response.data
    },
    (error) => {
        // 统一错误处理
        let message = '请求失败'

        if (error.response) {
            switch (error.response.status) {
                case 400:
                    message = '请求参数错误'
                    break
                case 401:
                    message = '未授权,请重新登录'
                    // 如果是认证失败,清除 token 并跳转到登录页
                    if (!error.config.skipAuth) {
                        localStorage.removeItem('token')
                        localStorage.removeItem('userInfo')
                        window.location.href = '/login'
                    }
                    break
                case 403:
                    message = '拒绝访问'
                    break
                case 404:
                    message = '请求地址不存在'
                    break
                case 500:
                    message = '服务器内部错误'
                    break
                case 502:
                    message = '网关错误'
                    break
                case 503:
                    message = '服务不可用'
                    break
                default:
                    message = `网络错误 (${error.response.status})`
            }
        } else if (error.request) {
            message = '网络连接失败,请检查网络'
        } else {
            message = error.message
        }

        ElMessage.error(message)
        console.error('Response Error:', error)

        return Promise.reject(error)
    }
)

注意:
这里的 window.$message 需要你在 main.js 里处理,下一节会讲。


4、在 main.js 里挂载全局 message

如果你用的是 Element Plus:

// main.js
import { ElMessage } from "element-plus";

window.$message = ElMessage;

5、在 API 模块中使用封装后的请求

例如创建一个用户相关 API 文件:

src/api/user.js

内容如下,这里请求登录接口,这里暂时先把用户名密码写死,没有从前端获取:

import request from '@/utils/request'

export function getUserList(params) {
    return request.get('/admin/user/page',params)
}

export function getToken() {
    return request.postForm('/auth/oauth2/token', {
        username: '',
        password: '',
        grant_type: 'password',
        scope: 'server'
    }, {
        skipAuth: true, // 跳过自动添加 token
        extraHeaders: {
            'authorization': 'Basic xxx'
        }
    })
}

这样写之后,每个 API 都会:

✔ 自动携带 token
✔ 自动处理错误信息
✔ 返回 data 数据,不用再 .data.data


6、在登录页中使用

<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Monitor, User, Lock } from '@element-plus/icons-vue'
import { getToken } from '@/api/user'
const router = useRouter()
const loginFormRef = ref()
// 登录表单数据
const loginForm = reactive({
  username: '',
  password: ''
})
// 加载状态
const loading = ref(false)
// 表单验证规则
const loginRules = {
  username: [
    { required: true, message: '请输入用户名', trigger: 'blur' },
    { min: 3, max: 20, message: '用户名长度在 3 到 20 个字符', trigger: 'blur' }
  ],
  password: [
    { required: true, message: '请输入密码', trigger: 'blur' },
    { min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
  ]
}
// 处理登录
const handleLogin = async () => {
  if (!loginFormRef.value) return

  try {
    // 表单验证
    await loginFormRef.value.validate()
    loading.value = true
    // 调用登录接口
    const response = await getToken()
    console.log(response)
    if (response!=null) {
      ElMessage.success('登录成功')
      // 存储token和用户信息(实际项目中应该使用更安全的方式)
      localStorage.setItem('token', response.access_token)
      localStorage.setItem('userInfo', JSON.stringify(response.user_info))
      // 跳转到首页
      router.push('/home')
    }
  } catch (error) {
    if (error.code === 401) {
      ElMessage.error(error.message)
    } else {
      ElMessage.error('登录失败,请重试')
    }
  } finally {
    loading.value = false
  }
}

// 页面加载时的初始化
onMounted(() => {
  // 检查是否已经登录
  const token = localStorage.getItem('token')
  if (token) {
    router.push('/home')
  }
})
</script>

这样我们就实现了从后端请求登录接口


7、解决跨域问题

server: {
            port: 6173, // 设置开发服务器端口
            host: '0.0.0.0', // 允许外部访问
            open: true, // 启动时自动打开浏览器
            proxy: {
                '/api': {
                    target: 'http://xxx',
                    changeOrigin: true,
                    rewrite: path => path.replace(/^\/api/, '')
                }
            }
        },

8、常见扩展点

本篇主要做基础封装,后面还会继续扩展:

功能

Token 自动刷新(refresh token)

取消重复请求、防抖 API

多环境 BaseURL 自动切换

文件上传、进度条

错误码自定义

这一篇只是“打地基”,后面会在这基础上继续升级。


8、小结

本篇完成之后,Axios 封装已具备后台项目应有的能力:

统一 baseURL
自动携带 Token
自动提示错误
登录失效自动跳回登录
API 模块清爽可维护

我们的项目正式进入“成熟结构”。

🌱 下一步计划:table表格、分页

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值