pengkobe / reading-notes

:stars: to record daily reading notes. I build an issue blog to record daily FE study notes. suggestion and comments are welcomed.
https://github.com/pengkobe/reading-notes/issues
MIT License
13 stars 1 forks source link

如何借助 Axios 实现 HTTP 请求库[译] #449

Open pengkobe opened 6 years ago

pengkobe commented 6 years ago

之前都没有翻译过成篇的文章,这次借阮博士的分享的这篇文章,小小的实践一下,所以难免有很多疏漏,得好好加油,继续努力。

原文地址: How to Implement an HTTP Request Library with Axios

背景

在前端开发过程中,我们经常会遇到发送异步请求的场景,而使用 HTTP 库可以很大程度上减少我们开发成本以及提高开发效率。
Axios 近年来普遍使用的 HTTP 库,如今,在 github 上有奖金 4 万个 star,很多大牛也在极力推广使用这个库。 所以,我们有必要了解 Axios 是如何设计的,当作者发布这篇文章的时候,Axios 的最新版本是 0.18.0 ,文章里的代码示例和分析也会基于这个版本,它的源代码位于 lib/ 目录下,文中代码的引用路径也是基于这个文件夹。
这篇文章将涵盖以下内容

如何使用 Axios

为了理解 Axios 的设计,我们首先得学会怎么使用,以下是几个简单的使用示例

发送请求

axios({
  method:'get',
  url:'http://bit.ly/2mTM3nY',
  responseType:'stream'
})
  .then(function(response) {
  response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'))
});

这个是官方的示例代码,我们可以看到,其使用起来跟 jQuery ajax 有很大的相似之处,都返回了一个 Promise 对象来进行处理( 其也可以使用成功回调,不过更建议使用 Promise 和 await )

这个示例很简单,这里也不多说,接下来,我们来看看如何添加一个拦截函数

添加拦截器

// 客户端请求拦截器
axios.interceptors.request.use(function (config) {
    // 发起请求前
    return config;
  }, function (error) {
    // 请求错误处理
    return Promise.reject(error);
  });

// 服务端返回拦截器
axios.interceptors.response.use(function (response) {
    // 处理返回数据
    return response;
  }, function (error) {
    // 返回错误处理
    return Promise.reject(error);
  })

从上述代码,我们可以知道,我们可以在请求正式发送前修改配置参数,也可以在请求返回后对数据执行特定的操作,当然,我们也可以对错误进行处理。

取消 HTTP 请求

当我们开发查询相关的模块时,我们经常需要频繁的发起请求,通常情况下,我们需要在发起新的请求前取消掉上一次发起的请求,这无疑是 axios 的一个优势,axios 取消请求的代码示例如下

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function(thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // handle error
  }
});

axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token
})

// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');

从上诉代码我们可以看到有一个叫作 CancelToken 的变量,借助其可以实现请求取消,不过这种方式已经被官方舍弃了,具体原因我们稍后会提到。

axios 核心模块时如何设计实现的?

通过上述案例分析,大家已经清楚如何使用 axios 了,接下来,我们会分析 axios 核心模块的设计与实现方式,下述图片涵盖了这篇文章中有分析到的模块,如果你有兴趣,也可以直接从 github 上下载 axios 源码研究,能够让你对相关模块有更深的了解。

HTTP request 模块

request 模块位于 core/dispatchReqeust.js文件. 这里我选取了相关片段做一个简要介绍

module.exports = function dispatchRequest(config) {
    throwIfCancellationRequested(config);

    // 其它代码(略)

    // 这个适配器能够自动根据环境选择发起请求的方式,选择 Node 或 XHR
    var adapter = config.adapter || defaults.adapter; 

    return adapter(config).then(function onAdapterResolution(response) {
        throwIfCancellationRequested(config);

         // 其它代码(略)

        return response;
    }, function onAdapterRejection(reason) {
        if (!isCancel(reason)) {
            throwIfCancellationRequested(config);

            // 其它代码(略)

            return Promise.reject(reason);
        });
};

从上述代码,我们可以知道 dispatchRequest 方法是通过 config.adapter 来实现请求发送的,我们也可以通过传一个符合规范的适配器方法替换原生的模块( 当然,我们并不是真的这么做,只是说这种解耦方式还是不错的 )

在 default.js 相关文件中, 我们可以看到适配器的选择逻辑,其是根据一些特殊变量以及当前容器的一些构造函数来进行判断的。

function getDefaultAdapter() {
    var adapter;
    if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
        // Node 端
        adapter = require('./adapters/http');
    } else if (typeof XMLHttpRequest !== 'undefined') {
        //  浏览器端
        adapter = require('./adapters/xhr');
    }
    return adapter;
}

