Fetch & Axios 介绍及使用区别

2,432 阅读9分钟

最近在看项目中一些封装好的基础请求包的实现代码,也顺便复习了下 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()就报错。
  • manualfetch()不跟随 HTTP 跳转,但是response.url属性会指向新的 URL,response.redirected属性会变为true,由开发者自己决定后续如何处理跳转。

referrer : 用于设定 fetch() 请求的 referer 标头。

这个属性可以为任意字符串,也可以设为空字符串(即不发送referer标头)。

referrerPolicy : 用于设定 referrer 标头的规则。

  • no-referrer-when-downgrade:默认值,总是发送Referer标头,除非从 HTTPS 页面请求 HTTP 资源时不发送。
  • no-referrer:不发送Referer标头。
  • **originReferer**标头只包含域名,不包含完整的路径。
  • **origin-when-cross-origin:同源请求Referer**标头包含完整的路径,跨域请求只包含域名。
  • same-origin:跨域请求不发送Referer****,同源请求发送。
  • strict-originReferer标头只包含域名,HTTPS 页面请求 HTTP 资源时不发送Referer标头。
  • strict-origin-when-cross-origin:同源请求时Referer标头包含完整路径,跨域请求时只包含域名,HTTPS 页面请求 HTTP 资源时不发送该标头。
  • unsafe-url:不管什么情况,总是发送Referer标头。

signal: 指定一个 AbortSignal 实例,用于取消 fetch() 请求。

fetch() 请求发送以后,如果中途想要取消,需要使用 AbortController 对象。配置对象的 signal 属性必须指定接收 AbortController 实例发送的信号 controller.signalcontroller.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.headersResponse.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可以通过 XMLHttpRequestfetch API 来发送 HTTP 请求,而对于 Node.js 环境来说,adapter可以通过 Node.js 内置的 httphttps 模块来发送 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);
  });
 }

拦截器

在请求或响应被 thencatch 处理前拦截它们。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。可以看到,我从 configcancelToken: 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()AxiosResponse 是可以直接读取的对象

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: