动手实现迷你版axios,彻底掌握axios原理

有几个问题,我们可以先思考一下,是否知道答案?

  1. 为什么 axios 既可以当函数调用,也可以当对象使用,比如axios({})、axios.get。
  2. axios 的调用流程是怎样的?
  3. 拦截器吗原理是怎样的?
  4. axios的取消功能是怎么实现的?
  5. 为什么支持浏览器中发送请求也支持node发送请求?

如果对于以上的问题还是一知半解或根本没听说,那么是时候深入了解下axios了~

为了更好地了解axios源码,我们先手写一个发送请求的小demo,掌握了大体流程之后,阅读axios源码会更加得心应手。

手写mini axios

初步搭建的框架如下:
undefined

  1. 定义一种字符串字面量类型 Method
export type Method = 'get' | 'GET'
  | 'delete' | 'Delete'
  | 'head' | 'HEAD'
  | 'options' | 'OPTIONS'
  | 'post' | 'POST'
  | 'put' | 'PUT'
  | 'patch' | 'PATCH'
复制代码
  1. 定义 AxiosRequestConfig 接口类型:
export interface AxiosRequestConfig {
  url: string
  method?: Method
  data?: any
  params?: any,
  headers?: any
}
复制代码
  1. 封装xhr,实现发送请求逻辑

新建xhr.ts 文件,导出 xhr 方法,其中使用到了XMLHttpRequest对象,调用send方法发送请求。

import { AxiosRequestConfig } from './types'
export default function xhr(config: AxiosRequestConfig): void {
  const { data = null, url, method = 'get' } = config
  const request = new XMLHttpRequest()
  request.open(method.toUpperCase(), url, true)
  request.send(data)
}
复制代码
  1. 定义axios类,引入xhr方法
import { AxiosRequestConfig } from './types'
import xhr from './xhr'
function axios(config: AxiosRequestConfig): void {
  xhr(config)
}
export default axios
复制代码

到此为止,发送请求的最小闭环已经实现了,之后会逐渐剖析其他功能。

1. 处理url

undefined

我们使用axios发送请求的结构可以为以下形式:

axios({
  method: 'get',
  url: 'xx/xx',
  params: {}
})
复制代码

其中,params的参数形式非常多,如:

// 对象,`/base/get?a=1&b=2`
params: {
    a: 1,
    b: 2
}

// 数组,’/base/get?foo[]=bar&foo[]=baz'
params: {
    foo: ['bar', 'baz']
}

// 复杂对象,/base/get?foo=%7B%22bar%22:%22baz%22%7D
params: {
    foo: {
        bar: 'baz'
    }
}

// Date类型,/base/get?date=2019-04-01T05:55:39.030Z
params: {
    date
}

// 特殊字符,空格需要转换为+号
params: {
    foo: '@:$, '
}

// 空值null,需要忽略
params: {
    foo: 'bar',
    baz: null
}
复制代码

而且url也可能遇到以下几种形式:

url: '/base/get#hash',// 需要丢弃 url 中的哈希标记
url: '/base/get?foo=bar',// 需要保留url中的参数
复制代码

基于以上的规则,我们写一个拼接请求url的函数:

// 辅助方法
const toString = Object.prototype.toString
function _isDate(val: any): val is Date {
    return toString.call(val) === '[object Date]'
}
function _isObject(val: any): val is Object {
    return val !== null && typeof val === 'object'
}

function _encode(val: string): string {
    return encodeURIComponent(val)
        .replace(/%40/g, '@')
        .replace(/%3A/gi, ':')
        .replace(/%24/g, '$')
        .replace(/%2C/gi, ',')
        .replace(/%20/g, '+')
        .replace(/%5B/gi, '[')
        .replace(/%5D/gi, ']')
}
export function bulidURL(url: string, params?: any) {
    if (!params) {
        return url
    }
    const parts: string[] = []
    Object.keys(params).forEach((key) => {
        let val = params[key]
        if (val === null || typeof val === 'undefined') {
            return
        }
        let values: string[]
        if (Array.isArray(val)) {
            values = val
            key += '[]'
        } else {
            values = [val]
        }
        values.forEach((val) => {
            if (_isDate(val)) {
                val = val.toISOString()
            } else if (_isObject(val)) {
                val = JSON.stringify(val)
            }
            parts.push(`${_encode(key)}=${_encode(val)}`)
        })
    })
    let serializedParams = parts.join('&')
    if (serializedParams) {
        const markIndex = url.indexOf('#')
        if (markIndex !== -1) {
            url = url.slice(0, markIndex)
        }
        url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams
    }
    return url
}
复制代码

