# ajax常见问题
# ajax成功状态
根据返回的状态值status判断
200
到300
或者304
,304
代表资源没有修改,可以使用缓存
来一段原始的ajax
:
var xhr = new XMLHttpRequest();
xhr.open('GET', url, false);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) { //readyState == 4说明请求已完成
var status = xhr.status;
if ((status >= 200 && status < 300) || status === 304) {
console.log(xhr.responseText);
}
}
};
xhr.send();
但在axios
中,默认情况下,304
并不代表成功,会走到下面的error
中:
axios.interceptors.response.use(function (response) {
return response;
}, function (error) {
console.error('----', error);
});
为什么呢?看了下源码,主要是这段:
var defaults = {
validateStatus: function validateStatus(status) {
return status >= 200 && status < 300;
}
};
校验状态码用的validateStatus
函数,如果非要使用304
的话,可以修改配置:
axios.defaults.validateStatus = function validateStatus(status) {
return (status >= 200 && status < 300) || status === 304;
};
不过,需要注意的是,每个状态码有特殊的含义,在返回304
状态码的时候,从response
中取得的data
为""
。
# ajax缓存问题
只有get
请求可能会被浏览器缓存使用。如果不使用缓存,有以下方案:
- 发送请求前加上
xhr.setRequestHeader("If-Modified-Since","0")
- 发送请求前加上
xhr.setRequestHeader("Cache-Control","no-cache")
- 在URL后面加上一个随机数或时间戳:
url += "fresh=" + Math.random();
url += "time=" + new Date().getTime();
- 如果是使用jQuery,直接这样就可以了
$.ajaxSetup({cache:false})。
# fetch的参数
fetch
使用方法:
const response = fetch(url, {
method: "GET",
headers: {
"Content-Type": "text/plain;charset=UTF-8"
},
body: undefined,
referrer: "about:client",
referrerPolicy: "no-referrer-when-downgrade",
mode: "cors",
credentials: "same-origin",
cache: "default",
redirect: "follow",
integrity: "",
keepalive: false,
signal: undefined
});
几个重要参数:
# mode
mode
属性指定请求的模式。可能的取值如下:
- cors:默认值,允许跨域请求。
- same-origin:只允许同源请求。
- no-cors:请求方法只限于
GET
、POST
和HEAD
,并且只能使用有限的几个简单标头,不能添加跨域的复杂标头,相当于提交表单所能发出的请求。
# credentials
credentials
属性指定是否发送Cookie
。可能的取值如下:
- same-origin:默认值,同源请求时发送 Cookie,跨域请求时不发送。
- include:不管同源请求,还是跨域请求,一律发送 Cookie。
- omit:一律不发送。
跨域请求发送 Cookie
,需要将credentials
属性设为include
。
假设我们不需要Cookie
传递什么消息,就可以使用omit
来关闭。
# signal
signal
属性指定一个 AbortSignal
实例,用于取消fetch()
请求。
fetch()
请求发送以后,如果中途想要取消,需要使用AbortController
对象。
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
上面示例中,首先新建 AbortController
实例,然后发送fetch()
请求,配置对象的signal
属性必须指定接收 AbortController
实例发送的信号controller.signal
。
controller.abort()
方法用于发出取消信号。这时会触发abort
事件,这个事件可以监听,也可以通过controller.signal.aborted
属性判断取消信号是否已经发出。
下面是一个1秒后自动取消请求的例子。
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;
}
}
# keepalive
keepalive
属性用于页面卸载时,告诉浏览器在后台保持连接,继续发送数据。
一个典型的场景就是,用户离开网页时,脚本向服务器提交一些用户行为的统计信息。这时,如果不用keepalive
属性,数据可能无法发送,因为浏览器已经把页面卸载了。
window.onunload = function() {
fetch('/analytics', {
method: 'POST',
body: "statistics",
keepalive: true
});
};
# redirect
redirect
属性指定HTTP
跳转的处理方法。可能的取值如下:
- follow:默认值,fetch()跟随 HTTP 跳转。
- error:如果发生跳转,fetch()就报错。
- manual:fetch()不跟随 HTTP 跳转,但是response.url属性会指向新的 URL,response.redirected属性会变为true,由开发者自己决定后续如何处理跳转。
# integrity
integrity
属性指定一个哈希值,用于检查HTTP
回应传回的数据是否等于这个预先设定的哈希值。
比如,下载文件时,检查文件的 SHA-256
哈希值是否相符,确保没有被篡改。
fetch('http://site.com/file', {
integrity: 'sha256-abcdef'
});
# referrer
referrer
属性用于设定fetch()
请求的referer
标头。
这个属性可以为任意字符串,也可以设为空字符串(即不发送referer
标头)。
fetch('/page', {
referrer: ''
});
# referrerPolicy
referrerPolicy
属性用于设定Referer
标头的规则。可能的取值如下:
- 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标头。
# 封装fetch
const caches = {};// 缓存所有已经请求的Promise,同一时间重复的不再请求
const getUniqueKey = config => config.url + config.method + (JSON.stringify(config.data) || '');
const getApi = () => process.env.VUE_APP_DCV_API;
const STOPAJAX_ERROR = 'Ajax has been stopped! ';
let IS_AJAX_STOP = false;
/**
* 停止ajax
*/
const stopAjax = function () {
IS_AJAX_STOP = true;
};
const isAjaxStopped = function () {
return IS_AJAX_STOP;
};
const showMessage = function (msg, config) {
if (config && config.isShowAlert === false) {
return;
}
if (!msg) {
console.error(config, 'No message available');
return;
}
console.error(config, msg);
};
/**
* 取消接口请求
* @param {AbortController} controller 取消控制器
*/
const cancel = (controller) => {
if (controller) {
controller.abort();
}
};
/**
* 取消所有接口请求
*/
const cancelAll = () => {
Object.values(caches).forEach(({ controller }) => {
cancel(controller);
});
};
const getHeaders = (type, headers = {}) => {
const contentType = type === 'POST' ? 'application/json; charset=utf-8' : undefined; //这个要看后台约定的格式,一般是json
return {
REQUEST_HEADER: 'binary-http-client-header',
'X-Requested-With': 'XMLHttpRequest',// 后台根据这个值判断当前属于是否浏览器调用。如果不是,返回的错误信息就是html格式,那不是我们需要的
language: getDefaultLanguage(),
token: getToken(),
contentType,
...headers
};
};
/**
* ajax请求
* @param {Object} config 配置
*/
const ajax = async (config) => {
const {
url,
baseURL, //接着的前缀url
headers,
data = {},
method = 'POST',
credentials = 'omit',
isFile,
isUseOrigin,
isOutFormat, //是否跳过系统返回格式验证
isEncodeUrl, //get请求时是否要进行浏览器编码
...otherParams
} = config;
let tempUrl = url;
if (baseURL) {
if (!url.startsWith('/') && !baseURL.endsWith('/')) {
tempUrl = baseURL + '/' + url;
} else {
tempUrl = baseURL + url;
}
}
let obj = data;
if (method.toUpperCase() === 'GET') {
obj = null;//get请求不能有body
const exArr = [];
for (const key in data) {
exArr.push(key + '=' + data[key]);
}
if (exArr.length > 0) {
const exUrl = isEncodeUrl ? encodeURI(encodeURI(exArr.join('&'))) : exArr.join('&'); //这里怎么加密,与后台解密方式也有关。如果不是这样的格式,就自己拼接url
if (!tempUrl.includes('?')) {
tempUrl += '?' + exUrl;
} else {
tempUrl += '&' + exUrl;
}
}
} else {
if (typeof data === 'object') {
if (isFile) { //文件上传
const formData = new FormData(); //构造空对象,下面用append方法赋值。
for (const key in data) {
formData.append(key, data[key]);//例:formData.append("file", document.getElementById('fileName').files[0]);
}
obj = formData;
if (!headers || headers['Content-Type'] === undefined) {
headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
} else {
obj = JSON.stringify(data);
}
}
}
try {
let response = await fetch(tempUrl, {
headers: getHeaders(method, headers),
body: obj,
method,
credentials,
...otherParams
});
if (!response.ok) {//代表网络请求失败,原因可能是token失效,这时需要跳转到登陆页
console.error(`HTTP error, status = ${response.status}, statusText = ${response.statusText}`);
if (response.status === 401) { //权限问题
// showMessage('token过期!', config);
stopAjax();
cancelAll();
toLogin();
}
return Promise.reject(response);
}
if (isUseOrigin) {
return response;
}
//以下处理成功的结果
const result = await response.json();
if (isOutFormat || url.endsWith('.json')) { // isOutFormat忽略返回格式要求
return result;
}
if (result && result.success) {
return result.data === undefined ? result : result.data;
}
//失败
showMessage(result.message, config);
return Promise.reject(result);
} catch (err) {//代表网络异常
if (err.name === 'AbortError') {//属于主动取消的
return Promise.reject(err);
}
showMessage(err, config);
}
};
/**
* 实现fetch的timeout 功能
* @param {object} fecthPromise fetch
* @param {Number} timeout 超时设置
* @param {AbortController} controller 取消控制器
**/
const fetch_timeout = (fecthPromise, { timeout = 2 * 60 * 1000, controller }) => {
let tp;
let abortPromise = new Promise((resolve, reject) => {
tp = setTimeout(() => {
cancel(controller);
reject({
code: 504,
message: 'DCV_REQUEST_TIMEOUT'
});
}, timeout);
});
return Promise.race([fecthPromise, abortPromise]).then(res => {
clearTimeout(tp);
return res;
});
};
/**
* 缓存请求,现一请求的拦截不再向后台发送
* @param {Object} config
* example
* {
* url: 'getDownData',
* baseURL: '/test-api/', //拼接前缀
* method:'POST',//参数传递方式,默认为POST
* data:{"jsonIds":["aaa","bbb"],"isAs":true}, //json或字符串
* isFile:false, //为true时,代表为formData表单上传文件,此时data中要上传的文件类似以下格式:{file:document.getElementById('fileName').files[0]}
* isDownload:false, //为true时,代表是用流的方式下载,这时是创建了一个form表单
* timeout:120000,//超时时间120s
* isShowAlert:true //为false时,不再弹出提示
* isOutFormat:false,//为true时,返回结果不再强制要求success:true,即有返回值,就认为是成功的结果
* isOutStop:false, //为true时,在所有ajax接口都停止时,它可以继续请求
* credentials:'omit', //默认是omit,忽略cookie的发送;same-origin: 表示cookie只能同域发送,不能跨域发送;include: cookie既可以同域发送,也可以跨域发送
* headers:{}, //请求头
* isUseOrigin:false, //为true时,直接返回response,不再处理结果
* mode:'same-origin' //same-origin:该模式是不允许跨域的,它需要遵守同源策略,否则浏览器会返回一个error告知不能跨域;其对应的response type为basic。
* cors: 该模式支持跨域请求,顾名思义它是以CORS的形式跨域;当然该模式也可以同域请求不需要后端额外的CORS支持;其对应的response type为cors。
* no-cors: 该模式用于跨域请求但是服务器不带CORS响应头,也就是服务端不支持CORS;这也是fetch的特殊跨域请求方式;其对应的response type为opaque。
* }
*/
const main = (config) => {
const { isOutStop, signal, method, type } = config;
if (!isOutStop && isAjaxStopped()) {
return Promise.reject(STOPAJAX_ERROR);
}
if (method === undefined && type !== undefined) { //兼容旧ajax的写法
config.method = type;
}
const isCanAbort = signal === undefined && typeof window.AbortController === 'function';//是否可以取消
const uniqueKey = getUniqueKey(config);
if (!caches[uniqueKey]) {
let controller;
if (isCanAbort) {
controller = new AbortController();
config.signal = controller.signal;
}
const promise = ajax(config).then((result) => {
delete caches[uniqueKey];
return result;
}, (err) => {
delete caches[uniqueKey];
throw err;
});
caches[uniqueKey] = {
promise: fetch_timeout(promise, {
timeout: config.timeout,
controller
}),
controller: controller
};
}
return caches[uniqueKey].promise;
};
export const get = (url, data, config) => {
config = config || {};
if (!config.baseURL) config.baseURL = getApi();
config.method = 'get';
config.url = url;
config.data = data;
return main(config);
};
export const post = (url, data, config) => {
config = config || {};
if (!config.baseURL) config.baseURL = getApi();
config.method = 'post';
config.url = url;
config.data = data;
return main(config);
};
export default main;
# jQuery的ajax请求中contentType与dataType区别
最近遇到一个老项目,有同事在用这两个时候有点儿懵逼,我也有点儿混了,捡起来重新看下。
简单说,区别是:
contentType
: 告诉服务器,我要发送
什么类型的数据
dataType
:告诉服务器,我要接收
什么类型的数据,如果没有指定,那么会自动推断是返回 XML,还是JSON,还是script,还是String。
从jQuery
的源码来看,最终dataType
是设置请求头Accept
。
# axios防御CSRF攻击
什么是CSRF
攻击,这里就不详解了,请看这篇。
axios
一个优势就是可以防御它,怎么做到的呢?
主要是在headers
中添加一个与后台约定的字段,而它的值又是从cookie
中读取的。它有2个配置项:
xsrfCookieName: 'XSRF-TOKEN', // default
xsrfHeaderName: 'X-XSRF-TOKEN', // default
前者是cookie
中的token
字段名称,也就是说,如果cookie
中有这个字段,就会在接口请求的headers
中添加一个xsrfHeaderName
对应的字段(默认是X-XSRF-TOKEN
),这个字段需要与后台约定好,后台拿到以后就可以进行校验了。
假设我们后台校验的字段名称就叫token
,一般cookie
中这个字段也叫token
,那么只需要修改默认的配置,就可以了:
axios.defaults.xsrfCookieName = 'token';
axios.defaults.xsrfHeaderName = 'token';
本文参考