fetch

Posted by CodingWithAlice on November 21, 2022

fetch、axios、ajax - 有关中止请求

参考文章:ajax和axios、fetch的区别MDN - AbortControllerMDN - AbortSignal

背景:

有一个查询接口,响应时间很长(5min)左右,所以需要提供一个用户不愿意等待时的「中止」按钮

知识学习:

一开始我们用的 XHRHttpRequest【传统 ajax】
-> jQuery 封装原生 XHR + 增添了对JSONP的支持,我们开始使用【JQuery ajax】

缺点:

1.本身是针对MVC的编程,不符合现在前端MVVM的浪潮 2.基于原生的XHR开发,XHR本身的架构不清晰。 3.JQuery整个项目太大,单纯使用ajax却要引入整个JQuery非常的不合理(采取个性化打包的方案又不能享受CDN服务) 4.不符合关注分离(Separation of Concerns)的原则 5.配置和调用方式非常混乱,而且基于事件的异步模型不友好

-> 本质上也是对原生XHR的封装,但是是 Promise 实现的版本,基于Promise 用于浏览器和 nodejs 的 HTTP 客户端【axios】

特性:

1.从浏览器中创建 XMLHttpRequest 2.支持 Promise API 3.客户端支持防止CSRF 4.提供了一些并发请求的接口(重要,方便了很多的操作) 5.从 node.js 创建 http 请求 6.拦截请求和响应 7.转换请求和响应数据 8.取消请求 9.自动转换JSON数据

-> fetch 是基于 Promise 设计的,但是不是 ajax 的进一步封装,是用原生 js 实现的,没有用 XHR 对象【fetch】

优点:

  1. 语法简洁,更加语义化
  2. 基于标准 Promise 实现,支持 async/await
  3. 同构方便,使用 isomorphic-fetch
  4. 更加底层,提供的API丰富(request, response)
  5. 脱离了XHR,是ES规范里新的实现方式

缺点:

1)fetch只对网络请求报错,对400,500都当做成功的请求,服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。

2)fetch默认不会带cookie,需要添加配置项: fetch(url, {credentials: ‘include’})

3)fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费

4)fetch没有办法原生监测请求的进度,而XHR可以

针对 fetch 使用的封装:

async function fetchBase(apiurl, options) {
    let url = apiurl;
    let opt = options;

    opt = opt || {};
    opt.method = opt.method || 'GET';
    opt.credentials = opt.credentials || 'include';
    opt.withCredentials = true;

    if (opt.headers) {
        Object.assign(opt.headers, {
            Accept: 'application/json'
        });
    }

    if (opt.query) {
        // 语法:querystring.stringify(obj, [sep=‘&’], [eq=‘=’])
        // 作用:将对象转换成字符串,默认用 & 连接多个参数,用 = 连接键值
        // 例如:将{ foo: 'bar', baz: ['qux', 'quux'], corge: '' } 转换为-->'foo=bar&baz=qux&baz=quux&corge=' 
        url = `${url}?${queryString.stringify(opt.query)}`;
    }

    // 防止同步render报错
    setImmediate(() => Progress.start());

    const res = await fetch(url, opt);

    try {
        let json = { code: 200 };

        if (opt.raw) {
            Progress.done();
            return res.text();
        }

        // 如果是HEAD请求,说明不需要返回数据
        if (!opt.method.toUpperCase() !== 'HEAD') {
            json = await parseJSON(res);
        }

        if (json.code === 200 || json.resultCode === 200 || json.resultCode === 0) {
            Progress.done();
            return {
                ...json,
                // 有些场合数据是在HEADER中的
                _headers: res.headers
            };
        }

        // if (json.code === 401 || json.code === 301) {
        //     Progress.done();
        //     // 未登陆的情况下,跳转登陆页, 这一步在Layout中处理
        //     // window.location.href = '/login';
        //     return false;
        // }

        throw json;
    } catch (err) {
        Progress.done();

        if (err.code === 302 || err.resultCode === 302) {
            // 状态码为 302 时跳转至登录页
            message.error('用户未登录或登录状态失效,请登录后再试');
            setTimeout(() => {
                // encodeURIComponent用于编码字符为UTF-8的编码,比encodeURI编码更多字符,例如?=&
                window.location.href = `${api.login}?redirect=${encodeURIComponent(window.location.href)}`;
            }, 1000);
        } else if (err.code === 301 || err.resultCode === 301) {
            // 状态码为 301 时提示无权限
            message.error('无操作权限,请联系系统管理员');
        } else if (!opt.noCommonTip && err.name !== 'AbortError') {
            // 其他错误情况,统一toast提示后端msg 信息
            const errorMsg = err.message || err.msg || err.errMsg || defaultErrorMessage;
            message.error(errorMsg);
        }

        throw err;
    }

    // return false 可以在对应model通过if(data)来判断是否取到值
    // return false;
}

针对可以中止的 fetch 的封装:

const fetchAbortWrapper = (url, options) => {
    const aborter = new AbortController();
    const req = fetchBase(url, {
        ...options,
        signal: options.signal || aborter.signal,
    });
    req.abort = () => aborter.abort();
    return req;
};