前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

vue-axios封装请求实战 拦截器与错误统一处理(十二)

首页2018-08-28 15:35:32Front-End
VueAxiosHTTP

项目写到第三个页面的时候,你大概率会发现一件事:每个组件的 created 里都躺着一段几乎一样的代码。拼 baseURL、塞 token、.then 里判断后端返回的 code、.catch 里弹一个 toast。改一次错误提示文案要动十几个文件,加一个新的 401 处理逻辑更是灾难。这篇把 axios 在 Vue 项目里的封装从头捋一遍,先讲清楚基础 API 和拦截器的执行规则,再一步步搭出 http.js 加 api.js 这套结构,最后补上取消请求、重复请求去重、超时重试这三件老文章里没写、但真上线之后一定会遇到的事。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • axios 的能力边界,以及它和原生 fetch 的差别在哪
  • 基础 API 速查,GET、POST、配置式调用、并发请求
  • 拦截器的执行顺序,以及一个很多人踩过的注册顺序坑
  • 请求方法封装,表单提交、文件上传、RESTful 四件套
  • 错误统一处理,HTTP 状态码和业务 code 的两层分工
  • http.js 加 api.js 的工程化目录,环境切换、超时、token 注入
  • 取消请求,CancelToken 已废弃,现在用 AbortController
  • 重复请求去重和超时重试的实现思路

# 一、axios 是什么

axios 是一个基于 Promise 的 HTTP 客户端,浏览器和 Node.js 里都能跑。它本身具有以下特征:

  • 从浏览器中创建 XMLHttpRequest
  • 从 Node.js 发出 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求和响应数据
  • 取消请求
  • 自动转换 JSON 数据
  • 客户端支持防止 CSRF/XSRF

这份清单里,真正让它在 2018 年前后干掉 vue-resource 和手写 XMLHttpRequest 封装的,其实是中间那三条:拦截器、数据转换、取消请求。

先说结论,浏览器原生的 fetch 到今天也没把这几件事做好。fetch 拿不到上传进度,没有超时选项(得自己配 AbortController 加定时器),HTTP 404 和 500 不会走 reject 分支,需要你自己判断 res.ok,请求体和响应体都要手动 JSON.stringify 和 res.json()。axios 把这些琐事全包了,代价是多了十几 KB 的包体。业务项目里我一直选 axios,纯静态站或者只发一两个请求的场景才会考虑 fetch。

需要提前说明的是,这篇文章写于 2018 年,当时项目跑的是 axios 0.18 和 Vue 2。axios 现在已经到 1.x,绝大部分 API 保持兼容,少数几处有变化的地方我会在对应章节单独标出来。

# 二、基础 API 速查

axios 给每个 HTTP 方法都配了别名,参数结构分两类。request、get、delete、head、options 这一组没有请求体,第二个参数直接就是 config;post、put、patch 这一组有请求体,第二个参数是 data,第三个才是 config。

  • axios.request(config)
  • axios.get(url[, config])
  • axios.delete(url[, config])
  • axios.head(url[, config])
  • axios.options(url[, config])
  • axios.post(url[, data[, config]])
  • axios.put(url[, data[, config]])
  • axios.patch(url[, data[, config]])

这个参数位置的差异是新手最容易翻车的地方。写 axios.get('/user', { id: 1 }) 拿不到参数,因为第二个位置是 config,id 不是 config 的合法字段,直接被丢掉了。

# 2.1 GET 请求

query 参数有两种写法,直接拼在 URL 上,或者交给 params 让 axios 帮你序列化。

// 向具有指定ID的用户发出请求
axios.get('/user?ID=12345')
.then(function (res) {
    console.log(res);
})
.catch(function (error) {
    console.log(error);
});

// 也可以通过 params 对象传递参数
axios.get('/user', {
    params: {
        ID: 12345
    }
})
.then(function (response) {
    console.log(response);
})
.catch(function (error) {
    console.log(error);
});
@前端进阶之旅: 代码已经复制到剪贴板

优先用 params。它会自动做 encodeURIComponent,中文和特殊字符不用你操心,值是 undefined 的字段还会被跳过,不会拼出 ?name=undefined 这种脏 URL。手动拼字符串则要自己处理这些边界。

params 遇到数组时默认序列化成 ids[]=1&ids[]=2,如果后端要的是 ids=1,2 或者 ids=1&ids=2,就得传 paramsSerializer 自己接管这一步。这个我踩过,前后端联调时对着 Network 面板看了半天才反应过来是数组格式对不上。

# 2.2 POST 请求

POST 的 data 在第二个参数,config 在第三个。

axios.post('/user', {
    userId: "123"
}, {
    headers: {
        token: "abc"
    }
})
.then(function (res) {
    console.log(res);
})
.catch(function (error) {
    console.log(error);
});
@前端进阶之旅: 代码已经复制到剪贴板

传对象时 axios 会自动 JSON.stringify 并把 Content-Type 设成 application/json。如果后端要的是表单格式,那就得自己转,第五节会讲。