实现完此函数后,在axios类中添加此方法:

function axios (config: AxiosRequestConfig): void {
  processConfig(config)
  xhr(config)
}
// 对config中的url进行规范化处理,重新设置config.url
function processConfig (config: AxiosRequestConfig): void {
  config.url = transformUrl(config)
}
// 对url进行规范化处理
function transformUrl (config: AxiosRequestConfig): string {
  const { url, params } = config
  return bulidURL(url, params)
}
复制代码

2. 处理data

undefined
send 方法的参数支持 DocumentBodyInit 类型,BodyInit 包括了 Blob, BufferSource, FormData, URLSearchParams, ReadableStreamUSVString,当没有数据的时候,我们还可以传入 null

// 判断是否是普通的json对象,具有key-value的形式
function _isPlainObject(val: any): val is Object {
   return toString.call(val) === '[object Object]'
}
function transformRequest(data: any): any {
   if (_isPlainObject(data)) {
       return JSON.stringify(data)
   }
   return data
}
复制代码

实现完此函数后,在axios类中添加此方法:

function processConfig(config: AxiosRequestConfig): void {
   config.url = transformURL(config)
   config.data = transformRequestData(config)
}

function transformRequestData(config: AxiosRequestConfig): any {
   return transformRequest(config.data)
}
复制代码

3. 处理header

undefined
上面我们把data转化为了json,但是服务器此时还不能正确地解析,我们在发送数据时需要设置合适的content-type。
首先编写处理header的函数:

// 判断是否是普通的json对象,具有key-value的形式
function _isPlainObject(val: any): val is Object {
    return toString.call(val) === '[object Object]'
}

function normalizeHeaderName(headers: any, normalizedName: string): void {
    if (!headers) {
        return
    }
    Object.keys(headers).forEach(name => {
        if (name !== normalizedName && name.toUpperCase() === normalizedName.toUpperCase()) {
            headers[normalizedName] = headers[name]
            delete headers[name]
        }
    })
}
export function processHeaders(headers: any, data: any): any {
    normalizeHeaderName(headers, 'Content-Type')
    if (_isPlainObject(data)) {
        if (headers && !headers['Content-Type']) {
            headers['Content-Type'] = 'application/json;charset=utf-8'
        }
    }
    return headers
}
复制代码

在xhr方法中,我们也需要处理下header:

export default function xhr (config: AxiosRequestConfig): void {
  const { data = null, url, method = 'get', headers } = config
  const request = new XMLHttpRequest()
  request.open(method.toUpperCase(), url, true)
  Object.keys(headers).forEach((name) => {
    if (data === null && name.toLowerCase() === 'content-type') {
      delete headers[name]
    } else {
      request.setRequestHeader(name, headers[name])
    }
  })
  request.send(data)
}
复制代码

实现完此函数后,在axios文件中添加此方法:

function processConfig (config: AxiosRequestConfig): void {
  config.url = transformURL(config)
  config.headers = transformHeaders(config)
  config.data = transformRequestData(config)
}
function transformHeaders (config: AxiosRequestConfig) {
  const { headers = {}, data } = config
  return processHeaders(headers, data)
}
复制代码

4. 处理response

undefined
如何实现服务端处理响应数据,并且返回promise链式调用的形式?
首先定义一个接口类型:

export interface AxiosResponse {
  data: any
  status: number
  statusText: string
  headers: any
  config: AxiosRequestConfig
  request: any
}
复制代码

xhr 函数添加onreadystatechange事件处理函数,并且让 xhr 函数返回的是 AxiosPromise 类型:

export default function xhr(config: AxiosRequestConfig): AxiosPromise {
  return new Promise((resolve) => {
    const { data = null, url, method = 'get', headers, responseType } = config
    const request = new XMLHttpRequest()
    if (responseType) {
      request.responseType = responseType
    }
    request.open(method.toUpperCase(), url, true)
	// 这里添加onreadystatechange监听事件
    request.onreadystatechange = function handleLoad() {
      if (request.readyState !== 4) {
        return
      }
      const responseHeaders = request.getAllResponseHeaders()
      const responseData = responseType && responseType !== 'text' ? request.response : request.responseText
	  // 构造 `AxiosResponse` 类型的 `reponse` 对象,并把它 `resolve` 出去
      const response: AxiosResponse = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config,
        request
      }
      resolve(response)
    }
    Object.keys(headers).forEach((name) => {
      if (data === null && name.toLowerCase() === 'content-type') {
        delete headers[name]
      } else {
        request.setRequestHeader(name, headers[name])
      }
    })
    request.send(data)
  })
}
复制代码

