阅读 594

HTTP系列:axios和fetch的二次封装处理

HTTP系列:AJAX基础梳理、axios基本使用梳理 (juejin.cn) 可以看一下前一篇关于前后端交互的基础知识。本篇文章重点放在前后端交互的封装处理上。

这篇文章的所有代码-github,可以对着完整的代码查看这篇文章更便于理解

前后端数据交互的几种方式汇总

前后端数据交互现在有两种常用的方式, XMLHttpRequestFetch ,另外一些跨域办法也可以进行交互

XMLHttpRequest (ajax请求)

  • $.ajax :jQuery中的请求库,其基于回调函数方式封装的ajax库
  • axios :基于Promise管理封装的ajax库

三种方式的使用:

 let xhr = new XMLHttpRequest;
xhr.open('GET', 'http://127.0.0.1:9999/user/list');
xhr.onreadystatechange = function () {
    if (xhr.readyState === 4 && xhr.status === 200) {
        console.log(xhr.responseText);
    }
};
xhr.send(); 
复制代码
$.ajax({
    url: 'http://127.0.0.1:9999/user/list',
    method: 'GET',
    dataType: 'json',
    success(data) {
        // data就是从服务器获取的结果
        console.log(data);
    }
}); 
复制代码
// 数据请求成功,会让返回的promise实例变为成功
axios.get('http://127.0.0.1:9999/user/list').then(response => {
    console.log(response.data);
}); 
复制代码

Fetch

Fetch是基于新的通信方案完成客户端和服务器端的数据交互(不是XMLHttpRequest)

  • ES6内置类
  • 默认是基于Promise管理异步编程
fetch('http://127.0.0.1:9999/user/list', {
    method: 'GET'
}).then(response => {
    // Response类的实例:text/json/blob/arrayBuffer方法 -> 返回的是新的promise实例「目的是把服务器返回的响应主体信息变为我们想要的格式数据」
    return response.json();//把返回的数据变成json格式,并且是Promise
}).then(data => {
    console.log(data);
});
复制代码

then() 中的 responseResponse 类的实例。其包含一些 text/json/blob/arrayBuffer 等方法。例如 response.json() 时把返回的数据变成 json 格式的对象,并且是Promise实例,然后我们可以继续使用 then 链进行进一步的处理

image。png

兼容性没有 XMLHttpRequest

某些跨域方案

某些跨域方案也可以实现前后端通信

  • proxy/cors(基于 XMLHttpRequest / Fetch 发送请求,但是可以实现跨域)
  • jsonp
  • postMesage + iframe

axios二次封装

axios基本使用: HTTP系列:AJAX基础梳理、axios基本使用梳理 (juejin。cn)

