# ajax常见问题

# ajax成功状态

根据返回的状态值status判断

200300或者304304代表资源没有修改,可以使用缓存

来一段原始的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请求可能会被浏览器缓存使用。如果不使用缓存,有以下方案:

  1. 发送请求前加上
xhr.setRequestHeader("If-Modified-Since","0")
  1. 发送请求前加上
 xhr.setRequestHeader("Cache-Control","no-cache")
  1. 在URL后面加上一个随机数或时间戳:
url += "fresh=" + Math.random();
url += "time=" + new Date().getTime();
  1. 如果是使用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:请求方法只限于 GETPOSTHEAD,并且只能使用有限的几个简单标头,不能添加跨域的复杂标头,相当于提交表单所能发出的请求。

# 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';