修改axios函数,使其返回AxiosPromise的数据:

function axios(config: AxiosRequestConfig): AxiosPromise {
  processConfig(config)
  return xhr(config)
}
复制代码

5. 处理response-header

undefined
通过 XMLHttpRequest 对象的 getAllResponseHeaders 方法获取到的值是一段字符串,我们需要将它们处理成对象的结构:

export function parseHeaders(headers: string): any {
    let parsed = Object.create(null)
    if (!headers) { return parsed }
    headers.split('\r\n').forEach(line => {
        let [key, val] = line.split(':')
        key = key.trim().toLowerCase()
        if (!key) { return }
        if (val) { val = val.trim() }
        parsed[key] = val
    })
    return parsed
}
复制代码

接下来在xhr方法中使用这个函数,用来处理response的header:

const responseHeaders = parseHeaders(request.getAllResponseHeaders())
复制代码

6. 处理response-data

undefined
服务器返回给我们的是string类型,我们需要将其转化为json对象。

export function transformResponse(data: any): any {
  if (typeof data === 'string') {
    try {data = JSON.parse(data)} catch (e) {}
  }
  return data
}
复制代码

使用此方法:

function axios(config: AxiosRequestConfig): AxiosPromise {
  processConfig(config)
  return xhr(config).then((res) => {
    return transformResponseData(res)
  })
}
function transformResponseData(res: AxiosResponse): AxiosResponse {
  res.data = transformResponse(res.data)
  return res
}
复制代码

7. 常见错误监听与处理

undefined

  1. 监听网络异常

在xhr方法中添加:

request.onerror = function handleError() {
  reject(new Error('网络错误'))
}
复制代码
  1. 网络超时
const { /*...*/ timeout } = config
if (timeout) {
  request.timeout = timeout
}
request.ontimeout = function handleTimeout() {
  reject(new Error(`Timeout of ${timeout} ms exceeded`))
}
复制代码
  1. 非 200 状态码
request.onreadystatechange = function handleLoad() {
    if (request.readyState !== 4) return
    if (request.status === 0) return
    const responseHeaders = parseHeaders(request.getAllResponseHeaders())
    const responseData = responseType && responseType !== 'text' ? request.response : request.responseText
    const response: AxiosResponse = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config,
        request
    }
    handleResponse(response)
}
function handleResponse(response: AxiosResponse) {
    if (response.status >= 200 && response.status < 300) {
        resolve(response)
    } else {
        reject(new Error(`Request failed with status code ${response.status}`))
    }
}
复制代码

8. 错误类封装

我们可以封装一个AxiosError类,用来承载详细的错误信息。

export interface AxiosError extends Error {
  config: AxiosRequestConfig
  code?: string
  request?: any
  response?: AxiosResponse
  isAxiosError: boolean
}
复制代码
import { AxiosRequestConfig, AxiosResponse } from '../types'

export class AxiosError extends Error {
  isAxiosError: boolean
  config: AxiosRequestConfig
  code?: string | null
  request?: any
  response?: AxiosResponse

  constructor(
    message: string,
    config: AxiosRequestConfig,
    code?: string | null,
    request?: any,
    response?: AxiosResponse
  ) {
    super(message)
    this.config = config
    this.code = code
    this.request = request
    this.response = response
    this.isAxiosError = true
    Object.setPrototypeOf(this, AxiosError.prototype)
  }
}

export function createError(
  message: string,
  config: AxiosRequestConfig,
  code?: string | null,
  request?: any,
  response?: AxiosResponse
): AxiosError {
  const error = new AxiosError(message, config, code, request, response)
  return error
}
复制代码

在抛出错误的时候,我们使用到createError方法自定义错误信息。

axios源码

1. axios.js文件

undefined
这个文件的作用:

  1. 暴露了一个默认的Axios实例
  2. 暴露了创建Axios实例的工厂方法
  3. 暴露了Axios类方便继承
  4. 暴露了Cancel相关的API