axios有三种方式去设置配置项

  • axios内部配置项,例如默认 method:'GET'
  • 通过 axios.defaults 设置的公共配置项,例如 axios.defaults.baseURL
  • 业务层发送请求时候传递的配置项
    axios.get( '',{
        params:{
            ...
        })
    复制代码

这三个优先级是递减的,即后面写的会覆盖前面的

对Axios的二次封装目的:把当前项目中,所有请求的公共部分进行统一处理。主要是两方面:

  • axios.defaults 设置公共的配置项
  • axios.interceptors 基于拦截器做统一处理

首先配置请求接口的统一前缀。在webpack环境里,我们可以根据环境变量的值,设置不同的前缀,来区分不同的环境,例如:


//   + 开发 development
//   + 测试 test
//   + 灰度 grayscale
//   + 生产 production
//npm run build/start/test/gray... 设置不同的环境变量
const env = process.env.NODE_ENV || 'development';
switch (env) {
    case 'development':
        axios.defaults.baseURL = 'http://127.0.0.1:9999';
        break;
    case 'test':
        axios.defaults.baseURL = 'http://168.1.123.1:9999';
        break;
    case 'production':
        axios.defaults.baseURL = 'http://api.zhufengpeixun.cn';
        break;
}
复制代码

使用模块化的方式进行封装。用一个文件来写封装axios的所有代码,然后将其导出。

相关的配置和注意事项都写在注释中

//http.js
axios.defaults.baseURL = 'http://localhost:9999';
// 设置超时时间{10S} & 设置跨域请求中是否携带资源凭证
axios.defaults.timeout = 10000;
axios.defaults.withCredentials = true;//默认允许携带资源凭证(后台有的时不允许携带资源凭证的,就要自己手动设置)
// 配置公共的自定义请求头信息  headers['common']/headers['post/get...']/headers/...
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
// POST系列请求对于请求主体信息的统一格式化
axios.defaults.transformRequest = function (data, headers) {
    //如果不是对象,直接返回,例如字符串
    if (data === null || typeof data !== "object") return data;
    let contentType = headers['Content-Type'] || headers.post['Content-Type'];
    //对urlencoded和json格式的body信息进行格式化
    if (contentType.includes('urlencoded')) return Qs.stringify(data);
    if (contentType.includes('json')) return JSON.stringify(data);
    return data;
};
// 设置响应状态码的校验处理{规定服务器返回的状态码哪些是算请求成功,哪些算失败}
axios.defaults.validateStatus = function (status) {
    return status >= 200 && status < 400;
};

// 请求拦截器,当所有配置处理完,在向服务器发送请求之前,我们拦截到现有的配置,再去做一些统一修改
axios.interceptors.request.use(function (config) {
    // 例如:传递Token
    /* const token = sessionStorage.getItem('token');
    if (token) {
        config.headers['Authorization'] = token;
    } */
    return config;
});

// 响应拦截器,当前请求有结果之后,我们在业务层自己调用then/catch方法之间拦截一下,这样可以做一些成功或者失败的统一提示处理等...
axios.interceptors.response.use(function onfulfilled(response) {
    // 成功:服务器正常返回结果 & validateStatus状态码校验成功
    //这里可以做一些与本公司业务有关的判断,例如错误的统一提示等
    //。。。
    return response.data;
}, function onrejected(reason) {
    // 失败:@1服务器返回了结果但是状态码没有经过validateStatus校验 || @2服务器压根没有返回任何的结果 || @3请求中断或者超时...
    let response = reason.response;
    if (response) {
        // @1
        switch (response.status) {
            case 401:
                break;
                // ...
        }
    } else {
        if (reason && reason.code === 'ECONNABORTED') {
            // @3
        }
        if (!navigator.onLine) {
            // @2
        }
    }
    return Promise.reject(reason);//保证promise到业务层还是失败的
});

export default axios;
复制代码

测试使用:

<!--index。html-->

<!--这里不是webpack环境,得加上完整的路径-->
<script src="node_modules/axios/dist/axios.min.js"></script>
<script src="node_modules/qs/dist/qs.js"></script>

<!-- type="module" 可以在JS中使用ES6Module模块管理规范{import & export} -->
<script type="module" src="index.js"></script>
复制代码

这里index.js type'module' 是为了可以使用import语法导入基于 export / export default 导出的模块

//index.js
import axios from './http.js';
axios.get('/user/list').then(data => {
    console.log('axios1',data);
});
axios.get('/home',{//自己进行一些配置
    baseURL:'http://127.0.0.1:8888',
    withCredentials:false//有的后台不允许携带资源凭证
}).then(data => {
    console.log('axios2',data);
});
axios.post('/user/login', {
    account: '137000000',
    password: '11111111'
}).then(data => {
    console.log(data);
});
复制代码

拦截器的原理:

相当于 axios.get('/user/list').then(data => {}); 在这个 then 前面 then 执行 .then(onfufilled,onrejected) ,里面的两个方法就是拦截器传入的方法,先走拦截器里的两个方法,再走自己写的 then 方法

对业务层的封装处理

上面那样封装还不够,如果业务层有一些共同的逻辑需要处理,那么还时需要再对业务层进行一层封装,对业务层的成功和失败或者特殊要求进行统一的提示和处理

//request.js
/* 对业务层的一些处理 */
import axios from './http.js';

const handle = function handle(data) {
    let code = +data.code;//公司内部本身自己进行的规定
    if (code === 0) return data;
    // 业务层失败:也可以做一些统一提示或者处理
    // ...
    return Promise.reject(data.codeText);
};

const requestGET = function requestGET(url, options) {
    return axios.get(url, options).then(handle);
};

const requestPOST = function requestPOST(url, data, options) {
    return axios.post(url, data, options).then(handle);
};

export default {
    requestGET,
    requestPOST
};
复制代码

多个请求配置处理封装成不同的实例

如果一个项目中100个请求90多个都是统一的,只有极个别几个是不一样的,那么我们只需要发送请求时候单独改一些配置即可。

如果一个项目中100个请求60多个都是一个配置,另外40多个是其他配置,那么这种情况应该怎么做呢?

我们可以创建axios的多个实例,让每个实例都拥有不同的配置,例如:

//http_file.js
import axios from "./http.js";

// 创建一个和axios类似的相同实例instance
const instance = axios.create();
instance.defaults.baseURL = '';
// ...


export default instance;
复制代码

这样用到其他配置的axios就可以单独写了,不会和公共配置项冲突。

fetch二次封装

具体的逻辑写在了注释中

//对Fetch的封装:让其支持params/请求主体的格式化/请求地址的公共前缀 

/* const env = process.env.NODE_ENV || 'development',
    baseURL = '';
switch (env) {
    case 'development':
        baseURL = 'http://127.0.0.1:9999';
        break;
    case 'test':
        baseURL = 'http://168.1.123.1:9999';
        break;
    case 'production':
        baseURL = 'http://api.zhufengpeixun.cn';
        break;
} */

// 公用前缀 & 默认配置
let baseURL = 'http://127.0.0.1:9999',
    inital = {
        method: 'GET',
        params: null,
        body: null,
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        credentials: true,
        responseType: 'JSON',
        cache: 'no-cache'
    };

// 校验是否为纯粹的对象
const isPlainObject = function isPlainObject(obj) {
    var proto, Ctor;
    if (!obj || typeof obj !== "object") return false;
    proto = Object.getPrototypeOf(obj);
    if (!proto) return true;
    Ctor = proto.hasOwnProperty('constructor') && proto.constructor;
    return typeof Ctor === "function" && Ctor === Object;//构造函数是Object
};

// 发送数据请求
const request = function request(url, config) {
    // 合并配置项{不要去更改inital中的内容}
    (config == null || typeof config !== "object") ? config = {}: null;//确保config肯定是对象
    if (config.headers && isPlainObject(config.headers)) {
        // 单独的给HEADERS先进行深度合并
        config.headers = Object.assign({}, inital.headers, config.headers);
    }
    let {
        method,
        params,
        body,
        headers,
        credentials,
        responseType,
        cache
    } = Object.assign({}, inital, config);//和饼config

    // 处理URL{格式校验 & 公共前缀 & 拼接params中的信息到URL的末尾}
    if (typeof url !== "string") throw new TypeError( ` ${url} is not an string! ` );
    if (!/^http(s?):///i.test(url)) url = baseURL + url;//判断是不是以http或者https开头,如果不是,就用baseurl拼起来
    if (params != null) {//不是null和undefined,存在params
        if (isPlainObject(params)) {
            params = Qs.stringify(params);
        }
        url +=  ` ${url.includes('?')?'&':'?'}${params} ` ;//拼接
    }

    // 处理请求主体的数据格式{根据headers中的Content-Type处理成为指定的格式}
    if (body != null) {
        if (isPlainObject(body)) {
            let contentType = headers['Content-Type'] || 'application/json';//默认application/json
            if (contentType.includes('urlencoded')) body = Qs.stringify(body);
            if (contentType.includes('json')) body = JSON.stringify(body);
        }
    }

    // 处理credentials{如果传递的是true,我们让其为include,否则是same-origin}
    //include,允许跨域请求当中携带资源凭证,same-origin,允许同源性请求当中携带资源凭证
    credentials = credentials ? 'include' : 'same-origin';

    // 基于fetch请求数据
    method = method.toUpperCase();
    responseType = responseType.toUpperCase();
    config = {
        method,
        credentials,
        cache,
        headers
    };
    /^(POST|PUT|PATCH)$/i.test(method) ? config.body = body : null;
    return fetch(url, config).then(function onfulfilled(response) {
        // 走到这边不一定是成功的:
        // Fetch的特点的是,只要服务器有返回结果,不论状态码是多少,它都认为是成功
        let {
            status,
            statusText
        } = response;
        if (status >= 200 && status < 400) {
            // 真正成功获取数据
            let result;
            switch (responseType) {
                case 'TEXT':
                    result = response.text();
                    break;
                case 'JSON':
                    result = response.json();
                    break;
                case 'BLOB':
                    result = response.blob();
                    break;
                case 'ARRAYBUFFER':
                    result = response.arrayBuffer();
                    break;
            }
            return result;
        }
        // 应该是失败的处理
        return Promise.reject({
            code: 'STATUS ERROR',
            status,
            statusText
        });
    }).catch(function onrejected(reason) {
        // @1:状态码失败
        if (reason && reason.code === "STATUS ERROR") {
            switch (reason.status) {
                case 401:
                    break;
                    // ...
            }
        }

        // @2:断网
        if (!navigator.onLine) {
            // ...
        }

        // @3:处理返回数据格式失败
        // ...

        return Promise.reject(reason);
    });
};
export default request;
复制代码

测试

//fetch
//get
request('/user/list').then(data => {
    console.log(data);
});
//post
request('/user/login', {
    method: 'POST',
    body: {
        account: '137000000',
        password: '1234567890'
    }
}).then(data => {
    console.log(data);
});
复制代码

这篇文章的所有代码-github

文章分类
前端
文章标签