axios 对外出口API是如何设计的?

本文是"axios源码系列"第六篇,你可以查看以下链接了解过去的内容。

  1. axios 是如何实现取消请求的?
  2. 你知道吗?axios 请求是 JSON 响应优先的
  3. axios 跨端架构是如何实现的?
  4. axios 拦截器机制是如何实现的?
  5. axios 浏览器端请求是如何实现的?

本文我们将讨论 axios 对外出口 API 是如何设计的。

axios 的 2 种使用方式

当通过 npm install axios 安装完 axios 之后,就可以以下 2 种方式使用 axios。

一种是在请求时传入 axios 配置项。

js 复制代码
// GET 请求
axios.get('/user', {
    params: {
      ID: 12345
    }
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  })
  .finally(function () {
    // always executed
  });
  
// POST 请求
axios.post('/user', {
    firstName: 'Fred',
    lastName: 'Flintstone'
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

还有一种,就是通过 axios.create() 创建一个包含预配置的 axios,再进行请求。

js 复制代码
const instance = axios.create({
  baseURL: 'https://some-domain.com/api/',
  timeout: 1000,
  headers: {'X-Custom-Header': 'foobar'}
});

axios.get('/user')
axios.post('/user', {firstName: 'Fred',lastName: 'Flintstone'})

这样就避免了每个请求中总是重复书写相同配置的苦恼。

请求方法别名

在实际项目中使用 axios 请求时,我们通过会使用 axios.method() 的方式发送请求。这样的方法一共有 8 个:

  • axios.get(url[, config])
  • axios.delete(url[, config])
  • axios.head(url[, config])
  • axios.options(url[, config])
  • axios.post(url[, data[, config]])
  • axios.put(url[, data[, config]])
  • axios.patch(url[, data[, config]])
  • axios(config)

这 8 个请求方法,底层都是基于 axios.request(config) 封装的。

其中:

  • axios(config) 是 axios.request(config) 的别名
  • axios.get、axios.delete、axios.head、axios.options(url[, config]) 类似于 axios.request({ ...config, method, url, data: config?.data })
  • axios.post、axios.put、axios.patch(url[, data[, config]]) 类似于 axios.request({ ...config, method, url, data })

了解了这些请求方法别名后,我们就来看它们的底层实现。

Axios 类

axios 其实是内部 Axios 类的实例。Axios 类的源码位于 lib/core/Axios.js

其总体实现如下:

js 复制代码
// /v1.6.8/lib/core/Axios.js
class Axios {
  // 1)
  constructor(instanceConfig) {
    this.defaults = instanceConfig;
    this.interceptors = {
      request: new InterceptorManager(),
      response: new InterceptorManager()
    };
  }
  
  // 2)
  async request(configOrUrl, config) {/* ... */}
  
  // 3)
  getUri(config) {/* ... */}
}

// 3)
['delete', 'get', 'head', 'options'].forEach(function forEachMethodNoData(method) {/* ... */})
['post', 'put', 'patch'].forEach(function forEachMethodNoData(method) {/* ... */})

1):可以看到,Axios 类的实现代码还是比较少的,就 2 个方法:

  • async request() 就对应前面所说的 axios.request(),是所有请求的入口地方
  • getUri() 比较少用,是用来获得完整请求路径(基于当前 axios 的默认配置和你传入的配置)

2):另外,在创建 Axios 实例时,实例对象上会绑定 defaults、interceptors 属性。

  • defaults 是在创建 Axios 实例传入的配置,作为默认配置用
  • interceptors 则是拦截器配置入口,如果你设置过拦截器,肯定知道它

3):最后这部分的代码,其实就是基于 Axios.prototype.request 方法进行封装,分别在 Axios.prototype 上添加 'delete'、'get'、'head'、'options' 和 'post'、'put'、'patch' 方法,前 4 个是一类,后 3 个是一类。

接下来,我们详细说一下。

Axios.prototype.request()

这是 axios 实现核心请求逻辑的方法。这块我们在"axios 浏览器端请求是如何实现的?" 这一篇文章中有讲解过,有兴趣的读者,可以浏览学习。

为了避免赘述,我这里给出伪代码实现。