# 2.3 配置式调用

axios(config) 是最原始的形态,上面那些别名都是它的语法糖。GET 请求发送参数在 params 中定义,POST 请求发送的是 request body,需要在 data 中定义。

// get 在params中定义
axios({
    url: "package.json",
    method: "get",
    params: {
        userId: "123"
    },
    headers: {
        token: "http-test"
    }
}).then(res => {
    console.log(res.data);
})

// post 在data中定义
axios({
    url: "package.json",
    method: "post",
    data: {
        userId: "123"
    },
    headers: {
        token: "http-test"
    }
}).then(res => {
    console.log(res.data);
})
@前端进阶之旅: 代码已经复制到剪贴板

原文这两处 URL 写的是 pakage.json,少了一个 c,顺手改成 package.json。

配置式写法在封装的时候更好用,因为整个请求就是一个纯对象,可以拼、可以透传、可以存进配置表。后面封装 postRequest、putRequest 的时候用的都是这种形式。

# 三、并发请求

有些页面进来要同时拉几份数据,比如用户信息和用户权限,两个接口互不依赖,串行发就是白白多等一个 RTT。

function getUserAccount() {
    // 返回一个promise对象
    return axios.get("/user/1234");
}
function getUserPermissions() {
    // 返回一个promise对象
    return axios.get("/user/1234/getUserPermissions");
}

// 一次性返回两个接口
axios.all([getUserAccount(), getUserPermissions()]).then(axios.spread((acct, perms) => {
    // spread展开两个返回的结果
    // 两个请求现已完成
}))
@前端进阶之旅: 代码已经复制到剪贴板

原文这段的函数定义写的是 getUserAcount 和 getUserPermissions,调用处却写成了 getUserAccount 和 getUserPerssions,两处都对不上,跑起来直接是 is not defined,上面已经改一致了。

axios.all 和 axios.spread 现在被官方标记为废弃,推荐直接用原生的 Promise.all,写出来其实更短:

const [acct, perms] = await Promise.all([
  getUserAccount(),
  getUserPermissions()
])
@前端进阶之旅: 代码已经复制到剪贴板

这里有个坑要注意。Promise.all 是全有或全无,任何一个请求失败,整个 await 就抛错,前面已经成功的那份数据你也拿不到。如果页面上这两块内容可以独立降级,改用 Promise.allSettled,它会等所有请求都有结果,返回一个带 status 字段的数组,你再逐个判断。

# 四、拦截器

拦截器是 axios 相对手写封装最大的价值点。它在请求真正发出去之前、和响应交给业务代码之前各插了一个口子,所有请求的公共逻辑都可以收在这两个口子里。

new Vue({
    el: "#app",
    data: {
        msg: ""
    },
    // 初始化生命周期的一个函数
    mounted: function () {
        // 拦截请求之前
        axios.interceptors.request.use(config => {
            // 这里做一些拦截操作,拦截用户的请求 请求之前做一些loading处理
            return config;
        })
        // 拦截响应之后处理
        axios.interceptors.response.use(response => {
            // 这里做一些拦截操作,响应以后做什么,在返回数据
            return response;
        })
    },

    methods: {
        get: function () {

        },
        post: function () {

        }
    }
})
@前端进阶之旅: 代码已经复制到剪贴板

这段演示代码把 interceptors.use 写在了组件的 mounted 里,只是为了让例子跑起来看效果。真实项目千万别这么写,mounted 每次进入页面都会执行一次,拦截器会一层层叠加上去,同一个响应被处理十几遍。拦截器要注册在应用初始化的地方,全局只跑一次。

# 4.1 执行顺序

拦截器可以注册多个,但两条链的执行顺序不一样。请求拦截器是后注册的先执行,响应拦截器是先注册的先执行。

fe
  • 一、axios 是什么
  • 二、基础 API 速查
    • 2.1 GET 请求
    • 2.2 POST 请求
    • 2.3 配置式调用
  • 三、并发请求
  • 四、拦截器
    • 4.1 执行顺序
    • 4.2 别污染全局
  • 五、请求封装与异常统一处理
    • 5.1 axios 请求封装
    • 5.2 请求异常统一处理
    • 5.3 resolve 还是 reject
    • 5.4 在 Vue 项目中使用
  • 六、工程化封装 http.js 加 api.js
    • 6.1 环境切换
    • 6.2 超时和请求头
    • 6.3 请求拦截,注入 token
    • 6.4 响应拦截,登录过期与错误提示
    • 6.5 封装 get 和 post
    • 6.6 api 的统一管理
    • 6.7 完整封装代码
  • 七、取消请求
  • 八、重复请求去重
  • 九、超时重试
  • 十、这套封装和 Vue 生态的衔接
  • 总结
  • 参考

← Vue 实例属性与实例方法完全指南(十三)Vue 路由 vue-router 用法完全梳理(十一) →