最近在看项目中一些封装好的基础请求包的实现代码,也顺便复习了下 Fetch & Axios 这两个基础的 HTTP 请求库的一些知识点,以及对 Axios 一部分源码的介绍。
Fetch API
fetch(url, optionObj)
基本概念
fetch() 的功能与 XMLHttpRequest 基本相同,但有三个主要的差异。
-
fetch()使用 Promise,不使用回调函数,因此大大简化了写法,写起来更简洁。 -
fetch()采用模块化设计,API 分散在多个对象上(Response对象、Request对象、Headers对象),更合理一些;相比之下,XMLHttpRequest 的 API 设计并不是很好,输入、输出、状态都在同一个接口管理,容易写出非常混乱的代码。 -
fetch()通过数据流(Stream 对象)处理数据,可以分块读取,有利于提高网站性能表现,减少内存占用,对于请求大文件或者网速慢的场景相当有用。XMLHTTPRequest对象不支持数据流,所有的数据必须放在缓存里,不支持分块读取,必须等待全部拿到后,再一次性吐出来。
在用法上, fetch() 接受一个 URL 字符串作为参数,默认向该网址发出 GET 请求,返回一个 `Promise 对象。它的基本用法如下。
fetch('https://api.github.com/users/ruanyf')
.then(response => response.json())
.then(json => console.log(json))
.catch(err => console.log('Request Failed', err));
上面示例中, fetch() 接收到的 response 是一个 Stream 对象, response.json() 是一个异步操作,取出所有内容,并将其转为 JSON 对象。Promise 可以用 await 改写,从而有更加清晰的语义。
async function getJSON() {
let url = 'https://api.github.com/users/ruanyf';
try {
let response = await fetch(url);
return await response.json();
//await语句必须放在try...catch里面,这样才能捕捉异步操作中可能发生的错误。
} catch (error) {
console.log('Request Failed', error);
}
}
Fetch 配置对象
const response = fetch(url, {
method: "GET", // HTTP 请求的方法
headers: {
"Content-Type": "text/plain;charset=UTF-8"
}, // HTTP 请求的标头
body: undefined, // POST 请求的数据体
referrer: "about:client",
referrerPolicy: "no-referrer-when-downgrade",
mode: "cors",
credentials: "same-origin",
cache: "default",
redirect: "follow",
integrity: "",
keepalive: false,
signal: undefined
});
cache : 用于指定如何处理缓存。
default:默认值,先在缓存里面寻找匹配的请求。no-store:直接请求远程服务器,并且不更新缓存。reload:直接请求远程服务器,并且更新缓存。no-cache:将服务器资源跟本地缓存进行比较,有新的版本才使用服务器资源,否则使用缓存。force-cache:缓存优先,只有不存在缓存的情况下,才请求远程服务器。only-if-cached:只检查缓存,如果缓存里面不存在,将返回504错误。
mode : 用于指定请求的模式。
cors:默认值,允许跨域请求。same-origin:只允许同源请求。no-cors:请求方法只限于 GET、POST 和 HEAD,并且只能使用有限的几个简单标头,不能添加跨域的复杂标头,相当于提交表单所能发出的请求。
credential : 用于指定是否发送Cookie。
same-origin:默认值,同源请求时发送 Cookie,跨域请求时不发送。include:不管同源请求,还是跨域请求,一律发送 Cookie。跨域请求发送 Cookie,需要将credentials属性设为include。omit:一律不发送。
keepalive : 用于页面卸载时,告诉浏览器在后台保持连接,继续发送数据。
redirect : 用于指定 HTTP 跳转的处理方法。
follow:默认值,fetch()跟随 HTTP 跳转。error:如果发生跳转,fetch()就报错。manual:fetch()不跟随 HTTP 跳转,但是response.url属性会指向新的 URL,response.redirected属性会变为true,由开发者自己决定后续如何处理跳转。
referrer : 用于设定 fetch() 请求的 referer 标头。
这个属性可以为任意字符串,也可以设为空字符串(即不发送referer标头)。
referrerPolicy : 用于设定 referrer 标头的规则。
no-referrer-when-downgrade:默认值,总是发送Referer标头,除非从 HTTPS 页面请求 HTTP 资源时不发送。no-referrer:不发送Referer标头。- **
origin:Referer**标头只包含域名,不包含完整的路径。 - **
origin-when-cross-origin:同源请求Referer**标头包含完整的路径,跨域请求只包含域名。 same-origin:跨域请求不发送Referer****,同源请求发送。strict-origin:Referer标头只包含域名,HTTPS 页面请求 HTTP 资源时不发送Referer标头。strict-origin-when-cross-origin:同源请求时Referer标头包含完整路径,跨域请求时只包含域名,HTTPS 页面请求 HTTP 资源时不发送该标头。unsafe-url:不管什么情况,总是发送Referer标头。
signal: 指定一个 AbortSignal 实例,用于取消 fetch() 请求。
fetch() 请求发送以后,如果中途想要取消,需要使用 AbortController 对象。配置对象的 signal 属性必须指定接收 AbortController 实例发送的信号 controller.signal 。 controller.abort() 方法用于发出取消信号。这时会触发 abort 事件,这个事件可以监听,也可以通过 controller.signal.aborted 属性判断取消信号是否已经发出。
let controller = new AbortController();
let signal = controller.signal;
fetch(url, {
signal: controller.signal
});
signal.addEventListener('abort',
() => console.log('abort!')
);
controller.abort(); // 取消
console.log(signal.aborted); // true
一个一秒后自动取消请求的🌰
let controller = new AbortController();
setTimeout(() => {
controller.abort()
}, 1000);
try {
let response = await fetch('/long-operation', {
signal: controller.signal
});
} catch(err) {
if (err.name === 'AbortError') {
console.log('Aborted')';
} else {
throw err;
}
}
Response 对象
Response 对象属性
Response.headers: Response 所关联的 Headers 对象。
你可以通过 Request.headers 和Response.headers 属性检索一个Headers对象, 并使用 Headers.Headers() 构造函数创建一个新的Headers 对象。Headers 对象可以使用for...of循环进行遍历。
const response = await fetch(url);
for (let [key, value] of response.headers) {
console.log(`${key} : ${value}`);
}
// 或者
for (let [key, value] of response.headers.entries()) {
console.log(`${key} : ${value}`);
}
-
Headers.append(): 给现有的header添加一个值, 或者添加一个未存在的header并赋值. -
Headers.delete(): 从Headers对象中删除指定header。 -
Headers.entries()\Headers.keys()\Headers.values(): 以迭代器的形式返回Headers对象中所有的键值对 \ header 名 \ header 值。 -
Headers.get: 以ByteString的形式从Headers对象中返回指定header的全部值。 -
Headers.has: 以布尔值的形式从Headers对象中返回是否存在指定的header。 -
Headers.set: 替换现有的header的值, 或者添加一个未存在的header并赋值。Headers.set()将会用新的值覆盖已存在的值, 但是Headers.append()会将新的值添加到已存在的值的队列末尾。let myHeaders = new Headers();
myHeaders.append('Content-Type', 'text/xml');
myHeaders.get('Content-Type'); // should return 'text/xml'
-
Response.ok: 包含了一个布尔值,标示该 Response 成功(HTTP 状态码的范围在 200-299),网址跳转(3XX)的状态码会被自动转成200。 -
Response.status: 包含 Response 的状态码 (例如 200 表示成功)。 -
Response.statusText: 包含了与该 Response 状态码一致的状态信息(例如,OK对应 200)。 -
Response.redirected: 表示该 Response 是否来自一个重定向,如果是的话,它的 URL 列表将会有多个条目。 -
Response.url: 包含 Response 的URL。 -
Response.type: 包含 Response 的类型(例如,basic、cors)。 -
basic:普通请求,即同源请求。
-
cors:跨域请求。
-
error:网络错误,主要用于 Service Worker。
-
opaque:如果fetch()请求的type属性设为no-cors,就会返回这个值,详见请求部分。表示发出的是简单的跨域请求,类似
表单的那种跨域请求。
opaqueredirect:如果fetch()请求的redirect属性设为manual,就会返回这个值,详见请求部分。
Response.body: 一个简单的 getter,用于暴露一个 ReadableStream 类型的 body 内容。Response.bodyUsed: 包含了一个布尔值来标示该 Response 是否读取过 Body。
注意: fetch() 发出请求后,只有网络错误或者无法链接时,才会报错。其他情况都不会报错,而是认为请求成功。 也就是说,即使服务器返回的状态码是 4xx 或 5xx, fetch() 也不会报错(即 Promise 不会变为 rejected 状态)。只有通过 Response.status 属性,得到 HTTP 回应的真实状态码,才能判断请求是否成功。
async function fetchText() {
let response = await fetch('/readme.txt');
if (response.status >= 200 && response.status < 300) {
return await response.text();
} else {
throw new Error(response.statusText);
}
}
另一种方法
if (response.ok) {
// 请求成功
} else {
// 请求失败
}
Response对象的方法
数据读取方法
异步方法,返回的都是 Promise 对象。必须等到异步操作结束,才能得到服务器返回的完整数据。
-
response.text():得到文本字符串。 -
response.json():得到 JSON 对象。 -
response.blob():得到二进制 Blob 对象。const response = await fetch('flower.jpg'); const myBlob = await response.blob(); const objectURL = URL.createObjectURL(myBlob);
const myImage = document.querySelector('img'); myImage.src = objectURL;
-
response.formData():得到 FormData 表单对象。 -
response.arrayBuffer():得到二进制 ArrayBuffer 对象,主要用于获取流媒体文件。const audioCtx = new window.AudioContext(); const source = audioCtx.createBufferSource();
const response = await fetch('song.ogg'); const buffer = await response.arrayBuffer();
const decodeData = await audioCtx.decodeAudioData(buffer); source.buffer = buffer; source.connect(audioCtx.destination); source.loop = true;
其他方法
-
Response.clone(): 创建一个 Response 对象的克隆。 -
Response.error(): 返回一个绑定了网络错误的新的 Response 对象。 -
Response.redirect: 用另一个 URL 创建一个新的 Response。
Axios
Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。Axios 会自动判断当前环境是浏览器还是node,如果是浏览器,就会基于 XMLHttpRequests 实现axios。如果是node.js环境,就会基于node内置核心模块 http 实现axios。Axios的特性主要包括:
- 从浏览器中创建 XMLHttpRequests
- 从 node.js 创建 http 请求
- 支持 Promise API
- 拦截请求和响应
- 转换请求数据和响应数据
- 取消请求
- 自动转换 JSON 数据
- 客户端支持防御 XSRF
Axios API
axios(config)
// Send a POST request
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
});
// GET request for remote image in node.js
axios({
method: 'get',
url: 'http://bit.ly/2mTM3nY',
responseType: 'stream'
})
.then(function (response) {
response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'))
});
axios(url[, config])
// Send a GET request (default method)
axios('/user/12345');
axios.request(config)
axios.get(url[, config])
axios.delete(url[, config])
axios.head(url[, config])
axios.options(url[, config])
axios.post(url[, data[, config]])
axios.put(url[, data[, config]])
axios.patch(url[, data[, config]])
创建 Axios 实例
axios.create([config])
const instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
});
Axios 实例方法
axios#request(config)
axios#get(url[, config])
axios#delete(url[, config])
axios#head(url[, config])
axios#options(url[, config])
axios#post(url[, data[, config]])
axios#put(url[, data[, config]])
axios#patch(url[, data[, config]])
axios#getUri([config])
当使用实例方法时,传入的 config 将与创建实例时的 instance config 合并成最终使用的 config 。
请求配置
{
// `url` 是用于请求的服务器 URL
url: '/user',
// `method` 是创建请求时使用的方法
method: 'get', // 默认是 get
// `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
// 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
baseURL: 'https://some-domain.com/api/',
// `transformRequest` 允许在向服务器发送前,修改请求数据
// 只能用在 'PUT', 'POST' 和 'PATCH' 这几个请求方法
// 后面数组中的函数必须返回一个字符串,或 ArrayBuffer,或 Stream
transformRequest: [function (data) {
// 对 data 进行任意转换处理
return data;
}],
// `transformResponse` 在传递给 then/catch 前,允许修改响应数据
transformResponse: [function (data) {
// 对 data 进行任意转换处理
return data;
}],
// `headers` 是即将被发送的自定义请求头
headers: {'X-Requested-With': 'XMLHttpRequest'},
// `params` 是即将与请求一起发送的 URL 参数
// 必须是一个无格式对象(plain object)或 URLSearchParams 对象
params: {
ID: 12345
},
// `paramsSerializer` 是一个负责 `params` 序列化的函数
// (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/)
paramsSerializer: function(params) {
return Qs.stringify(params, {arrayFormat: 'brackets'})
},
// `data` 是作为请求主体被发送的数据
// 只适用于这些请求方法 'PUT', 'POST', 和 'PATCH'
// 在没有设置 `transformRequest` 时,必须是以下类型之一:
// - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
// - 浏览器专属:FormData, File, Blob
// - Node 专属: Stream
data: {
firstName: 'Fred'
},
// `timeout` 指定请求超时的毫秒数(0 表示无超时时间)
// 如果请求话费了超过 `timeout` 的时间,请求将被中断
timeout: 1000,
// `withCredentials` 表示跨域请求时是否需要使用凭证
withCredentials: false, // 默认的
// `adapter` 允许自定义处理请求,以使测试更轻松
// 返回一个 promise 并应用一个有效的响应 (查阅 [response docs](#response-api)).
adapter: function (config) {
/* ... */
},
// `auth` 表示应该使用 HTTP 基础验证,并提供凭据
// 这将设置一个 `Authorization` 头,覆写掉现有的任意使用 `headers` 设置的自定义 `Authorization`头
auth: {
username: 'janedoe',
password: 's00pers3cret'
},
// `responseType` 表示服务器响应的数据类型,可以是 'arraybuffer', 'blob', 'document', 'json', 'text', 'stream'
responseType: 'json', // 默认的
// `xsrfCookieName` 是用作 xsrf token 的值的cookie的名称
xsrfCookieName: 'XSRF-TOKEN', // default
// `xsrfHeaderName` 是承载 xsrf token 的值的 HTTP 头的名称
xsrfHeaderName: 'X-XSRF-TOKEN', // 默认的
// `onUploadProgress` 允许为上传处理进度事件
onUploadProgress: function (progressEvent) {
// 对原生进度事件的处理
},
// `onDownloadProgress` 允许为下载处理进度事件
onDownloadProgress: function (progressEvent) {
// 对原生进度事件的处理
},
// `maxContentLength` 定义允许的响应内容的最大尺寸
maxContentLength: 2000,
// `validateStatus` 定义对于给定的HTTP 响应状态码是 resolve 或 reject promise 。如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),promise 将被 resolve; 否则,promise 将被 rejecte
validateStatus: function (status) {
return status >= 200 && status < 300; // 默认的
},
// `maxRedirects` 定义在 node.js 中 follow 的最大重定向数目
// 如果设置为0,将不会 follow 任何重定向
maxRedirects: 5, // 默认的
// `httpAgent` 和 `httpsAgent` 分别在 node.js 中用于定义在执行 http 和 https 时使用的自定义代理。允许像这样配置选项:
// `keepAlive` 默认没有启用
httpAgent: new http.Agent({ keepAlive: true }),
httpsAgent: new https.Agent({ keepAlive: true }),
// 'proxy' 定义代理服务器的主机名称和端口
// `auth` 表示 HTTP 基础验证应当用于连接代理,并提供凭据
// 这将会设置一个 `Proxy-Authorization` 头,覆写掉已有的通过使用 `header` 设置的自定义 `Proxy-Authorization` 头。
proxy: {
host: '127.0.0.1',
port: 9000,
auth: : {
username: 'mikeymike',
password: 'rapunz3l'
}
},
// `cancelToken` 指定用于取消请求的 cancel token
cancelToken: new CancelToken(function (cancel) {
})
}
相应结构
{
// `data` 由服务器提供的响应
data: {},
// `status` 来自服务器响应的 HTTP 状态码
status: 200,
// `statusText` 来自服务器响应的 HTTP 状态信息
statusText: 'OK',
// `headers` 服务器响应的头
headers: {},
// `config` 是为请求提供的配置信息
config: {}
}
配置的默认值/defaults
全局的 axios 默认值
axios.defaults.baseURL = 'https://api.example.com';
axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
自定义的实例默认值
// 创建实例时设置配置的默认值
var instance = axios.create({
baseURL: 'https://api.example.com'
});
// 在实例已创建后修改默认值
instance.defaults.headers.common['Authorization'] = AUTH_TOKEN;
配置的优先顺序
配置会以一个优先顺序进行合并。这个顺序是:在 lib/defaults.js 找到的库的默认值,然后是实例的 defaults 属性,最后是请求的 config 参数。后者将优先于前者。这里是一个例子:
// 使用由库提供的配置的默认值来创建实例
// 此时超时配置的默认值是 `0`
var instance = axios.create();
// 覆写库的超时默认值
// 现在,在超时前,所有请求都会等待 2.5 秒
instance.defaults.timeout = 2500;
// 为已知需要花费很长时间的请求覆写超时设置
instance.get('/longRequest', {
timeout: 5000
});
发送HTTP请求的 dispatchRequest 方法
dispatchRequest 方法的核心是adapter。对于浏览器环境来说,adapter可以通过 XMLHttpRequest 或 fetch API 来发送 HTTP 请求,而对于 Node.js 环境来说,adapter可以通过 Node.js 内置的 http 或 https 模块来发送 HTTP 请求。
function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
// For browsers use XHR adapter
adapter = require('./adapters/xhr');
} else if (typeof process !== 'undefined' &&
Object.prototype.toString.call(process) === '[object process]') {
// For node use HTTP adapter
adapter = require('./adapters/http');
}
return adapter;
}
浏览器环境的 Adapter函数(只列出关键函数)
function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
// 获得最终的config里面的配置
var requestData = config.data;
var requestHeaders = config.headers;
var responseType = config.responseType;
var request = new XMLHttpRequest(); // 基于 XMLHttpRequest
// 调用 open 方法
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
// 设置超时事件
request.timeout = config.timeout;
// xhr对象的事件监听函数
if ('onloadend' in request) {
// Use onloadend if available
request.onloadend = onloadend;
} else {
request.onreadystatechange = function handleLoad() {...}
}
// onabort事件监听函数
request.onabort = function handleAbort() {
if (!request) {
return;
}
reject(createError('Request aborted', config, 'ECONNABORTED', request));
// Clean up request
request = null;
};
// onerror 事件监听函数
request.onerror = function handleError() {}
// ontimeout 事件监听函数
request.ontimeout = function handleError() {}
// 对下载的处理
if (typeof config.onDownloadProgress === 'function') {
request.addEventListener('progress', config.onDownloadProgress);
}
// 对上传的处理
if (typeof config.onUploadProgress === 'function' && request.upload) {
request.upload.addEventListener('progress', config.onUploadProgress);
}
// 利用双重Cookie的方式防止XSRF攻击
if (utils.isStandardBrowserEnv()) {
// Add xsrf header
var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
cookies.read(config.xsrfCookieName) :
undefined;
if (xsrfValue) {
requestHeaders[config.xsrfHeaderName] = xsrfValue;
}
}
// 调用 send 方法发送请求
request.send(requestData);
})
}
dispatchRequest:
// 如果触发取消请求,调用 throwIfCancellationRequested
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
function dispatchRequest(config) {
throwIfCancellationRequested(config);
config.headers = config.headers || {};
//对请求参数中 data 的转换
config.data = transformData.call(
config,
config.data,
config.headers,
config.transformRequest
);
// 合并headers
config.headers = utils.merge(
config.headers.common || {},
config.headers[config.method] || {},
config.headers
);
// 其他代码
var adapter = config.adapter || defaults.adapter; // 可以自定义 adapter (对于浏览器,基于XMLHTTPRequest创建请求并做处理)
return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config); // 如果取消了请求,那么会 throw (token.reason)
// Transform response data
response.data = transformData.call(
config,
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.call(
config,
reason.response.data,
reason.response.headers,
config.transformResponse
);
}
}
return Promise.reject(reason);
});
}
拦截器
在请求或响应被 then 或 catch 处理前拦截它们。axios 的拦截器类似于洋葱模型,
请求拦截先写的后执行,响应拦截先写的先执行。
// 添加请求拦截器
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);
});
移除拦截器
var myInterceptor = axios.interceptors.request.use(function () {/*...*/});
axios.interceptors.request.eject(myInterceptor);
为实例添加拦截器
var instance = axios.create();
instance.interceptors.request.use(function () {/*...*/});
拦截器默认情况下是异步的。 当主线程被阻塞时,这可能会导致 axios 请求的执行延迟(在后台为拦截器创建了一个 promise ,并且请求被放在调用堆栈的底部)。 可以通过指定 { synchronous: true } 来让 axios 同步运行代码并避免请求执行中的任何延迟。
axios.interceptors.request.use(function (config) {
config.headers.test = 'I am only a header!';
return config;
}, null, { synchronous: true });
如果要基于运行时检查执行特定拦截器,可以向选项对象添加 runWhen 函数。当且仅当 runWhen 的返回值为 false 时,拦截器才会被执行。 该函数将使用 config 对象调用(不要忘记您也可以将自己的参数绑定到它。)当您有一个只需要在特定时间运行的异步请求拦截器时,这会很方便。
function onGetCall(config) {
return config.method === 'get';
}
axios.interceptors.request.use(function (config) {
config.headers.test = 'special get headers';
return config;
}, null, { runWhen: onGetCall });
源码解析
任务注册
axios 的拦截器包括对请求的拦截 axios.interceptors.request.use 和对响应的拦截 axios.interceptors.response.use。
// lib/core/Axios.js
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
InterceptorManager 是管理拦截器的库,其原型( InterceptorManage.prototype )上定义了 use (添加handler)、 eject(删除handler) 、 forEach (依次调用handler)方法。
// fulfilled:处理 Promise 中 then 回调的函数
// rejected:处理 Promise 中 reject 回调的函数
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected,
synchronous: options ? options.synchronous : false,
runWhen: options ? options.runWhen : null
});
return this.handlers.length - 1;
};
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null;
}
};
InterceptorManager.prototype.forEach = function forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
if (h !== null) {
fn(h);
}
});
};
任务编排
Axios 对 Request 拦截器对应的处理
// lib/core/Axios
var requestInterceptorChain = [];
var synchronousRequestInterceptors = true;
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
// 对运行时 runWhen 的处理 (runWhen接受config作为参数)
if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
return;
}
synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected); // 注意用的 unshift,请求拦截先写的后执行
});
Axios 对 Response 拦截器对应的处理
// lib/core/Axios
var responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected); // 用的push,响应拦截先写的先执行
});
任务调度
异步调度
如果没有设定 { synchronous: true } , 也就是异步执行拦截器(默认方式),那么拦截器的执行方式是
var chain = [dispatchRequest, undefined]; // 注意这个初始值
Array.prototype.unshift.apply(chain, requestInterceptorChain);
chain.concat(responseInterceptorChain);
promise = Promise.resolve(config);
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promis
注意,如果添加拦截器时的顺序是
axios.interceptors.request.use(requestInterceptor1)
axios.interceptors.request.use(requestInterceptor2)
axios.interceptors.resoponse.use(responseInterceptor
axios.interceptors.resoponse.use(responseInterceptor2)
那么源码中的 chain 的值是 [requestInterceptor2, requestInterceptor1, dispatchRequest, undefined, responseInterceptor1, responseInterceptor2 ] , chain 的执行方式是 shift() 。
同步调度
如果设定了{ synchronous: true },那么是同步执行拦截器,执行方式是:
// 先执行 request 拦截器
var newConfig = config;
while (requestInterceptorChain.length) {
var onFulfilled = requestInterceptorChain.shift();
var onRejected = requestInterceptorChain.shift();
try {
newConfig = onFulfilled(newConfig);
} catch (error) {
onRejected(error);
break;
}
}
// 然后执行 dispatchRequest
try {
promise = dispatchRequest(newConfig);
} catch (error) {
return Promise.reject(error);
}
// 然后执行 response 拦截器
while (responseInterceptorChain.length) {
promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift());
}
// 最后 return promise
return promise;
错误处理
axios.get('/user/12345')
.catch(function (error) {
if (error.response) {
// 请求已发出,但服务器响应的状态码不在 2xx 范围内
console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);
} else {
// Something happened in setting up the request that triggered an Error
console.log('Error', error.message);
}
console.log(error.config);
});
可以使用 validateStatus 配置选项定义一个自定义 HTTP 状态码的错误范围。
axios.get('/user/12345', {
validateStatus: function (status) {
return status < 500; // 状态码在大于或等于500时才会 reject
}
})
取消请求
可以使用 CancelToken.source 工厂方法创建 cancel token。可以看到,我从 config 入 cancelToken: axios.CancelToken.source().token , 并且可以用 axios.CancelToken.source().cancel() 执行取消请求。此外, canel 函数不仅是取消了请求,并且使得整个请求走入了 rejected 。`
var CancelToken = axios.CancelToken;
var source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function(thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// 处理错误
}
});
// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');
还可以通过传递一个 executor 函数到 CancelToken 的构造函数来创建 cancel token:
var CancelToken = axios.CancelToken;
var cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// executor 函数接收一个 cancel 函数作为参数
cancel = c;
})
});
// 取消请求
cancel();
Axios 取消请求相关的源码
CancelToken 是一个可用于请求取消操作的对象。
// CancelToken
function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
executor(function cancel(message) {
// cancel 函数的作用就是 resoleve 掉 promise
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}
// source 工厂函数
// 返回一个对象,其中包含一个新的 `CancelToken` 和一个在调用时取消 `CancelToken` 的函数。
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token, // 类 CancelToken 的实例
cancel: cancel // 调用 cancel 方法取消请求
};
};
// 当触发取消请求时(例如 source.cancel()),dispatchRequest中执行的方法
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
if (this.reason) {
throw this.reason;
}
};
Cancel 是在操作被 cancel 时候抛出的对象
function Cancel(message) {
this.message = message;
}
Cancel.prototype.toString = function toString() {
return 'Cancel' + (this.message ? ': ' + this.message : '');
};
Cancel.prototype.__CANCEL__ = true; // 在 axios.isCancel 中用到
axios.isCancel 判断请求是否取消
module.exports = function isCancel(value) {
return !!(value && value.__CANCEL__);
};
lib/adapters/xhr.js
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
// 其他代码
var request = new XMLHttpRequest();
// 其他代码
if (config.cancelToken) {
// Handle cancellation
// 在 CancelToken 中,调用 source.cancel() 的时候执行的是 resolvePromise(token.reason),因此 onCanceleds 是 then 回调中的第一个参数
config.cancelToken.promise.then(function onCanceled(cancel) { // cancel 是 token.reason
if (!request) {
return;
}
request.abort(); //(XHRHttpRequest 的 abort 方法)
reject(cancel); // 把 cancel (new Cancel(message))reject出去
// Clean up request
request = null;
});
}
}
Fetch Or Axios ?
请求
-
Fetch
fetch(url, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(data), // 请求为post/put时,请求数据放在 body 字段里面,并且要用 JSON.stringify()序列化 })
-
Axios
axios({ url: "api.com", method: "POST", header: { "Content-Type": "application/json", }, data: { name: "Sabesan", age: 25 }, // 请求数据放在 data 里面,并且可以直接传对象 });
响应
- Fetch
fetch() 接收到的 response 是一个 Stream 对象, response.json() 是一个异步操作,取出所有内容,并将其转为 JSON 对象。
fetch('url')
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.log(error));
- Axios
相比于 fetch() , Axios 的 Response 是可以直接读取的对象
axios.get('url')
.then((response)=>console.log(response))
.catch((error)=>console.log(error))
错误处理
- Fetch
fetch() 发出请求后,只有网络错误或者无法链接时,才会报错。其他情况都不会报错,而是认为请求成功。因此需要判断 response.status 或者 response.ok 来得知是否真的请求成功,并且在请求失败的情况下手动抛出错误。
fetch('url')
.then((response)=>{
if(!response.ok){
throw Error (response.statusText);
}
return response.json();
})
.then((data)=>console.log(data))
.catch((error)=>console.log(error))
- Axios
如果 HTTP 状态码是 4XX 或者 5XX,那么 axios 的返回的 Response 状态会成为 Rejected ,并且抛出错误,这可以被 response 对象的 catch 方法捕获到。
axios.get('url')
.then((response)=> console.log(response))
.catch((error)=>{
if(error.response){
// 如果响应的状态不是 2XX
console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);
} else if (error.request){
// 如果在请求发出后没有收到响应
console.log(error.request);
} else {
// 其他错误
console.log(error.message);
}
})
监听下载进度
- Fetch
要在 fetch() 中监听下载进度,可以使用 ReadableStream 类型的 response.body 属性,它逐块提供主体数据。
const element = document.getElementById('progress');
// 计算下载的百分比
function progress({loaded, total}) {
element.innerHTML = Math.round(loaded/total*100)+'%';
}
fetch('url')
.then(response => {
if (!response.ok) {
throw Error(response.status+' '+response.statusText)
}
if (!response.body) {
throw Error('ReadableStream not yet supported in this browser.')
}
// 获取响应数据的总大小
const contentLength = response.headers.get('content-length');
if (!contentLength) {
throw Error('Content-Length response header unavailable');
}
const total = parseInt(contentLength, 10);
let loaded = 0;
return new Response(
new ReadableStream({
start(controller) {
const reader = response.body.getReader();
read();
function read() {
reader.read().then(({done, value}) => {
if (done) {
controller.close();
return;
}
loaded += value.byteLength;
progress({loaded, total})
controller.enqueue(value);
read();
}).catch(error => {
console.error(error);
controller.error(error)
})
}
}
})
);
})
.then(response =>
// construct a blob from the data
response.blob()
)
.then(data => {
// insert the downloaded image into the page
document.getElementById('img').src = URL.createObjectURL(data);
})
.catch(error => {
console.error(error);
})
- Axios
Axios可以使用已有的Axios Progress Bar来获取下载进度
import { loadProgressBar } from 'axios-progress-bar'
loadProgressBar();
function downloadFile(url) {
axios.get(url, {responseType: 'blob'})
.then(response => {
const reader = new window.FileReader();
reader.readAsDataURL(response.data);
reader.onload = () => {
document.getElementById('img').setAttribute('src', reader.result);
}
})
.catch(error => {
console.log(error)
});
}
监听上传进度
- Fetch
Fetch 不能监听上传进度
- Axios
可以往 Axios 的配置对象中传入 onUploadProgress 作为 request.upload 对象的 progress 事件监听器。
// lib/adapters/xhr
if (typeof config.onUploadProgress === 'function' && request.upload) {
request.upload.addEventListener('progress', config.onUploadProgress);
}
const config = {
onUploadProgress: event => console.log(event.loaded)
};
axios.put("/api", data, config);
HTTP 拦截
- Fetch
fetch 默认不提供 HTTP 拦截,但可以通过重写 fetch() 方法并定义发送请求期间的行为。
fetch = (originFetch => {
return (...arguments) => {
const result = originFetch.apply(this, arguments)
return result.then(() => {
console.log('Request was sent')
});
};
})(fetch);
fetch('url')
.then(response => response.json())
.then(data => {
console.log(data)
});
- Axios
Axios 自带拦截器机制,可以对请求和响应进行对应的拦截操作。对拦截器的具体介绍可见上面 Axios 的拦截器部分
// 添加请求拦截器
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);
});
超时处理
- Fetch
fetch() 通过 AbortController 接口提供响应超时功能。
const controller = new AbortController();
const signal = controller.signal;
const options = {
method: 'POST',
signal,
body: JSON.stringfy({
first: a,
second: b
})
}
const timeoutId = setTimeout(() => controller.abort(), 5000);
fetch(url, options)
.then(res => res.json())
.then(data => {...})
.catch(err => console.log(err)
- Axios
通过在 config 对象中使用可选的超时属性,您可以设置请求终止前的毫秒数。
axios({
method: 'post',
url: '/login',
timeout: 5000, // 5 seconds timeout
data: {
firstName: 'Sabesan',
lastName: 'Sathananthan'
}
})
.then(response => {/* handle the response */})
.catch(error => console.error('timeout exceeded'))
Reference: