Axios
- 从浏览器中创建 XMLHttpRequests
- 从 node.js 创建 http 请求
- 支持 Promise API
- 拦截请求和响应
- 转换请求数据和响应数据
- 取消请求
- 自动转换 JSON 数据
- 客户端支持防御 XSRF
Axios
一般在前端项目中,会通过 axios create来获取一个axios的单例,并且配置请求拦截器,以及响应拦截器
需要注意的是,在项目,一般通过env环境变量的方式来获取到后端api的请求地址,在开发环境下,如果baseUrl不是'/'或'',会导致前端请求代理失效。
php
const instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
});
拦截器 interceptors
挂载token
arduino
// 设置token
async function setToken(config) {
config.headers.Authorization = getToken()
}
instance.interceptors.request.use(async (config)=>{
config = setToken(config)
return config
})
自动刷新token
当token过期后,自动将请求push到一个数组中,再获取到新token时,重新发起请求
先在请求拦截其中保存每次请求的配置
难点是如何保存失败请求的pending状态。并且在刷新token并请求成功后,这些状态置为fulfilled
javascript
let lastConfig = null
// 当前是否正在刷新token中
let isRefreshing = false
let requests = []
const instance = axios.create()
instance.interceptors.request.use(async (config)=>{
config = setToken(config)
lastConfig = decoup(config)
return config
})
// 需要后端提供两个token,一个用于请求接口,一个用于刷新token
// 这里获取的就是用于请求接口的token
async function refreshToken() {
}
instance.interceptors.response.use(async (response)=> {
// 这里code的值需要与后端协商
if(response.data.code === ruleStatus.expired) {
// 如果已经进入了刷新token的流程,则会将此阶段的请求,暂存到requests中
if(!isRefreshing) {
// 第一个token失效的请求配置
const config = response.config
isRefreshing = true
// 如果发现token过期了
const newToken = await refreshToken().catch(()=>{
// 如果token刷新失败,则直接跳转回登陆页面
toLogin()
})
if(newToken) {
// 将刷新token期间的请求resolve
requests.forEach(cb=> cb(newToken))
// 晴空数组
requests.length = 0
// 更换新token
config.headers.Authorization = token
// 继续请求第一个失效的请求
return instance(config)
}
} else {
// 这里返回一个pending 状态promise,并且将resolve方法放到requests中
// 只要requests中的方法不执行,这个promsie的状态就一直为pending
return new Promise(resolve=> {
requests.push((token)=>{
let temp = null;
temp = lastConfig
temp.headers.Authorization = token
resolve(instance(temp))
})
})
}
}
})
登陆过期
lua
instance.interceptors.response.use(()=>{}, (error)=>{
if(error.response && error.response.status === 401) {
ElMessageBox.confirm(
'抱歉,您当前登录状态已失效,点击【重新登录】跳转至登录页,【取消】则继续停留在当前页面',
'上传提示',
{
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
userStore.login()
})
}
})
返回类型处理
针对二进制流部分,可以根据与后端约定的格式,通过响应拦截器,直接获取到二进制流,
typescript
instance.interceptors.response.use(async (response)=>{
const {config} = response
// 接口请求时传入响应预处理方法
const resIteratee = Array.isArray(config.resIteratee) ? config.resIteratee : [config.resIteratee]
let result = response
while(config.resIteratee.length) {
const fn = config.resIteratee.shift()
const isAsync = fn instanceof Promise
try {
result = isAsync ? await fn(response) : fn(response)
if(result === false) {
return response
}
} catch (err) {
return Promise.reject(err)
console.log(e)
}
}
if (
response.request.responseType === 'blob' ||
response.request.responseType === 'arraybuffer' ||
responseIncrWhiteList.includes(response.config.url as string)
) {
return response.data.data
}
return response.data
})
Retry
利用拦截器自动重发失败请求,这里的失败指的是 500类型的错误,如果是业务层面的请求失败,可以默认是服务端逻辑,不需要重新请求。其实与自动刷新token的逻辑有一些类似 实现参考了 axios-retry的源码
lua
// 判断错误是否需要重试
function isRetryableError(error) {
return (
error.code !== 'ECONNABORTED' &&
(!error.response || (error.response.status >= 500 && error.response.status <= 599))
);
}
function getCurrentState(config) {
const currentState = config['_retry_'] || {};
currentState.retryCount = currentState.retryCount || 0;
config['_retry_'] = currentState;
return currentState;
}
// 对于下一次的请求,config上的这些字段并不能拷贝到请求配置上,需要手动设置
// 这是一个axios的bug
function fixConfig(axios, config) {
if (axios.defaults.agent === config.agent) {
delete config.agent;
}
if (axios.defaults.httpAgent === config.httpAgent) {
delete config.httpAgent;
}
if (axios.defaults.httpsAgent === config.httpsAgent) {
delete config.httpsAgent;
}
}
lua
function retry(axios, count) {
axios.interceptors.request((config)=>{
const currentState = getCurrentState(config)
return config
})
axios.interceptors.response(null, (error)=>{
const { config } = error
const currentState = getCurrentState(config)
if(config.retryCount > count) {
return Promsie.reject(error)
}
config.transformRequest = [(data) => data];
fixConfig(axios, config)
if(isRetryableError(error) && error.config.retryCount <= count) {
return axios(config)
} else {
return Promsie.reject(error)
}
})
return axios
}
const instance = retry(Axios.create(), 3)
请求防抖
同类型的请求,请求的config一致,大致思路为,新建一个正在请求的数组,存在时直接return Promise.reject()结束请求,请求完成后,则将此类型的请求从数组中删除
javascript
import { eq } from 'lodash'
const currentPendingSet = []
const sameRequestPendingindex = function (config) {
const index = currentPendingSet.findIndex((conf)=>{
return conf.url === config.url && eq(conf.data, config.data)
})
return index
}
const hasSameRequestPending = function (config) {
return sameRequestPendingindex(config) > -1
}
axios.interceptors.request((config)=>{
if(hasSameRequestPending(config)) {
return Promise.reject(config)
}
currentPendingSet.push(config)
return config
})
axios.interceptors.response((response)=>{
const { config } = response
const index = sameRequestPendingindex(config)
if(index > -1) {
currentPendingSet.splice(index, 1)
}
return response.data
})
Request方法
request方法是axios的基础方法,其他例如,get\post\put\patch等,都是基于reuqest扩展实现的、他接受一个请求地址、以及config对象
他最终会调用 dispatchRequest 来发起请求,在此之前,他会执行以下逻辑
合并配置
合并axios内置配置、axios.create时传入的配置,以及用户发起请求时传入配置、用户配置的优先级最高,create配置其次、axios内置默认配置最低、优先级高的可以在合并中覆盖优先级低的
ini
if (typeof configOrUrl === 'string') {
config = config || {};
config.url = configOrUrl;
} else {
config = configOrUrl || {};
}
config = mergeConfig(this.defaults, config);
mergeConfig 内部使用策略模式、获取对应属性的合并方法
scss
// 每种属性对应的默认值
var mergeMap = {
'url': valueFromConfig2,
'method': valueFromConfig2,
'data': valueFromConfig2,
'baseURL': defaultToConfig2,
'transformRequest': defaultToConfig2,
'transformResponse': defaultToConfig2,
'paramsSerializer': defaultToConfig2,
'timeout': defaultToConfig2,
'timeoutMessage': defaultToConfig2,
'withCredentials': defaultToConfig2,
'adapter': defaultToConfig2,
'responseType': defaultToConfig2,
'xsrfCookieName': defaultToConfig2,
'xsrfHeaderName': defaultToConfig2,
'onUploadProgress': defaultToConfig2,
'onDownloadProgress': defaultToConfig2,
'decompress': defaultToConfig2,
'maxContentLength': defaultToConfig2,
'maxBodyLength': defaultToConfig2,
'beforeRedirect': defaultToConfig2,
'transport': defaultToConfig2,
'httpAgent': defaultToConfig2,
'httpsAgent': defaultToConfig2,
'cancelToken': defaultToConfig2,
'socketPath': defaultToConfig2,
'responseEncoding': defaultToConfig2,
'validateStatus': mergeDirectKeys
};
//
utils.forEach(Object.keys(config1).concat(Object.keys(config2)), function computeConfigValue(prop) {
var merge = mergeMap[prop] || mergeDeepProperties;
var configValue = merge(prop);
(utils.isUndefined(configValue) && merge !== mergeDirectKeys) || (config[prop] = configValue);
});
axios默认配置
axios内置的配置
kotlin
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;
}
var defaults = {
// transitionalDefaults
// export default {
// silentJSONParsing: true,
// forcedJSONParsing: true,
// clarifyTimeoutError: false
// };
transitional: transitionalDefaults,
// 自定义请求,xhr or http //用于区分平台,
// nodejs平台使用request,borwser使用XMLHttpRequest
adapter: getDefaultAdapter(),
// 默认的请求拦截器
transformRequest: [function transformRequest(data, headers) {
// 格式化属性名
normalizeHeaderName(headers, 'Accept');
normalizeHeaderName(headers, 'Content-Type');
// 获取当前的
var contentType = headers && headers['Content-Type'] || '';
var hasJSONContentType = contentType.indexOf('application/json') > -1;
var isObjectPayload = utils.isObject(data);
if (isObjectPayload && utils.isHTMLForm(data)) {
data = new FormData(data);
}
var isFormData = utils.isFormData(data);
if (isFormData) {
if (!hasJSONContentType) {
return data;
}
return hasJSONContentType ? JSON.stringify(formDataToJSON(data)) : data;
}
// 处理各种数据类型
if (utils.isArrayBuffer(data) ||
utils.isBuffer(data) ||
utils.isStream(data) ||
utils.isFile(data) ||
utils.isBlob(data)
) {
// 返回
return data;
}
if (utils.isArrayBufferView(data)) {
return data.buffer;
}
// 如果使用了params参数
if (utils.isURLSearchParams(data)) {
setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
return data.toString();
}
// 是否为多个文件
var isFileList;
if (isObjectPayload) {
if (contentType.indexOf('application/x-www-form-urlencoded') !== -1) {
return toURLEncodedForm(data, this.formSerializer).toString();
}
if ((isFileList = utils.isFileList(data)) || contentType.indexOf('multipart/form-data') > -1) {
var _FormData = this.env && this.env.FormData;
// {'files[]': data}是一个固定格式表示data为一个数组,方便转换为x-www-form-url-encoded的格式
return toFormData(
isFileList ? {'files[]': data} : data,
_FormData && new _FormData(),
this.formSerializer
);
}
}
// json数据处理
if (isObjectPayload || hasJSONContentType ) {
setContentTypeIfUnset(headers, 'application/json');
// 将js对象转换为json字符串
return stringifySafely(data);
}
return data;
}],
// 默认响应配置
transformResponse: [function transformResponse(data) {
//
var transitional = this.transitional || defaults.transitional;
var forcedJSONParsing = transitional && transitional.forcedJSONParsing;
var JSONRequested = this.responseType === 'json';
// 这里
if (data && utils.isString(data) && ((forcedJSONParsing && !this.responseType) || JSONRequested)) {
var silentJSONParsing = transitional && transitional.silentJSONParsing;
var strictJSONParsing = !silentJSONParsing && JSONRequested;
// 将后端返回的json字符串格式化成js对象
try {
return JSON.parse(data);
} catch (e) {
// 当发生错误后,使用Axios内置错误类型将错误抛出
if (strictJSONParsing) {
if (e.name === 'SyntaxError') {
throw AxiosError.from(e, AxiosError.ERR_BAD_RESPONSE, this, null, this.response);
}
throw e;
}
}
}
return data;
}],
/**
* A timeout in milliseconds to abort a request. If set to 0 (default) a
* timeout is not created.
*/
timeout: 0,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
maxBodyLength: -1,
env: {
FormData: platform.classes.FormData,
Blob: platform.classes.Blob
},
validateStatus: function validateStatus(status) {
return status >= 200 && status < 300;
},
headers: {
common: {
'Accept': 'application/json, text/plain, */*'
}
}
};
浏览器平台请求逻辑
axios浏览器平台的核心请求逻辑
ini
'use strict';
var utils = require('./../utils');
var settle = require('./../core/settle');
var cookies = require('./../helpers/cookies');
var buildURL = require('./../helpers/buildURL');
var buildFullPath = require('../core/buildFullPath');
var parseHeaders = require('./../helpers/parseHeaders');
var isURLSameOrigin = require('./../helpers/isURLSameOrigin');
var transitionalDefaults = require('../defaults/transitional');
var AxiosError = require('../core/AxiosError');
var CanceledError = require('../cancel/CanceledError');
var parseProtocol = require('../helpers/parseProtocol');
var platform = require('../platform');
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
// 请求数据
var requestData = config.data;
// 请求头
var requestHeaders = config.headers;
// 服务器返回类型
var responseType = config.responseType;
//
var onCanceled;
// 请求完成后执行此方法
function done() {
if (config.cancelToken) {
config.cancelToken.unsubscribe(onCanceled);
}
if (config.signal) {
config.signal.removeEventListener('abort', onCanceled);
}
}
if (utils.isFormData(requestData) && utils.isStandardBrowserEnv()) {
delete requestHeaders['Content-Type']; // Let the browser set it
}
// 实例化一个xhr对象
var request = new XMLHttpRequest();
// HTTP basic authentication
if (config.auth) {
var username = config.auth.username || '';
var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
}
var fullPath = buildFullPath(config.baseURL, config.url);
// 方法初始化一个新创建的请求,或重新初始化一个请求。 如果再次调用,则相当于调用了abort()
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
// Set the request timeout in MS
request.timeout = config.timeout;
function onloadend() {
if (!request) {
return;
}
// Prepare the response
var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
var responseData = !responseType || responseType === 'text' || responseType === 'json' ?
request.responseText : request.response;
var response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config: config,
request: request
};
settle(function _resolve(value) {
resolve(value);
done();
}, function _reject(err) {
reject(err);
done();
}, response);
// Clean up request
request = null;
}
// 一些低版本的浏览器并不支持 onloadend
if ('onloadend' in request) {
// Use onloadend if available
request.onloadend = onloadend;
} else {
// Listen for ready state to emulate onloadend
request.onreadystatechange = function handleLoad() {
// readyState 4 : 数据传输已经完成,,或者传输过程中出现问题
// 3 正在接收后端响应的数据
// 2 收到响应头,准备开始接收数据
// 1 已经调用过open()方法
// 0 request 已经完成实例化
if (!request || request.readyState !== 4) {
return;
}
// 当status等于0时
// 1、request已经完成实例化
// 2、request被abort中断
if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
return;
}
// readystate handler is calling before onerror or ontimeout handlers,
// so we should call onloadend on the next 'tick'
setTimeout(onloadend);
};
}
// Handle browser request cancellation (as opposed to a manual cancellation)
request.onabort = function handleAbort() {
if (!request) {
return;
}
reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, config, request));
// Clean up request
request = null;
};
// Handle low level network errors
request.onerror = function handleError() {
// Real errors are hidden from us by the browser
// onerror should only fire if it's a network error
reject(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request));
// Clean up request
request = null;
};
// Handle timeout
request.ontimeout = function handleTimeout() {
var timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';
var transitional = config.transitional || transitionalDefaults;
if (config.timeoutErrorMessage) {
timeoutErrorMessage = config.timeoutErrorMessage;
}
reject(new AxiosError(
timeoutErrorMessage,
transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,
config,
request));
// Clean up request
request = null;
};
// Add xsrf header
// This is only done if running in a standard browser environment.
// Specifically not if we're in a web worker, or react-native.
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;
}
}
// Add headers to the request
if ('setRequestHeader' in request) {
utils.forEach(requestHeaders, function setRequestHeader(val, key) {
if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
// Remove Content-Type if data is undefined
delete requestHeaders[key];
} else {
// Otherwise add header to the request
request.setRequestHeader(key, val);
}
});
}
// Add withCredentials to request if needed
if (!utils.isUndefined(config.withCredentials)) {
request.withCredentials = !!config.withCredentials;
}
// Add responseType to request if needed
if (responseType && responseType !== 'json') {
request.responseType = config.responseType;
}
// Handle progress if needed
if (typeof config.onDownloadProgress === 'function') {
request.addEventListener('progress', config.onDownloadProgress);
}
// Not all browsers support upload events
if (typeof config.onUploadProgress === 'function' && request.upload) {
request.upload.addEventListener('progress', config.onUploadProgress);
}
if (config.cancelToken || config.signal) {
// Handle cancellation
// eslint-disable-next-line func-names
onCanceled = function(cancel) {
if (!request) {
return;
}
reject(!cancel || cancel.type ? new CanceledError(null, config, req) : cancel);
request.abort();
request = null;
};
config.cancelToken && config.cancelToken.subscribe(onCanceled);
if (config.signal) {
config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
}
}
if (!requestData) {
requestData = null;
}
// 获取请求协议 http https file
var protocol = parseProtocol(fullPath);
if (protocol && platform.protocols.indexOf(protocol) === -1) {
reject(new AxiosError('Unsupported protocol ' + protocol + ':', AxiosError.ERR_BAD_REQUEST, config));
return;
}
// Send the request
request.send(requestData);
});
};
中断请求能力
示例代码 -1
javascript
import axios from 'axios'
let cancelFunc;
let cancelToken = axios.CancelToken(function(cancelFn) {
cancelFunc = cancelFn
})
axios({
method: 'POST',
url:'xxx',
isCancelToken: cancelToken
})
cancelToken.promise.then(()=>{
// do something ...
})
// 调用cancelFunc取消请求
cancelFunc()
示例代码 - 2
php
import axios from 'axios'
const cancelObj = axios.CancelToken.source()
axios({
method: 'POST',
url:'xxx',
isCancelToken: cancelObj.token
})
cancelObj.cancel()
axios是如何实现请求中断的
发起
在XHR.js中有以下逻辑,用于处理传入CancelToken的情况
scss
// 当用户传入cancelToken时,会执行此逻辑
if (config.cancelToken || config.signal) {
// Handle cancellation
// eslint-disable-next-line func-names
onCanceled = function(cancel) {
if (!request) {
return;
}
reject(!cancel || cancel.type ? new CanceledError(null, config, req) : cancel);
request.abort();
request = null;
};
// 如果有cancel方法则会在cancelToken中订阅这个状态
config.cancelToken && config.cancelToken.subscribe(onCanceled);
// 浏览器默认的中断逻辑,比如用户直接在request上执行了abort方法
if (config.signal) {
config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
}
}
在CancelToken.js文件,是一个简单的状态机+订阅模式
有几个关键方法,和有意思的地方
- resolvePromise 利用外部作用于,将promise状态的控制权传递给外界
- this.promise.then = function () {} 在方法中,又新建一个promise
javascript
function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
// 关键
// 这个变量很关键,他获取到了 CancelToken 内置promise的resolve方法,将
// Promise的状态控制权交给了外部
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
// 将this。promise的状态控制权交给外界
resolvePromise = resolve;
});
// 获取当前调用实例
var token = this;
// 执行所有订阅此状态的回调函数
// eslint-disable-next-line func-names
this.promise.then(function(cancel) {
if (!token._listeners) return;
var i = token._listeners.length;
while (i-- > 0) {
token._listeners[i](cancel);
}
token._listeners = null;
});
// 这是第二个比较有意思的点
// 首先:他处理了实例调用 promise.then的逻辑,使得我们可以在请求被取消时做点什么
// 原理是利用新建一个Promise, 利用promie.then微任务总是比同步任务后执行,来保证
// 用户传入的回调永远在最后执行
// eslint-disable-next-line func-names
this.promise.then = function(onfulfilled) {
var _resolve;
// eslint-disable-next-line func-names
var promise = new Promise(function(resolve) {
token.subscribe(resolve);
_resolve = resolve;
}).then(onfulfilled);
promise.cancel = function reject() {
token.unsubscribe(_resolve);
};
return promise;
};
// executor 内传入的方法,就是在new CancelToken时从回调函数中获取的 cancel 变量,
// 他是一个方法,利用前面保存的 resolvePromise 让用户能够修改此时内置promise的状态
// 从pending -> fulfilled
executor(function cancel(message, config, request) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new CanceledError(message, config, request);
resolvePromise(token.reason);
});
}
/**
* Throws a `CanceledError` if cancellation has been requested.
*/
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
if (this.reason) {
throw this.reason;
}
};
/**
* Subscribe to the cancel signal
*/
CancelToken.prototype.subscribe = function subscribe(listener) {
if (this.reason) {
listener(this.reason);
return;
}
if (this._listeners) {
this._listeners.push(listener);
} else {
this._listeners = [listener];
}
};
/**
* Unsubscribe from the cancel signal
*/
CancelToken.prototype.unsubscribe = function unsubscribe(listener) {
if (!this._listeners) {
return;
}
var index = this._listeners.indexOf(listener);
if (index !== -1) {
this._listeners.splice(index, 1);
}
};
/**
* Returns an object that contains a new `CancelToken` and a function that, when called,
* cancels the `CancelToken`.
*/
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
module.exports = CancelToken;
参数处理
在合并完配置后,将会进入参数处理部分
ini
Axios.prototype.request = function request(configOrUrl, config) {
/*eslint no-param-reassign:0*/
// Allow for axios('example/url'[, config]) a la fetch API
if (typeof configOrUrl === 'string') {
config = config || {};
config.url = configOrUrl;
} else {
config = configOrUrl || {};
}
// 将配置请求与用户传入
config = mergeConfig(this.defaults, config);
// Set config.method
if (config.method) {
config.method = config.method.toLowerCase();
} else if (this.defaults.method) {
config.method = this.defaults.method.toLowerCase();
} else {
config.method = 'get';
}
var transitional = config.transitional;
// 这里是对传入参数类型进行校验,validators是一个对象,里面存放着number、string、boolean等类型
// 这里使用的是boolean校验器
if (transitional !== undefined) {
validator.assertOptions(transitional, {
// 忽略JSON.parse(response.body)的错误
silentJSONParsing: validators.transitional(validators.boolean),
// 当responseType!== json时将response转化为json
forcedJSONParsing: validators.transitional(validators.boolean),
// 当请求超时返回ETIMEDOUT而不是ECONNABORTED
clarifyTimeoutError: validators.transitional(validators.boolean)
}, false);
}
// filter out skipped interceptors
var requestInterceptorChain = [];
var synchronousRequestInterceptors = true;
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
return;
}
synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});
var responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});
var promise;
if (!synchronousRequestInterceptors) {
var chain = [dispatchRequest, undefined];
Array.prototype.unshift.apply(chain, requestInterceptorChain);
chain = chain.concat(responseInterceptorChain);
promise = Promise.resolve(config);
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
}
var newConfig = config;
while (requestInterceptorChain.length) {
var onFulfilled = requestInterceptorChain.shift();
var onRejected = requestInterceptorChain.shift();
try {
newConfig = onFulfilled(newConfig);
} catch (error) {
onRejected(error);
break;
}
}
try {
promise = dispatchRequest(newConfig);
} catch (error) {
return Promise.reject(error);
}
while (responseInterceptorChain.length) {
promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift());
}
return promise;
};
axios在项目中使用的新玩法
在日常开发中,难免会有部分接口的返回数据格式,或者层级与其他接口不一致,如果在返回拦截器中对值做了处理,则可能为此接口单独添加额外的配置,用来在拦截器中做判断。
而在一些开源的项目中,我发现他们项目的axios拦截器非常简单,仅仅只是挂载了一个token,而文件处理,以及请求前的一些操作,则是利用一个包装器的概念实现的,利用这种方式,上面的问题就迎刃而解了,代码如下:
对于axios自带的拦截器,只进行挂载token的操作。
javascript
export const BASIC_CONFIG = {
baseURL: import.meta.env.$BASIC_BASE_URL,
headers: {
"Content-Type": "application/json"
},
timeout: 10000
}
export const request = axios.create(BASIC_CONFIG)
// 只使用了一个请求拦截器,挂载token
request.interceptors.request.use(
config => {
if (localStorage.getItem("token")) {
config.headers.Authorization = localStorage.getItem("token")
}
return config
},
err => Promise.reject(err)
)
普通请求包装器
javascript
/*
普通请求包装器,用于包装普通请求,做一些所有请求的统一的处理
*/
export function basicRequestWrapper(options, { loading = true }) {
const _loading = loading ? Loading.service({ lock: true }) : { close: noop }
return request(options)
.then(res => {
const { code, msg, data } = res.data
if (code === "200") {
return data
}
if (["50001", "50002", "50003"].includes(code)) {
if (hasToLoginBox) {
return
}
Loading.closeAll()
hasToLoginBox = true
return ElMessageBox.confirm(
"登录状态已过期,点击确定按钮去重新登录。",
"系统提示",
{
type: "error",
confirmButtonText: "确认",
showCancelButton: false
}
).then(() => {
localStorage.removeItem("token")
hasToLoginBox = false
window.location.href = "/login"
})
}
if (msg) {
ElMessage({ type: "error", message: msg })
}
throw res.data
})
.finally(() => _loading.close())
}
对于二进制流的包装器
javascript
/*
二进制流包装器,适用于文件下载,图片等
*/
/**
* @param {import("axios").AxiosRequestConfig} options
* @param {{ loading: boolean }}
*/
export function streamRequestWrapper(options, { loading, fileType }) {
const _loading = loading ? Loading.service({ lock: true }) : { close: noop }
return request({ ...options, responseType: "arraybuffer" })
.then(res => {
const { data, headers } = res
if (
typeof data === "object" &&
data.code &&
["50001", "50002", "50003"].includes(data.code)
) {
if (hasToLoginBox) {
return
}
Loading.closeAll()
hasToLoginBox = true
return ElMessageBox.confirm(
"登录状态已过期,点击确定按钮去重新登录。",
"系统提示",
{
type: "error",
confirmButtonText: "确认",
showCancelButton: false
}
).then(() => {
localStorage.removeItem("token")
hasToLoginBox = false
window.location.href = "/login"
})
}
return { data, headers }
})
.finally(() => _loading.close())
}
针对常用请求防止再做一层封装
typescript
import { basicRequestWrapper, streamRequestWrapper } from "./request"
export const postRequest = ({ url, data = {}, loading }) => {
return basicRequestWrapper(
{ url, data: JSON.stringify(data), method: "POST" },
{ loading }
)
}
// formData的请求
export const formRequest = ({ url, data = {}, loading }) => {
return basicRequestWrapper(
{
url,
data: Object.entries(data).reduce((data, [key, value]) => {
Array.isArray(value)
? value.forEach(file => data.append(key, file))
: data.append(key, value)
return data
}, new FormData()),
method: "POST",
headers: { "Content-Type": null }
},
{ loading }
)
}
// 请求文件
export const fileRequest = ({ url, data = {}, loading }) => {
return streamRequestWrapper(
{ url: url + resolveURLQuery(data) },
{ loading }
).then(({ data, headers }) => {
return new Promise(resolve => {
const type = headers["content-type"].split(";")[0].trim()
const blob = new Blob([data], { type })
let reader = new FileReader()
reader.onload = function (e) {
resolve(e.target.result)
}
reader.readAsDataURL(blob)
})
})
}
Fetch
Fetch是一个现代的概念,等同于XML Request, 他提供了许多与XMLHttpRequest 相同的功能,但被设计成更具可扩展性和高效。
-
fetch()
的功能与 XMLHttpRequest 基本相同,但有三个主要的差异。 -
fetch()
使用 Promise,不使用回调函数,因此大大简化了写法,写起来更简洁。 -
fetch()
采用模块化设计,API 分散在多个对象上(Response 对象、Request 对象、Headers 对象),更合理一些;相比之下,XMLHttpRequest 的 API 设计并不是很好,输入、输出、状态都在同一个接口管理,容易写出非常混乱的代码。 -
fetch()
通过数据流(Stream 对象)处理数据,可以分块读取,有利于提高网站性能表现,减少内存占用,对于请求大文件或者网速慢的场景相当有用。XMLHTTPRequest 对象不支持数据流,所有的数据必须放在缓存里,不支持分块读取,必须等待全部拿到后,再一次性吐出来。
最简使用
fetch()接收一个url,默认发起get请求,返回一个Response对象
typescript
// fetch
const data = await fetch(baseUrl + '/api/getRecommend').catch((err)=>{
console.log('Failed', err)
})
console.log(data.json())
Response对象
响应头上的同步信息
fetch()
请求成功后返回,对应服务器的 HTTP 回应。
属性 | 类型 | 意义 |
---|---|---|
ok | boolean | 请求是否成功,true 对应HTTP状态码 200 - 299,false 对应其他的状态码,400 - 504, 300系列的状态码,如果请求成功,会被fetch自动转为200补充常见状态码 |
status | number | HTTP状态码- 1**: 服务器接收到请求,需要请求者继续操作 |
- 2**:成功,操作被成功接收并处理
- 3**:重定向,需要进一步操作完成请求
- 4**:客户端错误
- 5**:服务器错误 | | statusText | string | 响应状态信息,如OK, | | url | string | 请求url, 存在跳转时,返回最终url | | type | string | - basic: 普通请求
- cors: 跨域请求
- error: 网络错误
- opaque: 如果
fetch()
请求的type
属性设为no-cors
,就会返回这个值。表示发出的是简单的跨域请求,类似<form>
表单的那种跨域请求。 - opaqueredirect: 如果
fetch()
请求的redirect
属性设为manual
,就会返回这个值 | | redirected | boolean | 是否发生过跳转 |
数据获取
与XMLHttpRequest可以直接获取到数据不同,fetch需要调用以下方法
response.
- json() 获取json数据,如果返回的数据不是这个格式,将会报错
- blob() 获取到Blob对象
- formData() 获取表单对象
- arrayBuffer() 获取二进制对象
- text() 获取字符串数据
javascript
const imageType = await fetch('/image' + imageUrl)
// console.log(await imageType.clone().json()) // 报错
console.log(await imageType.clone().text(),'text')
console.log(await imageType.clone().arrayBuffer(), 'Arraybuffer')
console.log(await imageType.clone().blob(), 'blob')
// console.log(await imageType.clone().formData(), 'formdata') // 报错
text()
其他类型都是可以正常获取到对应类型的,text()有一点特殊:
如果返回的对象转换为string类型无意义的话,比如这里返回的是一个图片,那么他将获取到当前页面的html

如果是JSON的话,则会返回一个json字符串

分段读取数据Response.body
response.body 是Response对象暴露出的底层接口,返回一个 ReadableStream 对象
javascript
// fetch获取图片
const image = await fetch('/image' + imageUrl)
const reader = image.body.getReader()
async function readImage () {
const {done, value} = await reader.read()
console.log(value)
if(done) {
console.log('done')
return
}
window.requestAnimationFrame(readImage)
}
readImage()
fetch 配置
fetch(url, options) 接收两个参数,分别是请求地址、配置参数
options完整案例
javascript
const response = fetch(url, {
method: "GET", // 请求方式
headers: {"Content-Type": "text/plain;charset=UTF-8"}, // 请求头
body: undefined, // 请求体 可以接受Blob, FormData Text Json
referrer: "about:client", // 请求来源,默认为about:client
referrerPolicy: "no-referrer-when-downgrade", // 属性用于设定Referer标头的规则。
//
mode: "cors", // 属性指定请求的模式。
credentials: "same-origin", // 属性指定是否发送 Cookie。
cache: "default", // 属性指定如何处理缓存。
redirect: "follow", // 属性指定 HTTP 跳转的处理方法
integrity: "", // 属性指定一个哈希值,用于检查 HTTP 回应传回的数据是否等于这个预先设定的哈希值
keepalive: false, // 属性用于页面卸载时,告诉浏览器在后台保持连接,继续发送数据
signal: undefined // 请求中断信号
});
取消fetch请求
scss
const controller = new AbortController();
const signal = controller.signal
fetch(url, {
signal: controller.signal
})
controller.abort(); // 取消
console.log(signal.aborted); // true