Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中, axios 的特点:
从 axios 源码入手
{ [Function: wrap]
request: [Function: wrap],
getUri: [Function: wrap], // 别名函数, 发送 get 请求
delete: [Function: wrap], // 别名函数, 发送 delete 请求
get: [Function: wrap], // 别名函数, 发送 get 请求
head: [Function: wrap], // 别名函数, 发送 head 请求
options: [Function: wrap], // 别名函数, 发送 options 请求
post: [Function: wrap], // 别名函数, 发送 post 请求
put: [Function: wrap], // 别名函数, 发送 put 请求
patch: [Function: wrap],
defaults:
{ adapter: [Function: httpAdapter],
transformRequest: [ [Function: transformRequest] ],
transformResponse: [ [Function: transformResponse] ],
timeout: 0,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
validateStatus: [Function: validateStatus],
headers:
{ common: [Object],
delete: {},
get: {},
head: {},
post: [Object],
put: [Object],
patch: [Object] } },
interceptors:
{ request: InterceptorManager { handlers: [] },
response: InterceptorManager { handlers: [] } },
Axios: [Function: Axios],
create: [Function: create],
Cancel: [Function: Cancel],
CancelToken: { [Function: CancelToken] source: [Function: source] },
isCancel: [Function: isCancel],
all: [Function: all],
spread: [Function: spread],
default: [Circular] }
在 node 环境中打印出来的结果中可以看出,axios 暴露出来的一些属性和方法
抛开 webpack 打包的代码,这里就直接从 494 行开始,__webpack_require__(11)
方法(846行),该方法内部声明配置了 axios 的一些默认配置
以下是 axios 主要依赖模块
var utils = __webpack_require__(2); // 工具方法
var buildURL = __webpack_require__(6); // 地址构建器
var InterceptorManager = __webpack_require__(7); // 拦截器
var dispatchRequest = __webpack_require__(8); // 请求调度器
var mergeConfig = __webpack_require__(22); // 合并配置
每实例化一次 axios 时默认总会给当前实例增加一个 defaults
属性 和 interceptors
对象,前者表示该实例的默认配置,后者则表示一个拦截器, InterceptorManager 内部用一个 handlers
栈维护,每个拦截器都会有 fulfilled 和 rejected 两个方法,
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
该方法是整个 axios 请求入口,方法的开始和 jQuery.prototype.ajax 一样都是对参数作处理,也就是说可以直接传入 url 地址, 或者传入 url,再传入一个配置项对象
源码 537 行
var chain = [dispatchRequest, undefined]; // Line:537
var promise = Promise.resolve(config); // Line:538
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift()); // Line:549
}
return promise;
538行 由 ES6 的 Promise 实现可得出, Promise.resolve(config) 是为了将 config 创建成一个 promise 对象, 而 537 行创建了一个数组包裹的请求调度器,将注册在拦截器中的所有 fulfilled 和 rejected 方法依次放入请求调度器和 undefined 占位前后
特别注意第 537 行, 因为 promise 在 538 行时,状态已经为 resolved 了,也就是说已经完成了,也就会从头触发到尾巴,但是中途会发现 shift() 方法会经过 dispatchRequest, undefined
这两个占位符,当然此时的 then 永远会 reslove,永远不会 reject,因为 rejected 是一个 undefined,这也是为什么会出现 undefined 占位符原因
此处后面的 response 响应的拦截器也会永远的被执行,但再那之前,会经过请求调度器,所以目转调度器
补充一段实例,该实例是在浏览器环境中,当前保证能够成功响应时
const url = 'axios-test-json.json'
axios.interceptors.request.use( function rs1(config){
console.log('request 1 success')
return config
}, function re1 (error){
console.log('response 1 error', error)
return Promise.reject(error)
})
axios.interceptors.response.use( function rs2(config){
console.log('request 2 success')
return config
}, function re2 (error){
console.log('response 2 error', error)
return Promise.reject(error)
})
axios.request(url).then( r => console.log('response'))
// request 1 success
// request 2 success
// response
特别注意 Promise.resolve(config) 是将 config 包装成一个 promise 对象, ES6 的该方法实现有介绍, config 在默认情况下是不存在 then 方法的,也就是说 config 是一个不带 then 方法的对象,所以返回的 Promise 对象状态为 fulfilled,并且将该value传递给对应的then方法,这里在的 then 方法就是 549 行的 then,因为当前的状态直接为 fulfilled, 所以不仅将请求之前需要执行的拦截器的成功回调全部依次执行
这也是为什么 rs1 和 rs2 两个方法都被直接 fulfilled 执行了
如果当响应不成功时, 在响应的拦截器会将会依次执行,特别注意!!!
接下来就是源码第 779 行,利用 adapter 请求处理器,对 config 内容进行 promise 操作,取消操作的地方都用了 throwIfCancellationRequested
阻止当次操作
__webpack_require__(13)
创建默认 config 的方法,来到源码 970 行
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
request.onreadystatechange = function handleLoad() {
settle(resolve, reject, response)
request = null
}
request.onabort = function handleAbort() {}
request.onerror = function handleError() {}
request.ontimeout = function handleTimeout() {}
})
}
该方法是 defaluts.adapter 的源头。 axios 是可以支持 node 和浏览器环境的,虽然是对底层做了封装,但是对于过老 IE 这样的浏览器是不支持 XMLHttpRequest
对象的,也就不支持过老的版本
Content-Type
头部字段[200, 300)
区间视为成功此处的响应完全是原生的响应内容,需要返回给请求调度器对响应进行转换才是最后请求成功后的样子
直接来到源码的 __webpack_require__(8)
方法(717行),该方法中引入的更多的模块
var utils = __webpack_require__(2);
var transformData = __webpack_require__(9); // 请求响应转换工具
var isCancel = __webpack_require__(10); // 取消操作时付加对象,有个内部属性 __CANCEL__
var defaults = __webpack_require__(11); // 默认配置
var isAbsoluteURL = __webpack_require__(20); // URL 判断工具
var combineURLs = __webpack_require__(21); // 组全 URL
isAbsoluteURL
判断 URL 是否是一个绝对路径,遵守 RFC 3986
编码规范方案
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}
该方法简短,也没什么其它操作,但该方法在源码其它位置往往都在主方法体内的第一行,如注释其就是为了判断该实例是否取消了请求,而这个标识就是config.cancelToken
源码 777 行:
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
// 成功时对此次请求进行取消检查
throwIfCancellationRequested(config);
// Transform response data
response.data = transformData(
response.data,
response.headers,
config.transformResponse
);
return response;
}, function onAdapterRejection(reason) {
if (!isCancel(reason)) {
// 失败时对此次请求进行取消检查
throwIfCancellationRequested(config);
// Transform response data
if (reason && reason.response) {
reason.response.data = transformData(
reason.response.data,
reason.response.headers,
config.transformResponse
);
}
}
return Promise.reject(reason);
});
config.adapter 就是用户自定义请求的来源,在 axios 内部,默认每个实例都会有一个请求处理方法 defaults.adapter
, 但值得注意的是该方法需要返回一个 Promise 来处理后续操作并且还得提供一个有效的响应, axios 官方上解释,在当前的请求前后会分别执行转换和拦截,转换则是转换请求或响应,拦截则是拦截器,详情可见例子
axios 封装第一代 xhr 使用 withCredentials
共享
最后无论成功失败,将响应转换成 axios 独特格式 ☺
其实这个转换默认只是判断是否为字符串,如果是字符串则直接 JSON.parse()
否则啥也做,当然,这个转换规则可以是多个,默认是只有一个的,最后回到原点, 源码 549 行,将最后的 promise 返回给用户
响应以用请求的转换可以具体参考 config.transformResponse
配置
个人认为在此源码 549 行是整个 axios 的核心,因为它的奇妙设计,利用栈队列这个样的数据结构,完美的实现了请求拦截器,请求处理和响应拦截器之间的次序,很直观的对机器表达出了自己想要做的事,个人很佩服这一点。
当请求的目标地址和当前网站地址的 URL 端口不一样,或域名,或协议一样,满足其中任何一个的请求就会触发浏览器的同源策略限制,也就是不让你访问,完美的跨域解决是前后两端共同商讨决策
以下是可通过跨域访问的几种方案:
req 发送 origin 字段, res 响应Access-Control-Allow-Origin
,如果 origin 来源在 Access-Control-Allow-Origin 中则是达成 CORS,这也是简单请求完成的最简单的访问控制,使用该方案应注意下几点:
Access-Control-Allow-Origin
字段可以是*
Access-Control-Allow-Origin
不能在是*
应该具体到某一个访问域下面Access-Control-Allow-Credentials
字段,虽然可以资源共享了,但那也是后端可以支持,并不代表前后两端都能接收和发送,所以前端还必须将 Credentials
请求字段设置为 true
注意 application/json 或者是 application/xml 已经不满足简单请求,所以该 content-type 就应该需要预检
下面来构建一次简单请求和需要预检请求的环境
首先由于 Chrome 的 Provisional headers are shown
临时响应头问题所以客户端运行在FF http://local.notetest.com:81
浏览器环境
服务器用 node http 模块http://localhost:8080
起一个服务器,关于 node 可详情可见 nodejs http
客户端请求:
// http://local.notetest.com:81/index.html
const url = 'http://localhost:8080'
// 浏览器使用原生二代 fetch 请求
fetch(new Request(url)).then( r => console.log(r))
node 服务器端:
const http = require('http')
http.createServer( (request, response) => {
const host = request.headers.origin // 请求地址
// 打印源地址和请求方法
console.log(`Host: ${host} Method: ${request.method}`)
response.writeHead(200, {
'Content-Type': 'text/plain', // 简单请求普通文本
'Access-Control-Allow-Origin' : '*', // 允许所有源地址跨域访问
'Set-Cookie': 'money=1000' // 默认始终设置一个 cookie
})
response.end('{ "name" : "qlover", "age" : 21 }')
}).listen(8080)
浏览器中直接访问, 服务器会在命令行中打印出 Host: http://local.notetest.com:81 Method: GET
, 该次请求属于简单请求,请求和响应的头部信息如下
# 请求
请求网址:http://localhost:8080/
请求方法:GET
远程地址:127.0.0.1:8080
版本:HTTP/1.1
Referrer 政策:no-referrer-when-downgrade
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: http://local.notetest.com:81/
Origin: http://local.notetest.com:81
Connection: keep-alive
Cache-Control: max-age=0
# 响应
HTTP/1.1 200 OK
Content-Type: text/plain
Access-Control-Allow-Origin: *
Set-Cookie: money=1000
Date: Sun, 22 Dec 2019 09:15:08 GMT
Connection: keep-alive
Transfer-Encoding: chunked
# 服务器打印信息
Host: http://local.notetest.com:81 Method: GET
上述的简单请求使用 fetch 直接成功,后台设置Access-Control-Allow-Origin
字段为*
,简单请求直接通过,请求 content-type 指定为 json 格式返回,这样就不满足简单请求
fetch(new Request(url, {
mode: 'cors',
headers: {
'Content-Type': 'application/json'
}
})).then( r => console.log(r))
node 服务器打印结果为 Host: http://local.notetest.com:81 Method: OPTIONS
, 说明这个时间已经不再是简单,进行了预检请求,这个时的请求会在前台控制台打印抛出错误
Access to XMLHttpRequest at 'http://localhost:8080/' from origin 'http://local.notetest.com:81' has been blocked by CORS policy: Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.
这个时候的服务器端并未允许 content-type 字段,并且响应数据还只是 text/plain 所以会报错,接下来 node 允许Content-Type
字段并改变服务器的响应内容为 json
http.createServer( (request, response) => {
const host = request.headers.origin // 请求地址
// 打印源地址和请求方法
console.log(`Host: ${host} Method: ${request.method}`)
response.writeHead(200, {
'Content-Type': 'application/json', // 返回 json
'Access-Control-Allow-Origin' : '*', // 允许所有源地址跨域访问
'Access-Control-Allow-Headers' : 'Content-Type', // 允许请求通过 Content-Type 字段
'Set-Cookie': 'money=1000' // 默认始终设置一个 cookie
})
response.end('{ "name" : "qlover", "age" : 21 }')
}).listen(8080)
此时会先以 OPTION 方式进行预检请求,并且请求响应头部如下:
# 请求
请求网址:http://localhost:8080/
请求方法:OPTIONS # 请求方法为 options
远程地址:127.0.0.1:8080
版本:HTTP/1.1
Referrer 政策:no-referrer-when-downgrade
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Access-Control-Request-Method: GET
Access-Control-Request-Headers: content-type
Referer: http://local.notetest.com:81/
Origin: http://local.notetest.com:81
Connection: keep-alive
Cache-Control: max-age=0
# 响应
HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Content-Type
Set-Cookie: money=1000
Date: Sun, 22 Dec 2019 09:12:16 GMT
Connection: keep-alive
Transfer-Encoding: chunked
# 服务器打印结果
Host: http://local.notetest.com:81 Method: OPTIONS
当预检请求通过,之后的 post 请求就会当作实际的请求发送出去,这个时候的请求和响应头信息如下
#请求
请求网址:http://localhost:8080/
请求方法:GET
远程地址:127.0.0.1:8080
版本:HTTP/1.1
Referrer 政策:no-referrer-when-downgrade
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0
Accept: application/json, text/plain, */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/json;charset=utf-8
Content-Length: 35
Origin: http://local.notetest.com:81
Connection: keep-alive
Referer: http://local.notetest.com:81/
# 响应
HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Content-Type
Set-Cookie: money=1000
Date: Sun, 22 Dec 2019 08:37:57 GMT
Connection: keep-alive
Transfer-Encoding: chunked
# 实际请求发送后服务器打印结果
Host: http://local.notetest.com:81 Method: OPTIONS
Host: http://local.notetest.com:81 Method: POST
上述完成了一次非简单请求,仔细的话会发现,每次 node 都在响应付加了 cookie ,前端的 cookie 却一致获取不到,并且每一次的请求头都没有 cookie 信息,这是因为前后都没有允许跨域通过响应内容,接下来利用Access-Control-Allow-Credentials
字段允许前后端通过响应内容,并且要注意 origin 只能是具体的源地址
fetch(new Request(url, {
mode: 'cors',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include'
})).then( r => console.log(r))
fetch 属于第二代原生请求方式,使用 credentials = 'include'
字段允许传递共享数据,而原生一代请求方式使用 withCredentials = true
字段(axios 属于对原生一代 xhr 的封装,所以也使用 withCredentials 字段)
http.createServer( (request, response) => {
const host = request.headers.origin // 请求地址
// 打印源地址和请求方法
console.log(`Host: ${host} Method: ${request.method}`)
response.writeHead(200, {
'Content-Type': 'application/json', // 简单请求普通文本
'Access-Control-Allow-Credentials': true, // 允许跨域通过 cookie
'Access-Control-Allow-Origin' : host, // 只能是具体源地址
'Access-Control-Allow-Headers' : 'Content-Type',
'Set-Cookie': 'money=1000' // 默认始终设置一个 cookie
})
response.end('{ "name" : "qlover", "age" : 21 }')
}).listen(8080)
预检查请求的请求和响应如下:
# 请求
请求网址:http://localhost:8080/
请求方法:OPTIONS
远程地址:127.0.0.1:8080
版本:HTTP/1.1
Referrer 政策:no-referrer-when-downgrade
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Access-Control-Request-Method: GET
Access-Control-Request-Headers: content-type
Referer: http://local.notetest.com:81/
Origin: http://local.notetest.com:81
Connection: keep-alive
Cache-Control: max-age=0
# 响应
HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://local.notetest.com:81
Access-Control-Allow-Headers: Content-Type
Set-Cookie: money=1000; path=/ # 服务器返回 cookie
Date: Sun, 22 Dec 2019 09:04:18 GMT
Connection: keep-alive
Transfer-Encoding: chunked
# 服务器打印
Host: http://local.notetest.com:81 Method: OPTIONS
实际请求:
# 请求
请求网址:http://localhost:8080/
请求方法:GET
远程地址:127.0.0.1:8080
版本:HTTP/1.1
Referrer 政策:no-referrer-when-downgrade
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: http://local.notetest.com:81/
Content-Type: application/json
Origin: http://local.notetest.com:81
Connection: keep-alive
Cookie: money=1000 # !!! 第二次实际请求已经将上一次的预检请求携带上了
Cache-Control: max-age=0
# 响应
HTTP/1.1 200 OK
Content-Type: application/json
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://local.notetest.com:81
Access-Control-Allow-Headers: Content-Type, Set-Cookie, *
Set-Cookie: money=1000; path=/ # 服务器同样返回 cookie
Date: Sun, 22 Dec 2019 09:06:16 GMT
Connection: keep-alive
Transfer-Encoding: chunked
# 服务器打印
Host: http://local.notetest.com:81 Method: OPTIONS
Host: http://local.notetest.com:81 Method: GET
以上就是跨域时会遇见的三种情况