有几个问题,我们可以先思考一下,是否知道答案?
- 为什么 axios 既可以当函数调用,也可以当对象使用,比如axios({})、axios.get。
- axios 的调用流程是怎样的?
- 拦截器吗原理是怎样的?
- axios的取消功能是怎么实现的?
- 为什么支持浏览器中发送请求也支持node发送请求?
如果对于以上的问题还是一知半解或根本没听说,那么是时候深入了解下axios了~
为了更好地了解axios源码,我们先手写一个发送请求的小demo,掌握了大体流程之后,阅读axios源码会更加得心应手。
手写mini axios
初步搭建的框架如下:
- 定义一种字符串字面量类型
Method
:
export type Method = 'get' | 'GET'
| 'delete' | 'Delete'
| 'head' | 'HEAD'
| 'options' | 'OPTIONS'
| 'post' | 'POST'
| 'put' | 'PUT'
| 'patch' | 'PATCH'
复制代码
- 定义
AxiosRequestConfig
接口类型:
export interface AxiosRequestConfig {
url: string
method?: Method
data?: any
params?: any,
headers?: any
}
复制代码
- 封装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)
}
复制代码
- 定义axios类,引入xhr方法
import { AxiosRequestConfig } from './types'
import xhr from './xhr'
function axios(config: AxiosRequestConfig): void {
xhr(config)
}
export default axios
复制代码
到此为止,发送请求的最小闭环已经实现了,之后会逐渐剖析其他功能。
1. 处理url
我们使用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
send
方法的参数支持 Document
和 BodyInit
类型,BodyInit
包括了 Blob
, BufferSource
, FormData
, URLSearchParams
, ReadableStream
、USVString
,当没有数据的时候,我们还可以传入 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
上面我们把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
如何实现服务端处理响应数据,并且返回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
通过 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
服务器返回给我们的是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. 常见错误监听与处理
- 监听网络异常
在xhr方法中添加:
request.onerror = function handleError() {
reject(new Error('网络错误'))
}
复制代码
- 网络超时
const { /*...*/ timeout } = config
if (timeout) {
request.timeout = timeout
}
request.ontimeout = function handleTimeout() {
reject(new Error(`Timeout of ${timeout} ms exceeded`))
}
复制代码
- 非 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文件
这个文件的作用:
- 暴露了一个默认的Axios实例
- 暴露了创建Axios实例的工厂方法
- 暴露了Axios类方便继承
- 暴露了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文件
这个文件定义了Axios类,其中包括了以下几点:
- 定义Axios类
- 定义了Axios.prototype.request(包括配置与处理、拦截器处理)
- 定义了Axios.prototype.delete, Axios.prototype.get, Axios.prototype.head, Axios.prototype.options(不需要payload)
- 定义了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的源码:
我们可以看到这段代码是非常容易读的,包括了以下几点:
- InterceptorManager类
- 原型上的use方法——添加handler
- 原型上的eject方法——删除handler
- 原型上的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.forEach
、this.interceptors.response.forEach
进行拦截器的遍历。
我们来想一想,目前的request拦截器和response拦截器有哪些作用呢?
request拦截器:
- 记录每次request的时间和内容
- 请求发送之前呈现loading效果
- 给request加额外的数据
- 验证request是否有合法权限
- 对请求数据进行统一预处理
- ……
response拦截器:
- 记录接收到响应的时间,从而可以记录整个请求花销的时间
- 处理完请求后呈现请求已完成的效果
- 网络抖动情况下使用重试机制
- 更改response状态码
- 对响应数据进行统一预处理
- ……
4. dispatchRequest文件
我们爱看看Axios.prototype.request
里做了什么:
- 用一个链表 把 request拦截器(interceptors.request) + 真正发起request(dispatchRequest) + response拦截器(interceptors.response) 连起来
- 用 promise 逐个调用这个链表
- 返回 promise 给调用者
其中,有个核心的函数是dispatchRequest
,dispatchRequest
的代码也清晰易懂,其中包含了对于request 和 response 的处理 —— transformRequest
和 transformResponse
。(可以具体看dispatchRequest.js的源码)。
transformRequest做了以下的操作:
- 规范化header中的Content-Type
- 规范化request的data类型
- 根据数据类型补充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;
}]
复制代码
解答
开头提出的问题的答案:
- 为什么 axios 既可以当函数调用,也可以当对象使用,比如axios({})、axios.get。
答:axios本质是函数,赋值了一些别名方法,比如get、post方法,可被调用,最终调用的还是Axios.prototype.request函数。
- 简述 axios 调用流程。
答:实际是调用的Axios.prototype.request方法,最终返回的是promise链式调用,实际请求是在dispatchRequest中派发的。
- 拦截器吗原理是怎样的?
答:用axios.interceptors.request.use添加请求成功和失败拦截器函数,用axios.interceptors.response.use添加响应成功和失败拦截器函数。在Axios.prototype.request函数组成promise链式调用时,Interceptors.protype.forEach遍历请求和响应拦截器添加到真正发送请求dispatchRequest的两端,从而做到请求前拦截和响应后拦截。拦截器也支持用Interceptors.protype.eject方法移除。
- axios的取消功能是怎么实现的?
答:通过传递config配置cancelToken的形式,来取消的。判断有传cancelToken,在promise链式调用的dispatchRequest抛出错误,在adapter中request.abort()取消请求,使promise走向rejected,被用户捕获取消信息。
- 为什么支持浏览器中发送请求也支持node发送请求?
答:axios.defaults.adapter默认配置中根据环境判断是浏览器还是node环境,使用对应的适配器。适配器支持自定义。