手撸axios库

575 阅读33分钟

为什么会写这篇文章,或者说为什么要手撸axios库呢?很简单,看了公司项目中封装的axios之后,一脸懵啊。第一次见封装axios都有几百行的,这玩意儿有这么多吗?咋还有这么多方法呢?这方法咋看着这么面生呢?这还是我平常使用的axios吗?带着这么些疑问,我想了想,不如花点时间集中学习一下axios。于是乎,利用一周的假期时间去学习axios,跟着视频从零开始,一步一步实现axios库的大部分功能,最后发布到npm上。总体来说还是收获很大的,学到了不少东西。学完之后再看看公司项目封装的代码,嗷~,原来如此。大师,我悟了!

写下这篇文章,是为了记录下此次手撸axios库中所实现功能的核心代码以及方式方法,加深理解和记忆。

每天更新一个功能实现,万丈高楼平地起,一个功能强大的axios库也是由一个个小的功能实现的,加油!

01-编写基础请求代码

首先来实现一下最最基础的功能,发送请求功能。编写一个入口文件index.js,如下

function axios(config) {
}
export default axios

这就是整个axios库的入口文件了。接下来要做的是发送请求。利用模块化编程的思想,将发送请求的功能拆分出去,新建一个xhr.js文件,如下

export default function xhr(config){
  const { data = null, url, method = 'get' } = config
  const request = new XMLHttpRequest()
  request.open(method.toUpperCase(), url, true)
  request.send(data)
}

代码也很简单,就是接收一个config,根据配置去发送请求。然后回到入口文件,引入这个xhr方法。一个最基础的请求功能就完成了,如下,非常简单

import xhr from './xhr'

function axios(config){
  xhr(config)
}

export default axios

02-处理请求中的url参数

url参数就是请求中的params,在发送请求过程中会将其拼接在url后面。那么我们就来列举一下常见的params参数有哪些:

1. 字符串
axios({
  method: 'get',
  url: '/base/get',
  params: {
    a: 1,
    b: 2
  }
})
请求时的url => /base/get?a=1&b=2

2. 数组
axios({
  method: 'get',
  url: '/base/get',
  params: {
    foo: ['bar', 'baz']
  }
})
请求时的url => /base/get?foo[]=bar&foo[]=baz

3. 对象
axios({
  method: 'get',
  url: '/base/get',
  params: {
    foo: {
      bar: 'baz'
    }
  }
})
请求时的url => /base/get?foo=%7B%22bar%22:%22baz%22%7D  
foo 后面拼接的是{"bar":"baz"}被encode 后的结果

4. Date类型
const date = new Date()

axios({
  method: 'get',
  url: '/base/get',
  params: {
    date
  }
})
请求时的url => /base/get?date=2019-04-01T05:55:39.030Z
date 后面拼接的是date.toISOString()的结果

5. 特殊字符 `@`、`:`、`$`、`,`、``、`[`、`]`
axios({
  method: 'get',
  url: '/base/get',
  params: {
    foo: '@:$, '
  }
})
请求时的url => /base/get?foo=@:$+
空格` `会转换成 `+`

6. undefined null 会忽略
axios({
  method: 'get',
  url: '/base/get',
  params: {
    foo: 'bar',
    baz: null,
    bar: undefined
  }
})
请求时的url => /base/get?foo=bar

7. 去除请求中的哈希标记
axios({
  method: 'get',
  url: '/base/get#hash',
  params: {
    foo: 'bar'
  }
})
请求时的url => /base/get?foo=bar

8. 保留请求中已有的参数
axios({
  method: 'get',
  url: '/base/get?foo=bar',
  params: {
    bar: 'baz'
  }
})
请求时的url => /base/get?foo=bar&bar=baz

分析好了params可能存在的类型,接下来要做的就是定义一些辅助函数来帮助处理。依照模块化编程思想,我们可以新建一个helpers目录,在helpers目录下再创建一个utils.js文件,将这些工具函数都放在utils文件中,以后一些公用的工具函数也都放在这里。

在utils.js中我们先定义这两个isDateisObject函数,他们作用也是不言而喻,很简单,如下:

const toString = Object.prototype.toString

export function isDate (val){
  return toString.call(val) === '[object Date]'
}

export function isObject (val) {
  return val !== null && typeof val === 'object'
}

为什么要自己定义呢?因为简单,就不需要去使用lodash库了。定义好之后,就要处理将params拼接到url上的问题了。我们再新建一个url.js文件,专门存放处理url相关的函数。为了处理以上可能存在的params情况,在url.js中新建两个函数,分别是buildURLencode函数,如下:

import { isDate, isObject } from './util'

