• 封装axios


    前言

    axios 是一个轻量的HTTP客户端,它基于 XMLHttpRequest 服务来执行 HTTP 请求,支持丰富的配置,支持 Promise,支持浏览器端和 Node.js 端。自Vue2.0起,尤大大(Vue作者尤雨溪)宣布取消对 vue-resource 的官方推荐,转而推荐 axios。现在 axios 已经成为大部分 Vue 开发者的首选。
    (如果你还不熟悉 axios,可以在这里查看它的API)。

    axios 的API很友好,你完全可以很轻松地在项目中直接使用。不过随着项目规模增大,如果每发起一次HTTP请求,就要把这些比如设置超时时间、设置请求头、根据项目环境判断使用哪个请求地址、错误处理等等操作,都就地写一遍,得疯!这种重复劳动不仅浪费时间,而且让代码变得冗余不堪,难以维护。

    为了提高我们的代码质量,我们应该在项目中二次封装一下 axios 再使用。

    那么,怎么封装 axios 呢?

    原来的样子

    封装前,先来看下,不封装的情况下,一个实际项目中axios请求的样子。大概是长这样:

    axios('http://localhost:3000/data', {
      method: 'GET',
      timeout: 1000,
      withCredentials: true,
      headers: {
        'Content-Type': 'application/json',
        Authorization: 'xxx',
      },
      transformRequest: [function (data, headers) {
        return data;
      }],
      // 其他请求配置...
    })
    .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);
    });
    

    可以看到在这段代码中,页面代码逻辑只在第15行处,上方的一大块请求配置代码和下方一大块响应错误处理代码,几乎跟页面功能没有关系,而且每个请求中这些内容都差不多,甚至有的部分完全一样。想象一下,每发一次请求都来这么一下,十几个请求一写,会是什么盛况?

    封装步骤

    封装的本质就是在待封装的内容外面添加各种东西,然后把它们作为一个新的整体呈现给使用者,以达到扩展和易用的目的。

    封装axios要做的事情,就是把所有HTTP请求共用的配置,事先都在axios上配置好,预留好必要的参数和接口,然后把它作为新的axios返回。

    接下来我们借助一个demo实现一个具有良好扩展性的axios封装。

    demo目录结构如下(由Vue-cli 3.0 生成):

    |--public/
    |--mock/
    |   |--db.json  # 我新建的接口模拟数据
    |--src/
    |   |--assets/
    |   |--components/
    |   |--router/
    |   |--store/
    |   |--views/
    |       |--Home.Vue
    |   |--App.vue
    |   |--main.js
    |   |--theme.styl
    |--package.json
    |...
    

    封装目标

    我希望在 Home 页,发起 axios 请求时就像调用一个只有少量参数的方法一样简单,这样我就可以专注业务代码了。

    1. 将 axios 封装到一个独立的文件

    • 在src下创建 utils/http.js 文件
    cd src
    mkdir utils
    touch http.js
    
    • 引入 axios
    // src/utils/http.js
    
    import axios from 'axios';
    
    • 创建一个类
      你也可以用函数来封装,我只是觉得类更语义化而已。
    //src/utils/http.js
    
    //...
    class NewAxios {
    
    }
    
    • 给不同环境配置不同请求地址
      根据 process.env.NODE_ENV 配置不同的 baseURL,使项目只需执行相应打包命令,就可以在不同环境中自动切换请求主机地址。
    // src/utils/http.js
    
    //...
    const getBaseUrl = (env) => {
      let base = {
        production: '/',
        development: 'http://localhost:3000',
        test: 'http://localhost:3001',
      }[env];
      if (!base) {
        base = '/';
      }
      return base;
    };
    
    class NewAxios {
      constructor() {
        this.baseURL = getBaseUrl(process.env.NODE_ENV);
      }
    }
    
    • 配置超时时间
      timeout属性,我一般设置10秒。
    // src/utils/http.js
    
    //...
    class NewAxios {
      constructor() {
        //...
        this.timeout = 10000;
      }
    }
    
    • 配置允许携带凭证
      widthCredentials属性设为true。
    // src/utils/http.js
    
    //...
    class NewAxios {
      constructor() {
        //...
        this.withCredentials = true;
      }
    }
    
    • 给这个类创建实例上的方法request
      request 方法里,创建新的axios实例,接收请求配置参数,处理参数,添加配置,返回axios实例的请求结果(一个promise对象)。
      你也可以不创建,直接使用默认导出的axios实例,然后把所有配置都放到它上面,不过这样一来整个项目就会共用一个axios实例。虽然大部分项目下这样够用没问题,但是有的项目中不同服务地址的请求和响应结构可能完全不同,这个时候共用一个实例就没办法支持了。所以为了封装可以更通用,更具灵活性,我会使用axios的create方法,使每次发请求都是新的axios实例。
    // src/utils/http.js
    
    //...
    class NewAxios {
      //...
      request(options) {
        // 每次请求都会创建新的axios实例。
        const instance = axios.create();
        const config = { // 将用户传过来的参数与公共配置合并。
          ...options,
          baseURL: this.baseURL,
          timeout: this.timeout,
          withCredentials: this.withCredentials,
        };
        // 配置拦截器,支持根据不同url配置不同的拦截器。
        this.setInterceptors(instance, options.url);
        return instance(config); // 返回axios实例的执行结果
      }
    }
    

    因为拦截器配置内容比较多,所以封装成一个内部函数了。

    • 配置请求拦截器
      在发送请求前对请求参数做的所有修改都在这里统一配置。比如统一添加token凭证、统一设置语言、统一设置内容类型、指定数据格式等等。做完后记得返回这个配置,否则整个请求不会进行。
      我这里就配置一个token。
    // src/utils/http.js
    
    //...
    class NewAxios {
      //...
      // 这里的url可供你针对需要特殊处理的接口路径设置不同拦截器。
      setInterceptors = (instance, url) => { 
        instance.interceptors.request.use((config) => { // 请求拦截器
          // 配置token
          config.headers.AuthorizationToken = localStorage.getItem('AuthorizationToken') || '';
          return config;
        }, err => Promise.reject(err));
      }
      //...
    }
    
    • 配置响应拦截器
      在请求的thencatch处理前对响应数据进行一轮预先处理。比如过滤响应数据,更多的,是在这里对各种响应错误码进行统一错误处理,还有断网处理等等。
      我这里就判断一下403和断网。
    // src/utils/http.js
    
    //...
    class NewAxios {
      //...
      setInterceptors = (instance, url) => {
        //...
        instance.interceptors.response.use((response) => { // 响应拦截器
          // todo: 想根据业务需要,对响应结果预先处理的,都放在这里
          console.log();
          return response;
        }, (err) => {
          if (err.response) { // 响应错误码处理
            switch (err.response.status) {
              case '403':
                // todo: handler server forbidden error
                break;
                // todo: handler other status code
              default:
                break;
            }
            return Promise.reject(err.response);
          }
          if (!window.navigator.online) { // 断网处理
            // todo: jump to offline page
            return -1;
          }
          return Promise.reject(err);
        });
      }
      //...
    }
    

    另外,在拦截器里,还适合放置loading等缓冲效果:在请求拦截器里显示loading,在响应拦截器里移除loading。这样所有请求就都有了一个统一的loading效果。

    • 默认导出新的实例
    // src/utils/http.js
    
    //...
    export default new NewAxios();
    

    最后完整的代码如下:

    // src/utils/http.js
    
    import axios from 'axios';
    
    const getBaseUrl = (env) => {
      let base = {
        production: '/',
        development: 'http://localhost:3000',
        test: 'http://localhost:3001',
      }[env];
      if (!base) {
        base = '/';
      }
      return base;
    };
    
    class NewAxios {
      constructor() {
        this.baseURL = getBaseUrl(process.env.NODE_ENV);
        this.timeout = 10000;
        this.withCredentials = true;
      }
    
      setInterceptors = (instance, url) => {
        instance.interceptors.request.use((config) => {
          // 在这里添加loading
          // 配置token
          config.headers.AuthorizationToken = localStorage.getItem('AuthorizationToken') || '';
          return config;
        }, err => Promise.reject(err));
    
        instance.interceptors.response.use((response) => {
          // 在这里移除loading
          // todo: 想根据业务需要,对响应结果预先处理的,都放在这里
          return response;
        }, (err) => {
          if (err.response) { // 响应错误码处理
            switch (err.response.status) {
              case '403':
                // todo: handler server forbidden error
                break;
                // todo: handler other status code
              default:
                break;
            }
            return Promise.reject(err.response);
          }
          if (!window.navigator.online) { // 断网处理
            // todo: jump to offline page
            return -1;
          }
          return Promise.reject(err);
        });
      }
    
      request(options) {
        // 每次请求都会创建新的axios实例。
        const instance = axios.create();
        const config = { // 将用户传过来的参数与公共配置合并。
          ...options,
          baseURL: this.baseURL,
          timeout: this.timeout,
          withCredentials: this.withCredentials,
        };
        // 配置拦截器,支持根据不同url配置不同的拦截器。
        this.setInterceptors(instance, options.url);
        return instance(config); // 返回axios实例的执行结果
      }
    }
    
    export default new NewAxios();
    

    现在 axios 封装算是完成了80%。我们还需要再进一步把axios和接口结合再封装一层,才能达到我在一开始定的封装目标。

    2. 使用新的 axios 封装API

    • 在 src 目录下新建 api 文件夹。把所有涉及HTTP请求的接口统一集中到这个目录来管理。
    • 新建 home.js。我们需要把接口根据一定规则分好类,一类接口对应一个js文件。这个分类可以是按页面来划分,或者按模块等等。为了演示更直观,我这里就按页面来划分了。实际根据自己的需求来定。
    • 使用新的 axios 封装API(固定url的值,合并用户传过来的参数),然后命名导出这些函数。
    // src/api/home.js 
    
    import axios from '@/utils/http';
    export const fetchData = options => axios.request({
      ...options,
      url: '/data',
    });
    export default {};
    
    • 在 api 目录下新建 index.js,把其他文件的接口都在这个文件里汇总导出。
    // src/api/index.js
    
    export * from './home';
    

    这层封装将我们的新的axios封装到了更简洁更语义化的接口方法中。

    现在我们的目录结构长这样:

    |--public/
    |--mock/
    |   |--db.json  # 接口模拟数据
    |--src/
    |   |--api/     # 所有的接口都集中在这个目录下
    |       |--home.js  # Home页面里涉及到的接口封装在这里
    |       |--index.js # 项目中所有接口调用的入口
    |   |--assets/
    |   |--components/
    |   |--router/
    |   |--store/
    |   |--utils/
    |       |--http.js  # axios封装在这里
    |   |--views/
    |       |--Home.Vue
    |   |--App.vue
    |   |--main.js
    |   |--theme.styl
    |--package.json
    |...
    

    使用封装后的axios

    现在我们要发HTTP请求时,只需引入 api 下的 index.js 文件就可以调用任何接口了,并且用的是封装后的 axios

    // src/views/Home.vue
    
    <template>
      <div class="home">
        <h1>This is home page</h1>
      </div>
    </template>
    
    <script>
    // @ is an alias to /src
    import { fetchData } from '@/api/index';
    
    export default {
      name: 'home',
      mounted() {
        fetchData()  // axios请求在这里
          .then((data) => {
            console.log(data);
          })
          .catch((err) => {
            console.log(err);
          });
      },
    };
    </script>
    

    axios请求被封装在fetchData函数里,页面请求压根不需要出现任何axios API,悄无声息地发起请求获取响应,就像在调用一个简单的 Promise 函数一样轻松。并且在页面中只需专注处理业务功能,不用被其他事物干扰。

    运行

    运行 npm run serve 启动项目,执行 npm run mock 启动服务mock接口。

    现在打开 localhost:8080 可以看到home页面。打开浏览器控制台,可以看到打印的请求响应结果:

    wrap-axios.jpg

    简洁,优雅。

    总结

    1. 封装思想是前端技术中很有用的思想,简单的axios及接口封装,就可以让我们可以领略到它的魅力。
    2. 封装 axios 没有一个绝对的标准,只要你的封装可以满足你的项目需求,并且用起来方便,那就是一个好的封装方案。
    3. BTW:以上封装给大家提供了一个封装好的axios和api框架,经过以上过程封装好的 axios,可以不局限于 Vue,React 项目同样可以拿去使用,它适用任何前端项目。

    本文的代码可以在这里获取:https://github.com/yc111/wrap-axios

    欢迎交流~

    欢迎转载,转载请注明出处:
    https://champyin.com/2019/12/23/封装axios/

  • 相关阅读:
    如何实现一个教师与学生教学辅助平台?
    面向过程(或者叫结构化)分析方法与面向对象分析方法到底区别在哪里?请根据自己的理解简明扼要的回答。
    2..移动APP开发使用什么样的原型设计工具比较合适?
    new delete和malloc free的区别
    char * 和char[]的区别以及怎样与string类型进行转换
    浅谈const的基本用法
    c++ map按key或value的值分别进行排序
    二叉树及先序,中序,后序遍历
    c++发展趋势
    markdown 的基本操作
  • 原文地址:https://www.cnblogs.com/champyin/p/12115617.html
Copyright © 2020-2023  润新知