js 复制代码
async request(configOrUrl, config) {
  // 获得传入的配置
  config = getFinalConfigBy(configOrUrl, config)
  
  // 与内部默认配置合并
  config = mergeConfig(this.defaults, config);
  
  // 拼装完整请求链
  const chain = []
    .concat(requestInterceptorChain)
    .concat(dispatchRequest)
    .concat(responseInterceptorChain)
  
  // 发起并返回请求结果
  return request(chain)
}

axios.defaults/interceptors

这是 Axios 实例上的两个属性。

axios.defaults 允许你做默认配置的修改,会对所有当前 axios 实例的请求都生效。

axios.interceptors 则用来提供配置请求/响应拦截器。interceptors.request 允许你在请求前修改请求配置,interceptors.response 则允许你在请求得到响应后、交由用户处理前,提前统一对响应数据做处理。

request、response 2 个属性都是 InterceptorManager 实例,提供 use()/eject()/clear()(对外) 方法和 forEach()(对内)方法。

有兴趣的想要了解拦截器实现机制的读者,可以浏览之前的《axios 拦截器机制是如何实现的?》 一文进行学习。

请求方法别名

再来看看请求方法别名的实现。

先看 'delete'、'get'、'head'、'options' 这 4 个方法。

js 复制代码
// /v1.6.8/lib/core/Axios.js#L193-L202
['delete', 'get', 'head', 'options'].forEach(function forEachMethodNoData(method) {
  Axios.prototype[method] = function(url, config) {
    return this.request(mergeConfig(config || {}, {
      method,
      url,
      data: (config || {}).data
    }));
  };
});

实现稍稍简单。本质上就是为 Axios.prototype 分别添加以上述 4 种请求方式为名的请求实现。this.request() 就是 Axios.prototype.request() 方法。

以 axios.delete(url, config) 为例:'delete' 作为 config.method,url 作为 config.url,最后和传入的 config 合并成一个,交由 axios.request() 处理。

再来看看 'post'、'put'、'patch' 这 3 个方法的实现。

js 复制代码
// /v1.6.8/lib/core/Axios.js#L204-L223
['post', 'put', 'patch'].forEach(function forEachMethodWithData(method) {
  function generateHTTPMethod(isForm) {
    return function httpMethod(url, data, config) {
      return this.request(mergeConfig(config || {}, {
        method,
        headers: isForm ? {
          'Content-Type': 'multipart/form-data'
        } : {},
        url,
        data
      }));
    };
  }

  Axios.prototype[method] = generateHTTPMethod();
  Axios.prototype[method + 'Form'] = generateHTTPMethod(true);
});

会稍稍复杂一丢丢。因为 axios 还额外支持了 'postForm'、'putForm'、'patchForm' 3 个方法,用于传输文件的场景。

以 axios.post(url, data, config) 为例:'post' 作为 config.method,url 作为 config.url,data 作为 config.data,最后和传入的 config 合并成一个,交由 axios.request() 处理。

以 axios.postForm(url, data, config) 为例,相比 axios.post(url, data, config),多传入了一个 'Content-Type': 'multipart/form-data' 的请求头配置。这个请求头项会帮助后端理解要处理的请求类型。

导出 axios

以上我们介绍了 Axios 类的逻辑实现。

如果直接导出 Axios

不过,如果只导出 Axios 类作为对外输出,那么使用方式就是下面这样。

js 复制代码
//////
// axios.js
//////

export { default as defaults } from './defaults/index.js';
export default Axios;

////// 
// app.js
//////

import Axios, { defaults } from 'axios'
// 使用方式一
new Axios(defaults).get('/user')
new Axios(defaults).post('/user', {firstName: 'Fred',lastName: 'Flintstone'})
// 使用方式二
const createAxios = () {
  return new Axios(defaults) 
}
const axios = createAxios()
axios.get('/user')
axios.post('/user', {firstName: 'Fred',lastName: 'Flintstone'})

new Axios() 创建实例的方式不够优雅,而且每次使用都会有一段样板代码。

为了减少这部分的工作,axios 团队在导出时针对 Axios 做了一层封装,让导出 API 更加好用。源码位于 lib/axios.js