// 为了处理特殊字符
function encode (val) {
  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, params) {
  if (!params) {
    return url
  }

  const parts= []

 // 遍历params
  Object.keys(params).forEach((key) => {
    let val = params[key]
    
    // 处理空值
    if (val === null || typeof val === 'undefined') {
      return
    }
    
    let values
    // 构建成数组 方便遍历
    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
}

写好了这个,然后运用起来。回到index.js文件中,引入这个函数,如下:

function axios (config){
  processConfig(config)
  xhr(config)
}

// 处理config
function processConfig (config){
  config.url = transformUrl(config)
}

// 处理url
function transformUrl (config){
  const { url, params } = config
  return bulidURL(url, params)
}

这里新增的两个函数分别用来处理url和config。至于为什么要区分出另外两个函数,而不是直接在axios中编写。这是分工处理,未来还会有很多这样的处理函数,如果都直接放在axios函数中,则会非常臃肿,不便维护。我们可以将不同的功能放在各自的函数中,然后统一引入到axios中,这样看起来很舒服,也便于维护。

03-处理请求中的body数据

我们通过XMLHttpRequest对象实例的send方法来发送请求,并通过该方法的参数设置请求body数据。对于这个body数据类型,可以支持Document 和 BodyInit 类型。BodyInit 类型包括了 BlobArrayBufferTypedArrayURLSearchParamsFormData。当没有数据的时候,我们还可以传入 null。当然,肯定是支持普通对象的。目前仅考虑处理普通对象,其他的类型处理有点问题。

新建一个data.js文件,专门用来处理data。在send中是不能直接发送普通对象的,需要进行转换成json字符串。所以现在需要做两件事情,一是如何识别出普通对象,之前也写过一个isObject函数,但是对于 FormDataArrayBuffer 这些类型,isObject 判断也为 true,但是这些类型的数据我们是不需要做处理的。因此需要重新写一个判别普通对象的函数。二是将识别出来的普通对象进行转换,不是普通对象的直接返回。

根据以上两件事情,首先在util.js中新增一个函数,如下:

export function isPlainObject (val) {
  return toString.call(val) === '[object Object]'
}

在data.js中新增如下:

import { isPlainObject } from './util'

export function transformRequest (data){
  if (isPlainObject(data)) {
    return JSON.stringify(data)
  }
  return data
}

然后再回到index.js中加入这段处理函数,如下:

import { transformRequest } from './helpers/data'

```
function processConfig (config) {
  config.url = transformURL(config)
  config.data = transformRequestData(config)
}

// 处理data
function transformRequestData (config) {
  return transformRequest(config.data)
}

可以看到,这里也是专门定义一个函数来处理data,和处理url一样。为了方便以后进行功能拓展,将各个功能区分开。

再回到之前说处理除了普通对象之外的类型会有问题,目前还不知道问题出在那里,感觉写法没啥问题,也不是跨域的问题,我加了处理跨域的还是无法解决,如下:

image.png

image.png

// 请求
const arr = new Int32Array([21, 31])
// var arrayBuffer = new ArrayBuffer(16)
// const data = new FormData()
// data.append('username', 'Groucho')

axios({
  method: 'post',
  url: '/base/buffer',
  data: arr
})

// 返回
router.post('/base/buffer',function (req,res) {
    // res.header("Access-Control-Allow-Origin","*"); 

    let msg = []
    req.on('data',(chunk) => {
        if (chunk) {
            msg.push(chunk)
        }
    })
    req.on('end',() => {
        let buf = Buffer.concat(msg)
        res.json(buf.toJSON())
    })
})


// app.all('*',function (req,res,next) {
//     // 设置请求头为允许跨域
//     res.header('Access-Control-Allow-Origin','*');
//     // 设置服务器支持的所有头信息字段
//     res.header('Access-Control-Allow-Headers','Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild, sessionToken');
//     // 设置服务器支持的所有跨域请求的方法
//     res.header('Access-Control-Allow-Methods','PUT, POST, GET, DELETE, OPTIONS');
//     if (req.method.toLowerCase() == 'options') {
//         res.send(200);  // 让options尝试请求快速结束
//     } else {
//         next();
//     }
// });

04-处理请求中的header

在数据发送到服务端时,要想服务端能够正常的解析数据,就要给请求header设置正确的Content-Type。所以应该做到支持自定义header,且当传入的data为普通对象,而又没有传入Content-Type时,应该自动设置请求 header 的 Content-Type 字段为application/json;charset=utf-8

那么开始实现这个需求。在helpers目录下新建一个header.js文件,文件写入以下代码:

import { isPlainObject } from './util'

function normalizeHeaderName (headers, normalizedName) {
  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
}

normalizeHeaderName函数的作用时将header属性名规范化。因为header的属性名是大小写不敏感的,比如可以传入content-type的属性。那么可以通过normalizeHeaderName函数将其规范化,变成Content-Type,这样就方便做判断。然后在processHeaders函数中,判断是否传入header和设置Content-Type值,在data为普通对象的情况下,如果没有设置则自动添加上去。

然后再回到index.js文件中,将上面函数引入进去,如下:

function processConfig (config){
  config.url = transformURL(config)
  config.headers = transformHeaders(config)
  config.data = transformRequestData(config)
}

function transformHeaders (config) {
  const { headers = {}, data } = config
  return processHeaders(headers, data)
}

新增一个transformHeaders函数,专门处理header对象。注意 config.headers = transformHeaders(config)必须写在config.data = transformRequestData(config)之前,因为在执行了transformRequestData(config)之后,config.data就是一个字符串了,因此processHeaders函数中的if (isPlainObject(data)) 就不能通过。

那么在这里处理完后,还要在真正发送请求的地方将这个header设置上去。回到xhr.js文件中,添加如下代码,给请求设置header,如下:

export default function xhr(config) {
    const { data = null, url, method = 'get', headers } = config
    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)
}

新增的代码很简单,就是遍历headers,然后逐个添加上去。并且当data为空的时候删除掉content-type的值。因为data为空,这个属性值也没有必要留着。

05-获取响应数据

上面几节都是处理发送请求,这节来处理响应数据。这节要做到能处理服务端响应的数据,并支持 Promise 链式调用的方式。还可以拿到 res 对象,并且该对象包括:服务端返回的数据 data,HTTP 状态码status,状态消息 statusText,响应头 headers、请求配置对象 config 以及请求的 XMLHttpRequest 对象实例 request。同时,对于该请求,还可以通过设置 XMLHttpRequest 对象的 responseType 属性,来指定它响应的数据类型。responseType 的类型值有这些个:"" | "arraybuffer" | "blob" | "document" | "json" | "text"

那么总结一下需要实现的功能:拿到服务端响应的数据res,且支持链式调用。可以配置responseType属性。

回到xhr.js文件中,添加如下代码:

export default function xhr(config) {
   // 返回promise
  return new Promise((resolve) => {
  
     // 添加responseType可选属性
    const { data = null, url, method = 'get', headers, responseType } = config

    const request = new XMLHttpRequest()

    // 有则设置
    if (responseType) {
      request.responseType = responseType
    }

    request.open(method.toUpperCase(), url, true)

    // 在这个事件中拿到响应数据
    request.onreadystatechange = function handleLoad() {
        // readyState不为4 表示请求还没结束
      if (request.readyState !== 4) {
        return
      }
    
    // 拿到所有响应标头
      const responseHeaders = request.getAllResponseHeaders()
      
      // 通过响应类型来拿到响应数据
      const responseData = responseType && responseType !== 'text' ? request.response : request.responseText
      
      // 构建res
      const response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config,
        request
      }
      
      // 返回res
      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.js文件中,做一点小小的修改,代码如下:

function axios(config) {
  processConfig(config)
  return xhr(config)
}

因为xhr返回了一个promise,所以在axios中应该return出去,这样就可以实现链式调用了。

06-处理响应header

在上面拿到的res的header是一段字符串,如下这种:

date: Fri, 05 Apr 2019 12:40:49 GMT
etag: W/"d-Ssxx4FRxEutDLwo2+xkkxKc4y0k"
connection: keep-alive
x-powered-by: Express
content-length: 13
content-type: application/json; charset=utf-8

每一行都是以回车符和换行符 \r\n 结束,它们是每个 header 属性的分隔符。那么就可以利用这点来处理header,将其构建成如下这种对象:

{
  date: 'Fri, 05 Apr 2019 12:40:49 GMT'
  etag: 'W/"d-Ssxx4FRxEutDLwo2+xkkxKc4y0k"',
  connection: 'keep-alive',
  'x-powered-by': 'Express',
  'content-length': '13'
  'content-type': 'application/json; charset=utf-8'
}

回到header.js中,添加如下代码:

export function parseHeaders(headers){
  let parsed = Object.create(null)
  if (!headers) return parsed

  headers.split('\r\n').forEach(line => {
    let [key, ...vals] = line.split(':')
    key = key.trim().toLowerCase()
    if (!key) return
    const val = vals.join(':').trim()
    parsed[key] = val
  })
  return parsed
}

这个代码很简单,就是将这段字符串分割,然后遍历来构建。那么再回到xhr.js中引入这个函数,做出如下修改:

const responseHeaders = parseHeaders(request.getAllResponseHeaders())

非常简单,nice!!!

07-处理响应data

当没有主动设置responseType时,服务端返回给我们的数据是字符串类型。那么可不可以自动的将其转换为JSON 对象呢?当然是可以的,而且也非常简单。在data.js中添加如下代码:

export function transformResponse(data){
    if (typeof data === 'string') {
        try {
            data = JSON.parse(data)
        } catch (error) {
            // do nothing
        }
    }
    return data
}

这段代码就是尝试将字符串类型数据转换为JSON对象,转换失败就原地返回。然后在axios.js中引入这段代码:

function axios(config) {
  processConfig(config)
  return xhr(config).then((res) => {
    return transformResponseData(res)
  })
}

function transformResponseData(res: AxiosResponse): AxiosResponse {
  res.data = transformResponse(res.data)
  return res
}

新增transformResponseData函数,专门用来处理响应的data。然后在axios函数中引用transformResponseData函数。这里有一个点要注意,为什么不像处理响应header一样,直接在xhr.js中引入,而是专门放到axios.js中引用呢?这是因为后续还会继续处理响应data数据。如果全都放在xhr.js中,就感觉功能模块划分不清晰。在xhr.js中应该保持其简洁,即只有其本身的功能。像这种后续会多次处理响应data数据的函数,不属于xhr应该有的功能。这个功能应该放在axios.js中,这才是真正的处理各种数据的地方。

08-异常情况处理

之前都是处理了正常接收请求的逻辑,现在需要对可能发生的一些错误情况进行处理。当发生错误请求时,能够在reject函数中获取到。那么现在来梳理一下可能有那些错误:

  • 网络异常错误 当网络出现异常(比如不通)的时候,发送请求就会触发XMLHttpRequest对象实例的error事件
  • 超时错误 当发送请求后超过某个时间让仍然没有收到响应,则请求自动终止,并触发timeout事件
  • 处理非200状态码 当请求正常时,一般会返回200-300之间的HTTP状态码。对于不处于这个区间的状态码,也可以认为是一种错误的情况

那么理清了这些情况,就可以针对性的做出处理。那么在xhr.js中添加如下代码:

// 处理 网络异常错误
request.onerror = function handleError() {
  reject(new Error('Network Error'))
}

 const { data = null, url, method = 'get', headers, responseType,timeout } = config
 
 //  默认下是0,即永不超时,也可以手动设置请求的超时时间 
 if (timeout) {
     request.timeout = timeout
 }
 
 // 处理超时错误
 request.ontimeout=function handleTimeout(){
     reject(new Error(`Timeout of ${timeout}ms exceeded`))
 }
 

request.onreadystatechange = function handleLoad() {
    // readyState不为4 表示请求还没结束
  if (request.readyState !== 4) {
    return
  }

// 拿到所有响应标头
  const responseHeaders = request.getAllResponseHeaders()

  // 通过响应类型来拿到响应数据
  const responseData = responseType && responseType !== 'text' ? request.response : request.responseText

  // 构建res
  const response = {
    data: responseData,
    status: request.status,
    statusText: request.statusText,
    headers: responseHeaders,
    config,
    request
  }

  // 返回res
  handleResponse(response)
}

// 处理非200-300之间状态码
function handleResponse(response) {
    if (response.status >= 200 && response.status < 300) {
        resolve(response)
    } else {
        reject(new Error(`Request failed with status code ${response.status}`))
    }
}

当我们添加了以上代码后,就可以像下面这样使用了:

axios({
  method: 'get',
  url: '/error/get'
}).then((res) => {
  console.log(res)
}).catch((e) => {
  console.log(e)
})

非常简单,nice!!!

09-获取更多异常信息

对于上一节,我们虽然能够捕获到错误信息,但并不便于我们快速找到问题点。那么什么样的异常信息可以有助于找到我们找到问题呢?我觉得应该包含以下几个点:

  • 错误文本信息
  • 请求对象配置的config
  • 错误代码code
  • 发送的请求request
  • 响应的对象response

如果能在异常信息中获取以上信息,那么排查错误就应该方便很多了。那么接下来就对上一个版本的错误信息进行一些修改,加点料。

在helpers目录下新建error.js文件,添加如下代码:

export class AxiosError {
  constructor(
    message,
    config,
    code = null,
    request = null,
    response = null
  ) {
    
    this.message = message
    this.config = config
    this.code = code
    this.request = request
    this.response = response
    this.isAxiosError = true

    Object.setPrototypeOf(this, AxiosError.prototype) // 将当前对象的原型设置为 AxiosError.prototype
  }
}

export function createError(
  message,
  config,
  code = null,
  request = null,
  response = null
) {
  const error = new AxiosError(message, config, code, request, response)
  return error
}

这段代码中创建并导出了AxiosErrorcreateError工厂函数。代码很简单,就是传入上面提到的信息并赋值就行。可以看到coderequestresponse都是默认赋值为null的。因为在不同的错误情况下他们不一定会有值。

然后再来实际去应用这段代码,回到xhr.js文件中,代码进行如下修改:

import { createError } from '../helpers/error'

request.onerror = function handleError() {
    reject(createError('Network Error', config, null, request))
}

request.ontimeout = function handleTimeout() {
    reject(createError(`Timeout of ${timeout} ms exceeded`, config, 'ECONNABORTED', request))
}

function handleResponse(response) {
  if (!validateStatus || validateStatus(response.status)) {
    resolve(response)
  } else {
    reject(
      createError(
        `Request failed with status code ${response.status}`,
        config,
        null,
        request,
        response
      )
    )
  }
}

添加好了之后就能像下面这样使用了:

axios({
  method: 'get',
  url: '/error/timeout',
  timeout: 2000
}).then((res) => {
  console.log(res)
}).catch((e) => {
  console.log(e.message)
  console.log(e.code)
  console.log(e.request)
})

非常简单,nice!!!

10-接口拓展

为了更方便的使用axios发送请求,比如像axios.post(url[, config])axios.get(url[, config])axios.request(config) 这几种方式,用起来就很简单了,也不必在config中指定urlmethoddata这些属性了。

那么接下来就来简单实现一下这个功能。这个功能是axios发送请求的功能,是一个核心功能,那么基于模块化编程思想,新建一个core目录,在这个目录下新增一个Axios,添加如下代码:

// 这个下面会说 就是发送请求功能
import dispatchRequest from './dispatchRequest'

export default class Axios {
  request(config) {
    return dispatchRequest(config)
  }

  get(url, config) {
    return this._requestMethodWithoutData('get', url, config)
  }

  delete(url, config) {
    return this._requestMethodWithoutData('delete', url, config)
  }

  head(url, config) {
    return this._requestMethodWithoutData('head', url, config)
  }

  options(url, config) {
    return this._requestMethodWithoutData('options', url, config)
  }

  post(url, data, config) {
    return this._requestMethodWithData('post', url, data, config)
  }

  put(url, data, config) {
    return this._requestMethodWithData('put', url, data, config)
  }

  patch(url, data, config) {
    return this._requestMethodWithData('patch', url, data, config)
  }

// get delete head options 请求不需要携带data
  _requestMethodWithoutData(method, url, config) {
    return this.request(
      Object.assign(config || {}, {
        method,
        url
      })
    )
  }

// post put patch 请求需要携带data
  _requestMethodWithData(method, url, data, config) {
    return this.request(
      Object.assign(config || {}, {
        method,
        url,
        data
      })
    )
  }
}

可以看到所有调用的post,get方法等,最终调用的都是dispatchRequest方法。dispatchRequest也就是之前写的axios.js中的文件,只是现在将其拷贝到core下的dispatchRequest文件中,代码如下:

import xhr from './xhr'
import { buildURL } from '../helpers/url'
import { transformRequest,transformResponse } from '../helpers/data'
import { processHeaders } from '../helpers/headers'

export default function dispatchRequest(config){
// 发送请求前修改config
  processConfig(config)
  return xhr(config).then(res => {
   // 拿到res后修改res
    return transformResponseData(res)
  })
}

function processConfig(config){
  config.url = transformURL(config)
  config.headers = transformHeaders(config)
  config.data = transformRequestData(config)
}

function transformURL(config) {
  const { url,params } = config
  return buildURL(url,params)
}

function transformRequestData(config) {
  return transformRequest(config.data)
}

function transformHeaders(config) {
  const { headers = {},data } = config
  return processHeaders(headers,data)
}

function transformResponseData(res) {
  res.data = transformResponse(res.data)
  return res
}

那么再来回想一下axios的用法,有axios(config),也有axios.post()或者axios.get()。可以看到,axios可以是一个函数(直接使用axios(config)),同时又是一个对象(可使用axios.post())。简单来说就是axios是一个混合对象,是函数,也有Axios 类的所有原型属性和实例属性。

下面就来实现一下axios。首先写一个辅助函数,作用是复制属性,将from里面的属性都复制到to中。在helpers/utils.js中添加一个函数,如下代码:

export function extend(to,from) {
  for (const key in from) {
    to[key] = from[key]
  }
  return to
}

接下来就是对axios.js文件做修改,用工厂模式去创建一个混合对象,代码如下:

import Axios from './core/Axios'
import { extend } from './helpers/util'

function createInstance() {
  const context = new Axios()
  const instance = Axios.prototype.request.bind(context)

  extend(instance,context)

  return instancee
}

const axios = createInstance()

export default axios

createInstance工厂函数的内容,首先实例化了Axios实例的context,然后创建instance指向Axios.prototype.request方法,并绑定了上下文context;接着通过extend方法把context中的原型方法和实例方法都拷贝到instance上,这样就实现了一个混合对象:instance本身是一个函数,又拥有Axios类的所有原型和实例属性。最后工厂函数返回instance

这样就通过createInstance工厂函数创建了axios,当直接调用axios方法就相当于执行了Axios类的request方法发送请求,也可以调用axios.getaxios.post等方法。

使用方法如下:

import axios from '../../src/index'

axios({
  url: '/extend/post',
  method: 'post',
  data: {
    msg: 'hi'
  }
})

axios.request({
  url: '/extend/post',
  method: 'post',
  data: {
    msg: 'hello'
  }
})

axios.get('/extend/get')

axios.options('/extend/options')

axios.delete('/extend/delete')

axios.head('/extend/head')

axios.post('/extend/post', { msg: 'post' })

axios.put('/extend/put', { msg: 'put' })

axios.patch('/extend/patch', { msg: 'patch' })

非常简单,nice!!!

11-axios函数重载

现在又有如下使用方法:

// 使用方法一
axios({
  url: '/extend/post',
  method: 'post',
  data: {
    msg: 'hi'
  }
})

// 使用方法二
axios('/extend/post', {
  method: 'post',
  data: {
    msg: 'hello'
  }
})

那么要实现方法二的话,只能是对axios函数进行重载了,其实也就是多加一个参数。axios函数实际调用的是request函数,所以来修改request函数的实现。打开core中的Axios.js文件,修改request函数,如下代码:

// 支持两个参数,url必填,config选填
request(url,config) {
  if (typeof url === 'string') {
    if (!config) {
      config = {}
    }
    config.url = url
  } else {
    config = url
  }
  return dispatchRequest(config)
}

判断url是否为字符串类型,如果为字符串类型,则对config判断,如果config没有传,则将config构造成空对象,同时将url添加到config.url中。如果config不是字符串类型,则表示传的为单个参数,此时url就是config,把url赋值给config就行。

非常简单,nice!!!

12-拦截器实现

有时候我们希望在发送请求前对请求头或响应数据做一些额外处理,这些处理可能是一个,也可能是多个。并且当前处理的数据是上一个函数返回的,可以理解成一个链式调用的过程。如下图:

image.png 可以看到每个拦截器都会返回一个对象供下一个拦截器处理,并且每个拦截器都可以支持同步和异步处理。当然,也是支持删除某个拦截器的。对于这种链式调用,首先想到的应该就是使用promise链的方式来实现这个过程了。

在这个 Promise 链的执行过程中,请求拦截器 resolve 函数处理的是 config 对象,而相应拦截器 resolve 函数处理的是 response 对象。

使用拦截器的方式如下:

// 添加一个请求拦截器
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);
});

// 删除
const myInterceptor = axios.interceptors.request.use(function () {/*...*/})
axios.interceptors.request.eject(myInterceptor)

那么现在就来具体实现一下这种使用方式。先分析一下,axios要拥有一个interceptors对象属性,该属性又有requestresponse两个属性,这俩属性又对外提供一个use方法来添加拦截器。use方法支持两个参数,第一个是resolve函数,第二个是reject函数。

那么先来实现一下这个interceptors对象,在core目录下,新建InterceptorManager.js文件,添加如下代码:

export default class InterceptorManager {
    interceptors;

    constructor() {
        this.interceptors = []
    }

    use(resolved, rejected) {
        this.interceptors.push({
            resolved,
            rejected
        })
        return this.interceptors.length - 1
    }

    forEach(fn) {
        this.interceptors.forEach(interceptor => {
            if (interceptor !== null) fn(interceptor)
        })
    }

    eject(id) {
        if (this.interceptors[id] !== null) this.interceptors[id] = null
    }
}

分析一下代码,首先在初始化时将interceptors重置为[]

use方法接收两个参数resolvereject,然后将这两个函数同时放到同一个对象中,并这个对象添加到interceptors里,最后返回了这个对象在interceptors的index信息,以供删除使用。

forEach方法则是供内部使用,后面会用到,作用就是遍历interceptors并将里面的对象挨个放到fn中去执行。

eject方法则接收一个id,这个id就是use函数返回的index,通过id找到这个拦截器,然后置为null

然后在core/Axios.js中新增代码:

import InterceptorManager from './InterceptorManager'  // 新增
import dispatchRequest,{ transformURL } from './dispatchRequest'
import mergeConfig from './mergeConfig'

export default class Axios {
  defaults;
  interceptors;

  constructor() {
    this.interceptors = {
      request: new InterceptorManager,
      response: new InterceptorManager
    } // 新增
  }

  request(url,config){
    if (typeof url === 'string') {
      if (!config) {
        config = {}
      }
      config.url = url
    } else {
      config = url
    }

    config = mergeConfig(this.defaults,config)
    config.method = config.method.toLowerCase()

    // 链式  新增
    const chain = [
      {
        resolved: dispatchRequest,
        rejected: undefined
      }
    ]
    
     // 新增
    this.interceptors.request.forEach(interceptor => {
      chain.unshift(interceptor)
    })
    this.interceptors.response.forEach(interceptor => {
      chain.push(interceptor)
    })

    // 新增
    let promise = Promise.resolve(config)

    // 新增
    while (chain.length) {
      const { resolved,rejected } = chain.shift()
      promise = promise.then(resolved,rejected)
    }

    return promise
  }

  get(url,config) {
    return this._requestMethodWidthoutData('get',url,config)
  }

  delete(url,config) {
    return this._requestMethodWidthoutData('delete',url,config)
  }

  head(url,config) {
    return this._requestMethodWidthoutData('head',url,config)
  }

  options(url,config) {
    return this._requestMethodWidthoutData('options',url,config)
  }

  post(url,data,config) {
    return this._requestMethodWidthData('post',url,data,config)
  }

  put(url,data,config) {
    return this._requestMethodWidthData('put',url,data,config)
  }

  patch(url,data,config) {
    return this._requestMethodWidthData('patch',url,data,config)
  }

  getUri(config) {
    config = mergeConfig(this.defaults,config)
    return transformURL(config)
  }

  _requestMethodWidthoutData(method,url,config) {
    return this.request(
      Object.assign(config || {},{
        method,
        url
      })
    )
  }

  _requestMethodWidthData(method,url,data,config) {
    return this.request(
      Object.assign(config || {},{
        method,
        url,
        data
      })
    )
  }
}

首先引入InterceptorManager这个类,然后在Axios类中新增interceptors属性,在初始化时,将这个interceptors属性赋值为对象,这个对象有requestresponse属性,这俩属性又分别是InterceptorManager对象。有了这个之后就可以使用axios.interceptors.request.use()axios.interceptors.response.use()了。

新增一个chain变量来保存拦截器。首先这个chain中自带一个默认的拦截器,就是dispatchRequest函数。this.interceptors.request.forEach(interceptor => { chain.unshift(interceptor) }) 这段代码就是将request拦截器遍历插入到 chain 的前面,那么对于请求拦截器,就会先执行后添加的。this.interceptors.response.forEach(interceptor => { chain.push(interceptor) }) 这个同理,就是将response拦截器遍历插入到 chain 后面,则对于响应拦截器,就会先执行先添加的。然后while函数就是遍历chain数组,拿到每个拦截器对象,把它们的 resolved 函数和 rejected 函数添加到 promise.then 的参数中,这样就相当于通过 Promise 的链式调用方式,实现了拦截器一层层的链式调用的效果。

非常简单,nice!!!

13-实现配置化

我们希望也希望 axios 可以有默认配置,定义一些默认的行为。这样在发送每个请求,用户传递的配置可以和默认配置做一层合并。当然,对于这些默认配置,我们也是可以去直接修改的。

src目录下新建defaults.js文件,添加如下代码:

const defaults = {
  method: 'get',
  timeout: 0,
  headers: {
    common: {
      Accept: 'application/json, text/plain, */*'
    }
  },
}

const methodNoData = ['delete','get','head','options']
methodNoData.forEach(method => {
  defaults.headers[method] = {}
})

const methodWithData = ['put','patch','post']
methodWithData.forEach(method => {
  defaults.headers[method] = {
    'Content-Type': 'application/x-www-form-urlencoded'
  }
})

export default defaults

这些代码很简单,就是加一些默认的属性值,对于'delete','get','head','options'方法,默认为对象;而'put','patch','post'方法默认添加一个'Content-Type'属性。当然,在这里可以拓展一些项目特殊的属性。

那应该在那里使用这个默认配置呢?回想一下上一节的拦截器实现,有用到config配置。那么既然是默认配置,就是在拦截器使用之前来使用这个。

core/Axios.js中新增代码:

import mergeConfig from './mergeConfig'

//  新增一个参数
constructor(initConfig) {
  this.defaults = initConfig
  this.interceptors = {
    request: new InterceptorManager,
    response: new InterceptorManager
  }
}

// 在request函数中新增
if (typeof url === 'string') {
  if (!config) {
    config = {}
  }
  config.url = url
} else {
  config = url
}

config = mergeConfig(this.defaults, config)
config.method = config.method.toLowerCase()  // 统一将方法名小写

新增的代码很简单,就是做一层配置合并。但是,要注意合并策略,因为字段不同,合并的策略也是不一样的。这里要分析一下合并策略的使用。

core目录下新建mergeConfig.js文件:

import { isPlainObject,deepMerge } from '../helpers/util'
import { AxiosRequestConfig } from '../type'

const strats = Object.create(null)

//  合并策略1
function defaultStrat(val1,val2) {
  return typeof val2 !== 'undefined' ? val2 : val1
}

// 合并策略2
function fromVal2Strat(val1,val2) {
  if (typeof val2 !== 'undefined') return val2
}

// 合并策略3
function deepMergeStrat(val1,val2) {
  if (isPlainObject(val2)) {
    return deepMerge(val1,val2)
  } else if (typeof val2 !== 'undefined') {
    return val2
  } else if (isPlainObject(val1)) {
    return deepMerge(val1)
  } else {
    return val1
  }
}

// 默认取val2的值 --》 使用fromVal2Strat策略
const stratKeysFromVal2 = ['url','params','data']
stratKeysFromVal2.forEach(key => {
  strats[key] = fromVal2Strat
})

// val2有则取val2  没有则取val1  --》 使用deepMergeStrat
const stratKeysDeepMerge = ['headers','auth']
stratKeysDeepMerge.forEach(key => {
  strats[key] = deepMergeStrat
})

export default function mergeConfig(
  config1,
  config2
) {
  if (!config2) config2 = {}

  const config = Object.create(null) // 定义一个空对象

  for (let key in config2) {
    mergeFiled(key)
  }

  for (let key in config1) {
    if (!config2[key]) mergeFiled(key)
  }

  function mergeFiled(key) {
    const strat = strats[key] || defaultStrat  // 如果即没有使用fromVal2Strat和deepMergeStrat,则使用defaultStrat
    config[key] = strat(config1[key],config2[key])
  }

  return config
}

对于'url','params','data'属性肯定是优先取传入的值,而'headers','auth'则是有传入取传入,没有取默认的。对于其他的属性,比如timeout也是有传入取传入,没有取默认的。但是headerstimeout不同,timeout是属性,而headers是复杂的对象,所以不能用同一种策略。

经过合并后的配置中的 headers 是一个复杂对象,多了 commonpostget 等属性,而这些属性中的值才是我们要真正添加到请求 header 中的。比如:

// 压缩前
headers: {
  common: {
    Accept: 'application/json, text/plain, */*'
  },
  post: {
    'Content-Type':'application/x-www-form-urlencoded'
  }
}

// 压缩后
headers: {
  Accept: 'application/json, text/plain, */*',
 'Content-Type':'application/x-www-form-urlencoded'
}

要压缩header,可以在helpers/header.js中新建一个函数flattenHeaders函数,如下:

export function flattenHeaders(headers,method) {
  if (!headers) {
    return headers
  }

  headers = deepMerge(headers.common,headers[method],headers)

  const methodToDelete = ['delete','get','head','options','post','put','patch','common']
  methodToDelete.forEach(method => {
    delete headers[method]
  })

  return headers
}

然后来到真正发送请求的地方来压缩header逻辑,在core/dispatchRequest.js中添加如下代码:

function processConfig(config) {
  config.url = transformURL(config)
  config.headers = transformHeaders(config)
  config.data = transformRequestData(config)
  config.headers = flattenHeaders(config.headers,config.method) // 新增
}

至于为什么不在拦截器那里使用这个方法,是因为在拦截器中还会对config进行修改。那么到这里就实现了config的默认配置了。

然后再来到axios.js文件中,使用这个默认配置,在createInstance函数中新增一个参数config即可,代码如下:

function createInstance(config){
  const context = new Axios(config)
  const instance = Axios.prototype.request.bind(context)

  extend(instance, context)
  return instance as AxiosStatic
}

const axios = createInstance(defaults)

很简单,当使用axios时即默认使用了基础配置。

使用方法如下:

axios.defaults.headers.common['test2'] = 123

axios({
  url: '/config/post',
  method: 'post',
  data: qs.stringify({
    a: 1
  }),
  headers: {
    test: '321'
  }
}).then((res) => {
  console.log(res.data)
})

非常简单,nice!!!

14-扩展axios.create静态接口

目前为止,我们的axios都是一个单例,一旦修改了axios的默认配置,就会影响所有的请求。那么我们希望能提供一个axios.create的静态接口来允许创建一个新的axios实例,同时允许传入新的配置和默认配置合并,并作为新的默认配置。

其实这个也挺简单,调用createInstance函数并返回即可,在axios.js中新增如下代码:

axios.create = function create(config) {
  return createInstance(mergeConfig(defaults, config))
}

内部调用了 createInstance 函数,并且把参数 config 与 defaults 合并,作为新的默认配置。

非常简单,nice!!!

15-实现取消功能

这个功能应该算是比较重要的了,也是比较难理解的地方。对我而言,我也是看了三遍视频再加上执行代码才稍微懂了。

请求的发送是一个异步过程,最终会执行xhr.send方法,xhr对象提供了abort方法,可以取消请求。但是我们在外部并不能访问到xhr对象,所以我们想要在执行cancel方法的时候,去执行xhr.abort方法。就需要在xhr异步请求过程中,插入一段代码。当我们在外部执行cancel函数的时候,会驱动这段代码的执行,然后去执行xhr.abort方法取消请求。

那么如何实现异步过程中插入代码,并在外部驱动这段代码执行呢? 可以利用promise实现异步分离,也就是在cancel中保存一个pending状态的Promise对象,然后当我们执行cancel方法时,能够访问到这个Promise对象,把它从pending状态变成resolve状态,这样就可以在then函数中实现取消请求的逻辑。

这里可能有点不太理解到底是个什么意思,没事,接着往下看,后面会详细介绍啥意思。

那么对于取消功能,有以下两种方式来实现:

// 第一种
const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function (e) {
  if (axios.isCancel(e)) {
    console.log('Request canceled', e.message);
  } else {
    // 处理错误
  }
});

// 取消请求 (请求原因是可选的)
source.cancel('Operation canceled by the user.');


// 第二种
const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    cancel = c;
  })
});

// 取消请求
cancel();

首先来看第二种的实现吧,看着代码比较少。在core目录下新建CancelToken.js文件,添加以下代码:

export default class CancelToken {
  promise
  reason

  constructor(executor) {
    let resolvePromise
    this.promise = new Promise(resolve => {
      resolvePromise = resolve
    })

    executor(message => {
    // 如果已经有reason 表示已经取消过了
      if (this.reason) {
        return
      }
      this.reason = message
      resolvePromise(this.reason)
    })
  }
}

首先,在创建CancelToken对象的时候要传入一个executor函数,这个executor函数也接收一个message参数。在CancelToken对象中还有两个属性reasonpromise。在constructor内部,new Promise(resolve => {resolvePromise = resolve})这段代码的意思是返回一个Promise对象,注意此时是pendding状态。同时将Promise中的resovle函数赋值给resolvePromise。那么我们就可以在外部调用这个resolvePromise函数,当resolvePromise函数被执行,则Promise对象会转变为resolve状态。然后执行executor函数时,就会调用resolvePromise函数,此时Promise对象就转变为了resolve状态。记住这里的Promise状态变化,下面会用到。

然后我们在core目录下的xhr.js中添加一段代码:

if (cancelToken) {
  cancelToken.promise.then(reason => {
    request.abort()
    reject(reason)
  })
}

这个cancelToken就是上面的CancelToken对象。如果这个对象存在,那么就去访问该对象的promise属性,这个属性是一个Promise对象,如果是resolve状态,就会往下执行then方法,从而执行request.abort();如果是pendding状态,则不会执行。

那么这里就实现了取消功能的第二种方法了。再来看看第一种,其实也是一样的,只是在内部做了封装,还是在core目录下CancelToken.js文件,添加如下代码:

export default class CancelToken {
  //.........

  static source() {
  let cancel
  const token = new CancelToken(c => {
    cancel = c
  })
  return {
    cancel,
    token
  }
 }
}

仔细看看这个,有么有和第二种取消功能的方法长的一样。其实就是封装了一下,本质上还是一样。

那么这里就实现了取消功能。当然,还有一些地方要注意。比如,当捕获请求时,我们要怎么判断这个错误参数error是不是取消请求导致的呢?要实现这个功能,得加点判断。在core目录下新建Cancel.js文件,添加如下代码:

export default class Cancel {
  message

  constructor(message) {
    this.message = message
  }
}

export function isCancel(value) {
  return value instanceof Cancel
}

代码很简单,就是做一层包裹,再加个判断函数就完事了。使用的话,就是在CancelToken类中使用,在core目录下CancelToken.js中稍微修改一下:

    executor(message => {
      if (this.reason) return
      this.reason = new Cancel(message) // 修改部分
      resolvePromise(this.reason)
    })

然后给axios拓展一些静态方法,以供使用上面这些方法,在axios.js文件中添加以下代码:

import CancelToken from './cancel/CancelToken'
import Cancel, { isCancel } from './cancel/Cancel'


axios.CancelToken = CancelToken
axios.Cancel = Cancel
axios.isCancel = isCancel

还要注意到一个问题,比如当一个请求携带的 cancelToken 已经被使用过,那么我们甚至都可以不发送这个请求,只需要抛一个异常即可,并且抛异常的信息就是我们取消的原因,所以我们需要给 CancelToken 扩展一个方法。

cancel/CancelToken.js文件中添加代码:

export default class CancelToken {
  // ...

 // 判断如果存在 `this.reason`,说明这个 `token` 已经被使用过了,直接抛错。
  throwIfRequested(){
    if (this.reason) {
      throw this.reason
    }
  }
}

然后再发送请求的地方使用这个方法。在core/dispatchRequest.js中添加如下代码:

export default function dispatchRequest(config) {
  throwIfCancellationRequested(config) // 新增 放在最前面判断
  processConfig(config)
  return xhr(config).then(
    res => {
      return transformResponseData(res)
    },
    e => {
      if (e && e.response) {
        e.response = transformResponseData(e.response)
      }
      return Promise.reject(e)
    }
  )
}


function throwIfCancellationRequested(config){
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested()
  }
}

那么到这里就实现了完整的取消功能了,也考虑了一些额外情况并做了处理。

非常简单,nice!!!

16-实现XSRF防御

CSRF 的防御手段有很多,比如验证请求的 referer,但是 referer 也是可以伪造的,所以杜绝此类攻击的一种方式是服务器端要求每次请求都包含一个 token,这个 token 不在前端生成,而是在我们每次访问站点的时候生成,并通过 set-cookie 的方式种到客户端,然后客户端发送请求的时候,从 cookie 中对应的字段读取出 token,然后添加到请求 headers 中。这样服务端就可以从请求 headers 中读取这个 token 并验证,由于这个 token 是很难伪造的,所以就能区分这个请求是否是用户正常发起的。

那么我们应该如何去实现呢?其实就是在发送请求时,从cookie中读取对应的token值,然后添加到请求header中。我们允许用户配置xsrfCookieNamexsrfHeaderName,其中xsrfCookieName表示存储tokencookie名称,xsrfHeaderName表示请求headertoken对应的header名称。比如像下面这种:

axios.get('/more/get',{
  xsrfCookieName: 'XSRF-TOKEN', // default
  xsrfHeaderName: 'X-XSRF-TOKEN' // default
}).then(res => {
  console.log(res)
})

那么接下来就来实现这一需求。首先,在defaults.js文件中添加这两个属性,并给出默认值,代码如下:

const defaults = {
  // ...
  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN',
}

然后我们还要做三件事情:

  1. 首先判断如果是配置 withCredentials 为 true 或者是同域请求,我们才会请求 headers 添加 xsrf 相关的字段
  2. 如果判断成功,尝试从 cookie 中读取 xsrf 的 token 值
  3. 如果能读到,则把它添加到请求 headers 的 xsrf 相关字段中

先实现第一件事情,在helpers目录下新建url.js文件,添加如下代码:


// 对比url判断是否跨域
export function isURLSameOrigin(requestURL) {
  const parsedOirgin = resolveURL(requestURL)
  return (
    parsedOirgin.host === currentOrigin.host && parsedOirgin.protocol === currentOrigin.protocol
  )
}

const urlParsingNode = document.createElement('a')
const currentOrigin = resolveURL(window.location.href)

function resolveURL(url) {
  urlParsingNode.setAttribute('href',url)
  const { protocol,host } = urlParsingNode
  return {
    protocol,
    host
  }
}

同域名的判断主要利用了一个技巧,创建一个 a 标签的 DOM,然后设置 href 属性为我们传入的 url,然后可以获取该 DOM 的 protocolhost。当前页面的 url 和请求的 url 都通过这种方式获取,然后对比它们的 protocol 和 host 是否相同即可。

然后实现cookie的读取。在helpers目录下新建cookie.js文件,添加如下代码:

const cookie = {
  read(name) {
    const match = document.cookie.match(new RegExp('(^|;\\s*)(' + name + ')=([^;]*)'))
    return match ? decodeURIComponent(match[3]) : null
  }
}

export default cookie

最后在发送请求时给header加上这个cookie。在core/xhr.js中添加如下代码:

const {
  /*...*/
  xsrfCookieName,
  xsrfHeaderName
} = config

if ((withCredentials || isURLSameOrigin(url)) && xsrfCookieName) {
  const xsrfValue = cookie.read(xsrfCookieName)
  if (xsrfValue) {
    headers[xsrfHeaderName] = xsrfValue
  }
}

非常简单,nice!!!

17-实现上传和下载的进度监控

有些时候,当我们上传文件或者是请求一个大体积数据的时候,希望知道实时的进度,甚至可以基于此做一个进度条的展示。(当然,进度条这里就不实现了)

xhr 对象提供了一个 progress 事件,我们可以监听此事件对数据的下载进度做监控;另外,xhr.uplaod 对象也提供了 progress 事件,我们可以基于此对上传进度做监控。

那么,基于此,我们就给axios 的请求配置提供 onDownloadProgress 和 onUploadProgress 2 个函数属性,用户可以通过这俩函数实现对下载进度和上传进度的监控。

core/xhr.js中添加如下代码:

const {
  /*...*/
  onDownloadProgress,
  onUploadProgress
} = config

if (onDownloadProgress) {
  request.onprogress = onDownloadProgress
}

if (onUploadProgress) {
  request.upload.onprogress = onUploadProgress
}

另外,如果请求的数据是 FormData 类型,我们应该主动删除请求 headers 中的 Content-Type 字段,让浏览器自动根据请求数据设置 Content-Type。比如当我们通过 FormData 上传文件的时候,浏览器会把请求 headers 中的 Content-Type 设置为 multipart/form-data

所以,添加一个判断 FormData 的方法。在helpers/util.js中添加如下代码:

export function isFormData(val) {
  return typeof val !== 'undefined' && val instanceof FormData
}

然后再回到core/xhr.js中添加如下代码:

if (isFormData(data)) {
  delete headers['Content-Type']
}

使用方式如下:

axios.get('/more/get',{
  onDownloadProgress(progressEvent) {
    // 监听下载进度
  }
})

axios.post('/more/post',{
  onUploadProgress(progressEvent) {
    // 监听上传进度
  }
})

非常简单,nice!!!

18-自定义合法状态码

一般来说,在处理响应结果的时候,认为 HTTP status 在 200 和 300 之间是一个合法值,在这个区间之外则创建一个错误。有些时候我们想自定义这个规则,比如认为 304 也是一个合法的状态码,所以我们希望 axios 能提供一个配置,允许我们自定义合法状态码规则,使用方式如下:

axios.get('/more/304', {
  validateStatus(status) {
    return status >= 200 && status < 400
  }
}).then(res => {
  console.log(res)
}).catch((e: AxiosError) => {
  console.log(e.message)
})

那么来实现这一需求,在defaults.js中添加如下代码:

const defaults = {
  // ....

  // 新增
  validateStatus(status) {
    return status >= 200 && status < 300
  }
}

然后再请求后对响应数据的处理逻辑。在core/xhr.js添加如下代码:

const {
  /*...*/
  validateStatus
} = config

function handleResponse(response) {
  if (!validateStatus || validateStatus(response.status)) {
    resolve(response)
  } else {
    reject(
      createError(
        `Request failed with status code ${response.status}`,
        config,
        null,
        request,
        response
      )
    )
  }
}

如果没有配置 validateStatus 以及 validateStatus 函数返回的值为 true 的时候,都认为是合法的,正常 resolve(response),否则都创建一个错误。

非常简单,nice!!!

19-自定义参数序列化

在之前的章节,我们对请求的 url 参数做了处理,我们会解析传入的 params 对象,根据一定的规则把它解析成字符串,然后添加在 url 后面。在解析的过程中,我们会对字符串 encode,但是对于一些特殊字符比如 @+ 等却不转义,这是 axios 库的默认解析规则。当然,我们也希望自己定义解析规则,于是我们希望 ts-axios 能在请求配置中允许我们配置一个 paramsSerializer 函数来自定义参数的解析规则,该函数接受 params 参数,返回值作为解析后的结果,如下:

axios.get('/more/get', {
  params: {
    a: 1,
    b: 2,
    c: ['a', 'b', 'c']
  },
  paramsSerializer(params) {
    return qs.stringify(params, { arrayFormat: 'brackets' })
  }
}).then(res => {
  console.log(res)
})

那么就来修改一下 buildURL 函数的实现,在helpers/url.js中修改代码:

export function bulidURL(
  url,
  params,
  paramsSerializer
){
  if (!params) {
    return url
  }

  let serializedParams

  if (paramsSerializer) {
    serializedParams = paramsSerializer(params)
  } else if (isURLSearchParams(params)) {
    serializedParams = params.toString()
  } else {
    const parts = []

    Object.keys(params).forEach(key => {
      let val = params[key]
      if (val === null || typeof val === 'undefined') {
        return
      }
      let values
      if (Array.isArray(val)) {
        values = val
        key += '[]'
      } else {
        values = [val]
      }
      values.forEach(val => {
        if (isDate(val)) {
          val = val.toISOString()
        } else if (isPlainObject(val)) {
          val = JSON.stringify(val)
        }
        parts.push(`${encode(key)}=${encode(val)}`)
      })
    })

    serializedParams = parts.join('&')
  }

  if (serializedParams) {
    const markIndex = url.indexOf('#')
    if (markIndex !== -1) {
      url = url.slice(0,markIndex)
    }

    url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams
  }

  return url
}

这里我们给 buildURL 函数新增了 paramsSerializer 可选参数,另外我们还新增了对 params 类型判断,如果它是一个 URLSearchParams 对象实例的话,我们直接返回它 toString 后的结果。

对于isURLSearchParams函数,在helpers/util.js中新增如下代码:

export function isURLSearchParams(val) {
  return typeof val !== 'undefined' && val instanceof URLSearchParams
}

最后一步就是修改 buildURL 调用的逻辑,在core/dispatchRequest.js中修改代码:

function transformURL(config) {
  const { url, params, paramsSerializer } = config
  return buildURL(url!, params, paramsSerializer)
}

非常简单,nice!!!

20-实现baseURL

有些时候,我们会请求某个域名下的多个接口,我们不希望每次发送请求都填写完整的 url,希望可以配置一个 baseURL,之后都可以传相对路径。如下:

const instance = axios.create({
  baseURL: 'https://some-domain.com/api'
})

instance.get('/get')

instance.post('/post')

我们一旦配置了 baseURL,之后请求传入的 url 都会和我们的 baseURL 拼接成完整的绝对地址,除非请求传入的 url 已经是绝对地址。

那么要实现这个功能,先实现两个辅助功能,第一个是判断是否为绝对路径,第二个是拼接路径,在helpers/url.js中添加如下代码:

export function isAbsoluteURL(url){
  return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url)
}

export function combineURL(baseURL,relativeURL) {
  return relativeURL ? baseURL.replace(/\/+$/,'') + '/' + relativeURL.replace(/^\/+/,'') : baseURL
}

最后我们来调用这俩个辅助函数,在core/dispatchRequest.js中添加如下代码:

function transformURL(config){
  let { url,params,paramsSerializer,baseURL } = config
  if (baseURL && !isAbsoluteURL(url)) {
    url = combineURL(baseURL,url)
  }
  return buildURL(url,params,paramsSerializer)
}

非常简单,nice!!!

21-完结

磕磕绊绊花了一个多月的时间才写完,期间还有几个月完全没有写,不然早就应该写完了吧。不过还好,终于是写完了。写完和看完真的是两种状态,看完就觉得非常简单,轻松拿捏;写完才知道,期间好多细节我都不知道,还得重新查找资料才能理解。

写完了,该干下一件事情了。希望下件事情不会这么拖拉。冲冲冲,干了这碗鸡汤,你我皆是黑马。