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

503 阅读3分钟

我正在参加「掘金·启航计划」
经过前面两章,我们现在已经构建出一个基础的axios,并且对request部分进行处理。
接下来,我们来处理response部分。

需求分析

我们来看下 axios文档 image.png

可以看到,默认设置的 responseType为json格式,encoding为utf8,我们再看下response的数据类型定义 axios-http.com/docs/res_sc…

image.png image.png

从文档中我们知道,axios是支持.then()调用,即使用了Promise

处理responseType

  • 处理responseType
  • 将xhr用Promise 封装

因此,我们来定义一下response的类型

export interface AxiosResponse {
	data: any
	status: number
	statusText: string
	headers: any
	config: AxiosRequestConfig
	request: any
}
export interface AxiosPromise extends Promise<AxiosResponse> {
}
export interface AxiosRequestConfig {
  url?: string
  method?: string
  data?: any
  params?: any
  headers?: any
	// 新增
  responseType?: XMLHttpRequestResponseType
  timeout?: number
}

我们在原有的xhr中用promise进行封装

对应的源码在 github.com/axios/axios…

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)

    request.onreadystatechange = function handleLoad() {
      if (request.readyState !== 4) {
        return
      }
        // 下面会处理headers
      // const responseHeaders = 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
      }
      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
function axios(config: AxiosRequestConfig): AxiosPromise {
	processConfig(config)
	return xhr(config)
}

处理headers

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

我们使用getAllResponseHeaders可以得到头部信息的字符串,我们做一层转换,将其转化成对象的形式,已更方便阅读 我们先来看看源码是怎么实现这一步骤的

static from(thing) {
	return thing instanceof this ? thing : new this(thing);
}
// 省略...
constructor(headers) {
	headers && this.set(headers);
}
set(header, valueOrRewrite, rewrite) {
  const self = this;

  function setHeader(_value, _header, _rewrite) {
    const lHeader = normalizeHeader(_header);

    if (!lHeader) {
      throw new Error('header name must be a non-empty string');
    }

    const key = utils.findKey(self, lHeader);

    if(!key || self[key] === undefined || _rewrite === true || (_rewrite === undefined && self[key] !== false)) {
      self[key || _header] = normalizeValue(_value);
    }
  }

  const setHeaders = (headers, _rewrite) =>
    utils.forEach(headers, (_value, _header) => setHeader(_value, _header, _rewrite));

  if (utils.isPlainObject(header) || header instanceof this.constructor) {
    setHeaders(header, valueOrRewrite)
  } else if(utils.isString(header) && (header = header.trim()) && !isValidHeaderName(header)) {
    setHeaders(parseHeaders(header), valueOrRewrite);
  } else {
    header != null && setHeader(valueOrRewrite, header, rewrite);
  }

  return this;
}

我们来构造自己的简易版本

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
}

处理response

我们希望如果不设置响应类型时,默认为json格式。

// helper/data.ts
export function transformResponse(data: any): any {
  if (typeof data === 'string') {
    try {
      data = JSON.parse(data)
    } catch (e) {
      // @todo:
			// throw Axios Error
    }
  }
  return data
}

然后,在原有的axios函数返回值中,做一层处理

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

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

测试用例

axios({
  method: 'post',
  url: '/post',
  data: {
    a: 1,
    b: 2
  }
}).then((res) => {
  console.log(res)
})

axios({
  method: 'post',
  url: '/post',
  responseType: 'json',
  data: {
    a: 'aaa',
    b: 8888
  }
}).then((res) => {
  console.log(res)
})

我们可以看到,即使没有传responseType的时候,返回的数据类型仍然为json。至此,我们已经实现了转换request和response的转换数据的步骤,当然,这只是正常情况下的处理,我们还需要处理异常情况。

异常情况处理

通常情况下,当一个程序出现异常时,我们希望在控制台里能看到更多信息,包括对应的错误提示。因此,我们需要捕获Xhr(XMLHttpRequest)中的异常,并对这些信息进行增强处理,以便开发者更好地排查问题。 首先,我们来看看源码是怎么处理这个部分的: github.com/axios/axios…

源码当中处理了几种情况:

  • http状态码处理
  • 超时处理
  • 状态码非200的处理

由此,我们可以在xhr中创建一个函数

function handleResponse(response: AxiosResponse): void {
	if (response.status >= 200 && response.status < 300) {
		resolve(response)
	} else {
		reject(
			createError(
				`Request fail with status code ${response.status}`,
				config,
				undefined,
				request,
				response
			)
		)
	}
}

改造一下AxiosRequestConfig

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

    const request = new XMLHttpRequest()
    // set response Type
    if (responseType) {
      request.responseType = responseType
    }
    if (timeout) {
      request.timeout = timeout
    }

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

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

    request.onreadystatechange = function handleLoad() {
      if (request.readyState !== 4) {
        return
      }

      if (request.status === 0) {
        return
      }

      const responseHeaders = parseHeaders(request.getAllResponseHeaders())
      const responseData = responseType !== 'text' ? request.response : request.responseText
      const response: AxiosResponse = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config,
        request
      }
      handleResponse(response)
    }
		request.onerror = function handleError() {
      reject(createError('Network Error', config, undefined, request))
    }
// 代码省略...

总结

通过上述步骤,我们已经实现了将请求和响应数据进行转换的正常逻辑以及异常捕获功能。接下来的章节中,我们将会创建一个名为 AxiosError 的类,以实现对错误信息的增强处理。