js 复制代码
// /v1.6.8/lib/axios.js#L28-L44
function createInstance(defaultConfig) {
  // 1.1)
  const context = new Axios(defaultConfig);
  const instance = Axios.prototype.request.bind(context);

  // 2)
  // Copy axios.prototype to instance
  utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});
  // Copy context to instance
  utils.extend(instance, context, null, {allOwnKeys: true});

  // 3)
  // Factory for creating new instances
  instance.create = function create(instanceConfig) {
    return createInstance(mergeConfig(defaultConfig, instanceConfig));
  };
  
  // 1.2)
  return instance;
}

// 1)
// Create the default instance to be exported
const axios = createInstance(defaults);

// this module should only have a default export
export default axios

1)、首先,导出的 axios 其实是 createInstance(defaults) 的返回值

参数 defaults 是 axios 内部默认配置信息。

js 复制代码
import defaults from './defaults/index.js';

而 createInstance() 内部,返回的其实并不是 Axios 实例,而是 Axios 实例的 request() 方法。

js 复制代码
function createInstance(defaultConfig) {
  // 返回的并不是 Axios 实例
  const context = new Axios(defaultConfig);
  // 而是 Axios 实例的 request() 方法
  const instance = Axios.prototype.request.bind(context);
  
  // ...
  
  return instance;
}

这样,我们就能以 axios(config) 方式发起请求,没必要写成 axios.request(config) 这样了。

同时值得注意的时,request() 方法内部的 this 绑定到了 Axios 实例(即 context)上。这样,request() 方法内部访问的 this 时就不会有问题了。

2)、不过 instance(也就是 axios.request)上现在是没有 Axios 实例上的其他方法了!

因此,接下来我们把 Axios.prototype 和 context 上的属性都复制给 instance。

js 复制代码
function createInstance(defaultConfig) {
  const context = new Axios(defaultConfig);
  const instance = Axios.prototype.request.bind(context);

  // 把 Axios.prototype 上的属性都复制给 instance
  utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});
  // 把 context 上的属性都复制给 instance
  utils.extend(instance, context, null, {allOwnKeys: true});

  // ...
  
  return instance;
}

如此一来,我们就能在 instance 上调用 get()/post()/getUri() 等这些方法了。

3)另外,在来实现 axios.create()

js 复制代码
function createInstance(defaultConfig) {
  // ...
  
  // 基于内部默认配置,在创建一个新的 Axios 实例
  instance.create = function create(instanceConfig) {
    return createInstance(mergeConfig(defaultConfig, instanceConfig));
  };
  
  return instance;
}

可以看到 instance.create 手动增加了 create() 方法。

create() 方法内部其实就是递归调用 createInstance()。不同的是,instance.create() 本质上是基于导出的 axios 的基础上再多一步自定义配置合并。

总结

本文我们讲解了 axios 对外出口 API 是如何设计的。

我们首先介绍了内部 Axios 类的实现,这是 axios 核心逻辑所在;其次为了让出口 API 更好使用,真正导出的 axios 其实是 Axios 实例的 request() 方法,最后又在增加了 create() 方法,方便进一步得到拥有自定义配置的 axios 对象。

好了,希望本文的讲解对你理解 axios 有所帮助,感谢你的阅读。再见。

相关推荐
dorisrv1 小时前
优雅的React表单状态管理
前端
蓝瑟1 小时前
告别重复造轮子!业务组件多场景复用实战指南
前端·javascript·设计模式
dorisrv2 小时前
高性能的懒加载与无限滚动实现
前端
韭菜炒大葱2 小时前
别等了!用 Vue 3 让 AI 边想边说,字字蹦到你脸上
前端·vue.js·aigc
StarkCoder2 小时前
求求你,别在 Swift 协程开头写 guard let self = self 了!
前端
清妍_2 小时前
一文详解 Taro / 小程序 IntersectionObserver 参数
前端
电商API大数据接口开发Cris2 小时前
构建异步任务队列:高效批量化获取淘宝关键词搜索结果的实践
前端·数据挖掘·api
符方昊2 小时前
如何实现一个MCP服务器
前端
喝咖啡的女孩2 小时前
React useState 解读
前端
渴望成为python大神的前端小菜鸟2 小时前
浏览器及其他 面试题
前端·javascript·ajax·面试题·浏览器