最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 面试官:Vue项目中有封装过axios吗?怎么封装的?

    正文概述 掘金(jjjona0215)   2021-08-24   554

    面试官:Vue项目中有封装过axios吗?怎么封装的?

    一、什么是axios,有什么特性

    描述

    axios是一个基于promiseHTTP库,可以用在浏览器或者node.js中。本文围绕XHR。

    特性

    • 从浏览器中创建XMLHttpRequests
    • 从node.js创建http请求
    • 支持promise API
    • 拦截请求与响应
    • 转换请求数据与响应数据
    • 取消请求
    • 自动转换JSON数据
    • 客户端支持防御XSRF

    背景

    Vue2.0起,尤大宣布取消对 vue-resource 的官方推荐,转而推荐 axios。现在 axios 已经成为大部分 Vue 开发者的首选,目前在github上有87.3k star。axios的熟练使用和基本封装也成为了vue技术栈系列必不可少的一部分。如果你还不了解axios,建议先熟悉 axios官网文档。

    基本使用

    安装

    npm install axios -S
    

    使用

    import axios from 'axios'
    // 为给定ID的user创建请求 
    axios.get('/user?ID=12345')   
        .then(function (response) {     
            console.log(response);   
        })   
        .catch(function (error) {    
            console.log(error);   
        });  
    // 上面的请求也可以这样做 
    axios.get('/user', {     
        params: {ID: 12345}})   
        .then(function (response) {     
            console.log(response);   
        })   
        .catch(function (error) {     
            console.log(error);   
        });
    

    二、Vue项目中为什么要封装axios

    axios的API很友好,可以在项目中直接使用。但是在大型项目中,http请求很多,且需要区分环境, 每个网络请求有相似需要处理的部分,如下,会导致代码冗余,破坏工程的可维护性扩展性

    axios('http://www.kaifa.com/data', {
      // 配置代码
      method: 'GET',
      timeout: 3000,
      withCredentials: true,
      headers: {
        'Content-Type': 'application/json'
      },
      // 其他请求配置...
    })
    .then((data) => {
      // todo: 真正业务逻辑代码
      console.log(data);
    }, (err) => {
      // 错误处理代码  
      if (err.response.status === 401) {
      // handle authorization error
      }
      if (err.response.status === 403) {
      // handle server forbidden error
      }
      // 其他错误处理.....
      console.log(err);
    });
    
    • 环境区分
    • 请求头信息
    • 请求超时时间
      • timeout: 3000
    • 允许携带cookie
      • withCredentials: true
    • 响应结果处理
      • 登录校验失败
      • 无权限
      • 成功
    • ...

    三、Vue项目中如何封装axios

    axios文件封装在目录src/utils/https.js,对外暴露callApi函数

    1、环境区分

    callApi函数暴露prefixUrl参数,用来配置api url前缀,默认值为api

    // src/utils/https.js
    import axios from 'axios'
    
    export const callApi = (
      url,
      ...
      prefixUrl = 'api'
    ) => {
      if (!url) {
        const error = new Error('请传入url')
        return Promise.reject(error)
      }
      const fullUrl = `/${prefixUrl}/${url}`
      
      ...
      
      return axios({
        url: fullUrl,
        ...
      })
    }
    

    看到这里大家可能会问,为什么不用axios提供的配置参数baseURL,原因是baseURL会给每个接口都加上对应前缀,而项目实际场景中,存在一个前端工程,对应多个服务的场景。需要通过不用的前缀代理到不同的服务,baseURL虽然能实现,但是需要二级前缀,不优雅,且在使用的时候看不到真实的api地址是啥,因为代理前缀跟真实地址混合在一起了

    使用baseURL,效果如下

    面试官:Vue项目中有封装过axios吗?怎么封装的?

    函数设置prefixUrl参数,效果如下 面试官:Vue项目中有封装过axios吗?怎么封装的?

    利用环境变量webpack代理(这里用vuecli3配置)来作判断,用来区分开发、测试环境。生产环境同理配置nginx代理

    // vue.config.js
    const targetApi1 = process.env.NODE_ENV === 'development' ? "http://www.kaifa1.com" : "http://www.ceshi1.com"
    
    const targetApi2 = process.env.NODE_ENV === 'development' ? "http://www.kaifa2.com" : "http://www.ceshi2.com"
    module.exports = {
        devServer: {
            proxy: {
                '/api1': {
                    target: targetApi1,
                    changeOrigin: true,
                    pathRewrite: {
                        '/api1': ""
                    }
                },
                '/api2': {
                    target: targetApi2,
                    changeOrigin: true,
                    pathRewrite: {
                        '/api2': ""
                    }
                },
            }
        }
    }
    

    2、请求头

    常见以下三种

    (1)application/json

    参数会直接放在请求体中,以JSON格式的发送到后端。这也是axios请求的默认方式。这种类型使用最为广泛。

    面试官:Vue项目中有封装过axios吗?怎么封装的?

    (2)application/x-www-form-urlencoded

    请求体中的数据会以普通表单形式(键值对)发送到后端。

    面试官:Vue项目中有封装过axios吗?怎么封装的?

    (3)multipart/form-data

    参数会在请求体中,以标签为单元,用分隔符(可以自定义的boundary)分开。既可以上传键值对,也可以上传文件。通常被用来上传文件的格式。

    面试官:Vue项目中有封装过axios吗?怎么封装的? callApi函数暴露contentType参数,用来配置请求头,默认值为application/json; charset=utf-8

    看到这里大家可以会疑惑,直接通过options配置headers不可以嘛,答案是可以的,可以看到newOptions的取值顺序,先取默认值,再取配置的options,最后取contentType

    通过options配置headers,写n遍headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'};而通过contentType配置,传参json || urlencoded || multipart即可

    // src/utils/https.js
    import axios from 'axios'
    import qs from 'qs'
    
    const contentTypes = {
      json: 'application/json; charset=utf-8',
      urlencoded: 'application/x-www-form-urlencoded; charset=utf-8',
      multipart: 'multipart/form-data',
    }
    
    const defaultOptions = {
      headers: {
        Accept: 'application/json',
        'Content-Type': contentTypes.json,
      }
    }
    
    export const callApi = (
      url,
      data = {},
      options = {},
      contentType = 'json', // json || urlencoded || multipart
      prefixUrl = 'api'
    ) => {
    
      ...
      
      const newOptions = {
        ...defaultOptions,
        ...options,
        headers: {
          'Content-Type': contentTypes[contentType],
        },
      }
      
      const { method } = newOptions
    
      if (method !== 'get' && method !== 'head') {
        if (data instanceof FormData) {
          newOptions.data = data
          newOptions.headers = {
            'x-requested-with': 'XMLHttpRequest',
            'cache-control': 'no-cache',
          }
        } else if (options.headers['Content-Type'] === contentTypes.urlencoded) {
          newOptions.data = qs.stringify(data)
        } else {
          Object.keys(data).forEach((item) => {
            if (
              data[item] === null ||
              data[item] === undefined ||
              data[item] === ''
            ) {
              delete data[item]
            }
          })
          // 没有必要,因为axios会将JavaScript对象序列化为JSON
          // newOptions.data = JSON.stringify(data);
        }
      }
      
      return axios({
        url: fullUrl,
        ...newOptions,
      })
    }
    

    注意,在application/json格式下,JSON.stringify处理传参没有意义,因为axios会将JavaScript对象序列化为JSON,也就说说无论你转不转化都是JSON

    3、请求超时时间

    // src/utils/https.js
    const defaultOptions = {
      timeout: 15000,
    }
    

    4、允许携带cookie

    // src/utils/https.js
    const defaultOptions = {
      withCredentials: true,
    }
    

    5、响应结果处理

    通过axios响应拦截器处理

    这块需要跟服务端约定接口响应全局码,从而统一处理登录校验失败无权限成功等结果

    比如有些服务端对于登录校验失败无权限成功等返回的响应码都是200,在响应体内返回的状态码分别是20001,20002,10000,在then()中处理

    比如有些服务端对于登录校验失败无权限成功响应码返回401,403,200,在catch()中处理

    // src/utils/https.js
    import axios from 'axios'
    import { Message } from "element-ui";
    
    axios.interceptors.response.use(
    (response) => {
      const { data } = response
      if (data.code === 'xxx') { // 与服务端约定
        // 登录校验失败
      } else if (data.code === 'xxx') { // 与服务端约定
        // 无权限
        router.replace({ path: '/403' })
      } else if (data.code === 'xxx') { // 与服务端约定
        // 成功
        return Promise.resolve(data)
      } else {
        const { message } = data
        Message.error(message)
        return Promise.reject(data)
      }
    },
    (error) => {
      if (error.response) {
        const { data } = error.response
        const resCode = data.status
        const resMsg = data.message || '服务异常'
        // if (resCode === 401) { // 与服务端约定
        //     // 登录校验失败
        // } else if (data.code === 403) { // 与服务端约定
        //     // 无权限
        //     router.replace({ path: '/403' })
        // }
        Message.error(resMsg)
        const err = { code: resCode, respMsg: resMsg }
        return Promise.reject(err)
      } else {
        const err = { type: 'canceled', respMsg: '数据请求超时' }
        return Promise.reject(err)
      }
    }
    )
    

    上述方案在Message.error(xx)时,当多个接口返回的错误信息一致时,会存在重复提示的问题,如下图

    面试官:Vue项目中有封装过axios吗?怎么封装的?

    优化方案,利用防抖,实现错误提示一次,更优雅

    四、完整封装及具体使用

    可以访问github

    完成封装

    默认请求方式get,不同的请求方通过options参数传入

    // src/utils/https.js
    import axios from 'axios'
    import qs from 'qs'
    import { debounce } from './debounce'
    
    const contentTypes = {
      json: 'application/json; charset=utf-8',
      urlencoded: 'application/x-www-form-urlencoded; charset=utf-8',
      multipart: 'multipart/form-data',
    }
    
    function toastMsg() {
      Object.keys(errorMsgObj).map((item) => {
        Message.error(item)
        delete errorMsgObj[item]
      })
    }
    
    let errorMsgObj = {}
    
    const defaultOptions = {
      method: 'get',
      withCredentials: true, // 允许把cookie传递到后台
      headers: {
        Accept: 'application/json',
        'Content-Type': contentTypes.json,
      },
      timeout: 15000,
    }
    
    export const callApi = (
      url,
      data = {},
      options = {},
      contentType = 'json', // json || urlencoded || multipart
      prefixUrl = 'api'
    ) => {
      if (!url) {
        const error = new Error('请传入url')
        return Promise.reject(error)
      }
      const fullUrl = `/${prefixUrl}/${url}`
    
      const newOptions = {
        ...defaultOptions,
        ...options,
        headers: {
          'Content-Type': contentTypes[contentType],
        },
      }
    
      const { method } = newOptions
    
      if (method !== 'get' && method !== 'head') {
        if (data instanceof FormData) {
          newOptions.data = data
          newOptions.headers = {
            'x-requested-with': 'XMLHttpRequest',
            'cache-control': 'no-cache',
          }
        } else if (newOptions.headers['Content-Type'] === contentTypes.urlencoded) {
          newOptions.data = qs.stringify(data)
        } else {
          Object.keys(data).forEach((item) => {
            if (
              data[item] === null ||
              data[item] === undefined ||
              data[item] === ''
            ) {
              delete data[item]
            }
          })
          // 没有必要,因为axios会将JavaScript对象序列化为JSON
          // newOptions.data = JSON.stringify(data);
        }
      }
    
      axios.interceptors.request.use((request) => {
        // 移除起始部分 / 所有请求url走相对路径
        request.url = request.url.replace(/^\//, '')
        return request
      })
    
      axios.interceptors.response.use(
        (response) => {
          const { data } = response
          if (data.code === 'xxx') { // 与服务端约定
            // 登录校验失败
          } else if (data.code === 'xxx') { // 与服务端约定
            // 无权限
            router.replace({ path: '/403' })
          } else if (data.code === 'xxx') { // 与服务端约定
            return Promise.resolve(data)
          } else {
            const { message } = data
            if (!errorMsgObj[message]) {
              errorMsgObj[message] = message
            }
            setTimeout(debounce(toastMsg, 1000, true), 1000)
            return Promise.reject(data)
          }
        },
        (error) => {
          if (error.response) {
            const { data } = error.response
            const resCode = data.status
            const resMsg = data.message || '服务异常'
            // if (resCode === 401) { // 与服务端约定
            //     // 登录校验失败
            // } else if (data.code === 403) { // 与服务端约定
            //     // 无权限
            //     router.replace({ path: '/403' })
            // }
            if (!errorMsgObj[resMsg]) {
              errorMsgObj[resMsg] = resMsg
            }
            setTimeout(debounce(toastMsg, 1000, true), 1000)
            const err = { code: resCode, respMsg: resMsg }
            return Promise.reject(err)
          } else {
            const err = { type: 'canceled', respMsg: '数据请求超时' }
            return Promise.reject(err)
          }
        }
      )
    
      return axios({
        url: fullUrl,
        ...newOptions,
      })
    }
    
    // src/utils/https.js
    export const debounce = (func, timeout, immediate) => {
      let timer
    
      return function () {
        let context = this
        let args = arguments
    
        if (timer) clearTimeout(timer)
        if (immediate) {
          var callNow = !timer
          timer = setTimeout(() => {
            timer = null
          }, timeout)
          if (callNow) func.apply(context, args)
        } else {
          timer = setTimeout(function () {
            func.apply(context, args)
          }, timeout)
        }
      }
    }
    

    具体使用

    api管理文件在目录src/service下,index.js文件暴露其他模块,其他文件按功能模块划分文件

    面试官:Vue项目中有封装过axios吗?怎么封装的? 自定义前缀代理不同服务 面试官:Vue项目中有封装过axios吗?怎么封装的? 文件类型处理 面试官:Vue项目中有封装过axios吗?怎么封装的?

    五、总结

    axios封装没有一个绝对的标准,且需要结合项目中实际场景来设计,但是毋庸置疑,axios-ajax的封装是非常有必要的


    起源地下载网 » 面试官:Vue项目中有封装过axios吗?怎么封装的?

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元