【源码阅读】| 从零到一实现简易版axios (七)

278 阅读3分钟

我正在参加「掘金·启航计划」
经过了前面的章节,我们继续来完善axios的功能。
我们从文档中可以看到,axios还有哪些额外功能提供给用户。

实现cancel功能

为什么需要这个功能:

  • 我们请求时,如果响应很慢,除了设置timeout挂起之外,希望有一个功能,能够提前或者手动的将请求挂起,释放资源。

axios-http.com/docs/cancel…

image.png


我们看文档得知,源码中使用了xhr提供的api接口AbortController

developer.mozilla.org/en-US/docs/…

首先,修改AxiosRequestConfig

// types/index.ts
export interface AxiosRequestConfig {
  url?: string
  method?: Method
  data?: any
  params?: any
  headers?: any
  responseType?: XMLHttpRequestResponseType
  timeout?: number
  transformRequest?: AxiosTransformer | AxiosTransformer[]
  transformResponse?: AxiosTransformer | AxiosTransformer[]
  // new
  signal?: AbortController['signal']

  [propName: string]: any
}
// core/dispatchRequest.ts
function throwIfCancellationRequested(config: AxiosRequestConfig) {
  if (config.signal && config.signal.aborted) {
    throw createError(config.signal.reason || 'Request aborted', config, 'ECONNABORTED', config.request)
  }
}
export default function dispatchRequest(config: AxiosRequestConfig): AxiosPromise {
  // new
  throwIfCancellationRequested(config)
  processConfig(config)
  return xhr(config).then(res => {
    // new
    throwIfCancellationRequested(config)
    return transformResponseData(res)
  })
}
// core/xhr.ts
export default function xhr(config: AxiosRequestConfig): AxiosPromise {
  return new Promise((resolve, reject) => {
    const {
      // 省略...
      signal
    } = config
    let onCanceled: (cancel?: Event) => any;
    // 省略...
    if (signal) {
      const onCanceled = (cancel?: Event) => {
        if (!request) {
          return
        }
        reject(
          !cancel || cancel.type
          ? createError('Request aborted', config, 'ECONNABORTED', request)
          : cancel
        )
        request.abort()
      }
      signal.aborted ? onCanceled() : signal.addEventListener('abort', onCanceled)
    }
    // 省略...
    function handleResponse(response: AxiosResponse): void {
      if (response.status >= 200 && response.status < 300) {
        resolve(response)
        if (config.signal) {
          // new
          config.signal.removeEventListener('abort', onCanceled);
        }
      } else {
        reject(
          createError(
            `Request fail with status code ${response.status}`,
            config,
            undefined,
            request,
            response
          )
        )
        // new
        config.signal.removeEventListener('abort', onCanceled);
      }
    }

测试用例

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

const controller = new AbortController()

axios
  .get('/cancel/get', {
    signal: controller.signal
  })
  .then(function () {
    controller.abort('Operation canceled by the user.')
  })
  .catch(function (e) {
    console.log(e)
  })


axios
  .get('/cancel/get', {
    signal: controller.signal
  })
  .catch(function (e) {
    console.log(e)
  })

axios
  .get('/cancel/get', {
    signal: controller.signal
  })
  .catch(function (e) {
    console.log(e)
  })

可以看出,如果我们传入signal,然后执行controller.abort(),后面的请求就被终止,不会发出xhr请求。

实现withCredentials功能

为什么需要这个功能:

  • 我们在发送请求中,可能有一些跨域的请求,而浏览器根据同源策略会限制这类请求,但是可以通过CROS来实现跨域
  • 同域的情况下,发送的请求会携带cookies,但在跨域的情况下默认是不携带的

跨域资源共享 (CORS)是一种机制,它使用额外的HTTP头来告诉浏览器,让运行在一个origin (domain) 上的Web应用被准许访问来自另一个源服务器上的指定资源。只要服务器设置了相应的HTTP响应头,就可以通过CORS实现跨域访问。

因为xhr中自带这个属性,我们只需要传递过去即可
首先,修改AxiosRequestConfig

// types/index.ts
export interface AxiosRequestConfig {
  // 省略...
  withCredentials?: boolean

}
// core/xhr.ts
export default function xhr(config: AxiosRequestConfig): AxiosPromise {
  return new Promise((resolve, reject) => {
    // 省略...

    if (withCredentials) {
      request.withCredentials = withCredentials
    }

实现XSRF防御功能

怎么理解XSRF,这是一种常见的攻击手段,设想一个场景:

  1. 假如我现在登录了银行www.mybank.com,并操作了转账。
  2. 随后我又登录了一个网站www.pictures.com,但是这个网站是恶意网站。
  3. 此时因为默认携带cookies,会将mybankcookies传到pictures中。
  4. 而攻击者恰好也请求mybank,就可以通过我们的cookies 去伪造用户操作。

为了杜绝这种情况,我们需要服务端每次去生成一个token并通过set-cookies的形式在客户端生成cookies


而我们axios库需要做的事:

  • cookies中取出token
  • 添加到对应请求的headers

我们想实现如下效果:

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

首先,修改代码

// types/index.ts
export interface AxiosRequestConfig {
  // 省略...
  xsrfCookieName?: string
  xsrfHeaderName?: string

  // 省略...
}

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

然后,我们需要做以下处理:

  • 同源 || withCredentials = true
  • 获取token
  • 添加到请求中
// helpers/url.ts
// 判断是否同源
interface URLOrigin {
  protocol: string
  host: string
}

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

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

export function isURLSameOrigin(requestURL: string): boolean {
  const parsedOrigin = resolveURL(requestURL)
  return (
    parsedOrigin.protocol === currentOrigin.protocol && parsedOrigin.host === currentOrigin.host
  )

}

然后,我们来实现提取cookies的方法:

// helpers/cookie.ts
const cookie = {
  read(name: string): string | null {
    // ^ 匹配输入的开始
    // | 匹配 | 左右两边的表达式
    // \s* 匹配零个或多个空白字符
    // ; 匹配分号
    // ([^;]*) 匹配除了 ; 以外的任意字符
    const match = document.cookie.match(new RegExp('(^|;\\s*)(' + name + ')=([^;]*)'))
    return match ? decodeURIComponent(match[3]) : null
  }
}

export default cookie

然后放入request

// core/xhr.ts
export default function xhr(config: AxiosRequestConfig): AxiosPromise {
  return new Promise((resolve, reject) => {
    const {
      data = null,
      url,
      method = 'get',
      headers,
      responseType,
      timeout,
      withCredentials,
      signal,
      xsrfCookieName,
      xsrfHeaderName
    } = config
    // 省略...

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

至此,我们就实现了XSRF防御功能

总结

通过上述的编码,我们拓展了axios库的更多功能,通过实现这个库,也拓展了自身的知识和视野。