注意代码中utils.extend(instance, Axios.prototype, context); 是把Axios.prototype的所有方法和域都复制到实例中,因此axios本身就可以作为一个函数使用,如:

axios.request({
  method: 'post',
  url: '/user',
  data: {
    name: 'xxx'
  }
  // ...other
});
复制代码

完整源码如下:

'use strict';

var utils = require('./utils');
var bind = require('./helpers/bind');
var Axios = require('./core/Axios');
var defaults = require('./defaults');

// 创建一个Axios实例
function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);//实际上是 Axios.prototype.request 这个方法
  utils.extend(instance, Axios.prototype, context); //把Axios.prototype的所有方法和域都复制到实例中
  utils.extend(instance, context);  //把context的所有域都复制到实例中
  return instance;
}

//创建默认实例
var axios = createInstance(defaults);

//暴露 Axios类 方便外部使用继承
axios.Axios = Axios;

// 暴露创建Axios实例的工厂方法
axios.create = function create(instanceConfig) {
  return createInstance(utils.merge(defaults, instanceConfig));
};

// 暴露 取消request相关的方法
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');

// 暴露 all 和 spread 这2个工具方法
axios.all = function all(promises) {
  return Promise.all(promises);
};
axios.spread = require('./helpers/spread');

module.exports = axios;
module.exports.default = axios;
复制代码

2. core/Axios.js文件

undefined
这个文件定义了Axios类,其中包括了以下几点:

  1. 定义Axios类
  2. 定义了Axios.prototype.request(包括配置与处理、拦截器处理)
  3. 定义了Axios.prototype.delete, Axios.prototype.get, Axios.prototype.head, Axios.prototype.options(不需要payload)
  4. 定义了Axios.prototype.put, Axios.prototype.post, Axios.prototype.patch(需要payload)
'use strict';

var defaults = require('./../defaults');
var utils = require('./../utils');
var InterceptorManager = require('./InterceptorManager');
var dispatchRequest = require('./dispatchRequest');

//Axios类
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = { //初始化 interceptors (拦截器)
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

// Axios的核心方法request
// 将 request发起,request拦截器和 response拦截器 链式拼接,最后用 promise 串起来调用
Axios.prototype.request = function request(config) {
  if (typeof config === 'string') {
    config = utils.merge({
      url: arguments[0]
    }, arguments[1]);
  }
  config = utils.merge(defaults, {method: 'get'}, this.defaults, config);
  config.method = config.method.toLowerCase();

  //链表初始值是 dispatchRequest 和 undefined 组成的 2个元素的数组
  var chain = [dispatchRequest, undefined];
  // promise 链的第一个promise将 config 传入
  var promise = Promise.resolve(config);
  // 将request拦截器逐一插入到 链表的头部
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
// 将request拦截器逐一插入到 链表的尾部
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  while (chain.length) {
    // 从链表中从头连续取出2个元素,第一个作为 promise 的 resolve handler, 第二个做 reject handler
    promise = promise.then(chain.shift(), chain.shift());
  }
  return promise;
};

// 用更优雅更短的代码定义了 Axios.prototype.delete, Axios.prototype.get, Axios.prototype.head, Axios.prototype.options,这四个方法都不需要payload(代码中的data)
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  Axios.prototype[method] = function(url, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url
    }));
  };
});

// 用更优雅更短的代码定义了  Axios.prototype.put, Axios.prototype.post, Axios.prototype.patch,这三个方法都需要payload(代码中的data)
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  Axios.prototype[method] = function(url, data, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});

module.exports = Axios;
复制代码

3. InterceptorManager文件

由上述的core/Axios.js代码可以看出,在 Axios 的构造器里,request 和 response 拦截器就被初始化了,它们都是 InterceptorManager 实例,如下所示:

  // 初始化 interceptors (拦截器)
  this.interceptors = { 
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
复制代码

接下来看看InterceptorManager的源码:
undefined
我们可以看到这段代码是非常容易读的,包括了以下几点:

  1. InterceptorManager类
  2. 原型上的use方法——添加handler
  3. 原型上的eject方法——删除handler
  4. 原型上的forEach方法——依次调用handler
var utils = require('./../utils');

function InterceptorManager() {
  this.handlers = [];// 内部使用一个简单数组存放所有 handler
}

// use方法加入新的 handler —— 加到数组的尾部,返回所在的下标(即数组最后一位)作为 id
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};
// 根据 id (实际上就是数组下标)废除使用指定的 handler
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null; 
  }
};
// 遍历所有非null的 handler,并且调用
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};
module.exports = InterceptorManager;
复制代码