Interceptor 模块

现在,让我们来看看 axios 是如何实现请求和返回的拦截器方法的,先让我们看看公布的相关接口,以 request 函数为例

Axios.prototype.request = function request(config) {

    // 其它代码

    var chain = [dispatchRequest, undefined];
    var promise = Promise.resolve(config);

    this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
        chain.unshift(interceptor.fulfilled, interceptor.rejected);
    });

    this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
        chain.push(interceptor.fulfilled, interceptor.rejected);
    });

    while (chain.length) {
        promise = promise.then(chain.shift(), chain.shift());
    }

    return promise;
};

这个函数是 axios 发送请求的接口,因为函数的实现比较长,这里我只做简要的说明
chain 是执行队列,初始值是带有配置参数的 Promise ,在chain 队列里,第一个值是 dispatchReqeust,其用来发送请求,第二值是 undefined,相对于 dispatchReqeust,那么 undefined 的作用是什么呢?其主要是为了作为 Promise 中成功和失败回调函数的默认值,从代码 promise = promise.then(chain.shift(), chain.shift());中我们也可以看到,因此 dispatchReqeust 和 undefined 可以看做是一对函数。

在chain 队列里,dispatchReqeust 函数用于发送请求,在此之前,使用 unshift 插入了一个请求拦截器,在此之后,使用 push 在后方插入了一个,需要注意的是,这两个拦截器一般是成对出现的。 从上述代码,我们粗略的知道了拦截器的使用,接下来,我们学习下如何取消请求。

Cancel-request 模块

请求取消模块代码位于 Cancel/文件夹, 让我们看看其中的关键代码。 首先,让我们瞧一瞧 Cancel这个类,这个类主要用于记录取消状态,具体代码如下

function Cancel(message) {
  this.message = message;
}

Cancel.prototype.toString = function toString() {
  return 'Cancel' + (this.message ? ': ' + this.message : '');
};

Cancel.prototype.__CANCEL__ = true;

在 CancelToken 中,其通过传递一个 Promise 对象实现了 HTTP 请求取消 ,具体代码如下

function CancelToken(executor) {
    if (typeof executor !== 'function') {
        throw new TypeError('executor must be a function.');
    }

    var resolvePromise;
    this.promise = new Promise(function promiseExecutor(resolve) {
        resolvePromise = resolve;
    });

    var token = this;
    executor(function cancel(message) {
        if (token.reason) {
            // cancle 方法已经调用了
            return;
        }

        token.reason = new Cancel(message);
        resolvePromise(token.reason);
    });
}

CancelToken.source = function source() {
    var cancel;
    var token = new CancelToken(function executor(c) {
        cancel = c;
    });
    return {
        token: token,
        cancel: cancel
    };
};

相应的 cancel-request 代码位于 adapter/xhr.js 文件中

if (config.cancelToken) {
    // 等待 cancel 方法调用
    config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
            return;
        }

        request.abort();
        reject(cancel);
        // 重置请求
        request = null;
    });
}

通过对以上代码的分析,我们简要总结下取消请求代码的实现逻辑

axios 的优势是什么

发送请求

如前面所说,axios 并没有特殊对待请求发送方法 dispatchRequest,而是把其放到了队列的中间来保证处理的一致性,同时也提高了代码的可读性。

适配器

在适配器处理逻辑中,http 和 xhr 模块并没有嵌入到 dispatchRequest 中,而是在 default.js 文件中通过配置方法进行引入,这不仅让模块很好的进行解耦,也使得未来用户自定义自己的适配器成为可能。

取消请求

在取消请求逻辑中,axios 使用 Promise 作为触发器,传递 resolve 函数到回调函数中作为外部参数,这不仅保证了内部逻辑的一致性,也保证了在取消发送请求时,不需要直接改变相关类的数据,从而很大程度上避免了侵入其它模块。

总结

这篇文章详细介绍了 axios 的使用、设计思想以及实现方法,阅读后,你能够知道 axios 是如何设计的以及了解到模块之间的封装与交互。
当然,这篇文章也仅仅是介绍了核心模块的相关的内容,如果你对其它代码有兴趣,可以直接去 github阅读相关代码。

有任何问题和建议,欢迎留言。