因此,在Axios.prototype.request方法中,我们可以采用this.interceptors.request.forEachthis.interceptors.response.forEach进行拦截器的遍历。

我们来想一想,目前的request拦截器和response拦截器有哪些作用呢?

request拦截器:

  1. 记录每次request的时间和内容
  2. 请求发送之前呈现loading效果
  3. 给request加额外的数据
  4. 验证request是否有合法权限
  5. 对请求数据进行统一预处理
  6. ……

response拦截器:

  1. 记录接收到响应的时间,从而可以记录整个请求花销的时间
  2. 处理完请求后呈现请求已完成的效果
  3. 网络抖动情况下使用重试机制
  4. 更改response状态码
  5. 对响应数据进行统一预处理
  6. ……

4. dispatchRequest文件

我们爱看看Axios.prototype.request里做了什么:

  1. 用一个链表 把 request拦截器(interceptors.request) + 真正发起request(dispatchRequest) + response拦截器(interceptors.response) 连起来
  2. 用 promise 逐个调用这个链表
  3. 返回 promise 给调用者

其中,有个核心的函数是dispatchRequestdispatchRequest的代码也清晰易懂,其中包含了对于request 和 response 的处理 —— transformRequesttransformResponse。(可以具体看dispatchRequest.js的源码)。

transformRequest做了以下的操作:

  1. 规范化header中的Content-Type
  2. 规范化request的data类型
  3. 根据数据类型补充request header(如果是json则转换成 json string)

transformResponse做了以下的操作:
将后端 response 的数据转化为 json 格式(默认)

transformRequest: [function transformRequest(data, headers) {
    normalizeHeaderName(headers, 'Content-Type');
    if (utils.isFormData(data) ||
      utils.isArrayBuffer(data) ||
      utils.isBuffer(data) ||
      utils.isStream(data) ||
      utils.isFile(data) ||
      utils.isBlob(data)
    ) {
      return data;
    }
    if (utils.isArrayBufferView(data)) {
      return data.buffer;
    }
    // 根据数据类型 补充request header
    if (utils.isURLSearchParams(data)) {
      setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
      return data.toString();
    }
    // 根据数据类型 补充request header 且 转换成 json string
    if (utils.isObject(data)) {
      setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
      return JSON.stringify(data);
    }
    return data;
  }],

  // 默认后端 response 的数据 是 json 格式
  transformResponse: [function transformResponse(data) {
    if (typeof data === 'string') {
      try {
        data = JSON.parse(data);
      } catch (e) { /* Ignore */ } // “竟然” 默认吞掉了 error 
    }
    return data;
  }]
复制代码

解答

开头提出的问题的答案:

  1. 为什么 axios 既可以当函数调用,也可以当对象使用,比如axios({})、axios.get。

答:axios本质是函数,赋值了一些别名方法,比如get、post方法,可被调用,最终调用的还是Axios.prototype.request函数。

  1. 简述 axios 调用流程。

答:实际是调用的Axios.prototype.request方法,最终返回的是promise链式调用,实际请求是在dispatchRequest中派发的。

  1. 拦截器吗原理是怎样的?

答:用axios.interceptors.request.use添加请求成功和失败拦截器函数,用axios.interceptors.response.use添加响应成功和失败拦截器函数。在Axios.prototype.request函数组成promise链式调用时,Interceptors.protype.forEach遍历请求和响应拦截器添加到真正发送请求dispatchRequest的两端,从而做到请求前拦截和响应后拦截。拦截器也支持用Interceptors.protype.eject方法移除。

  1. axios的取消功能是怎么实现的?

答:通过传递config配置cancelToken的形式,来取消的。判断有传cancelToken,在promise链式调用的dispatchRequest抛出错误,在adapter中request.abort()取消请求,使promise走向rejected,被用户捕获取消信息。

  1. 为什么支持浏览器中发送请求也支持node发送请求?

答:axios.defaults.adapter默认配置中根据环境判断是浏览器还是node环境,使用对应的适配器。适配器支持自定义。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享