采坑记--fetch 请求header与body配置及常用问题

崔琦
2023-12-01

fetch 请求-header与body配置及常用问题

引入

说道fetch就不得不提XMLHttpRequest了,XHR在发送web请求时需要开发者配置相关请求信息和成功后的回调,尽管开发者只关心请求成功后的业务处理,但是也要配置其他繁琐内容,导致配置和调用比较混乱,也不符合关注分离的原则;fetch的出现正是为了解决XHR存在的这些问题。例如下面代码:

fetch(url).then(function(response) {
  return response.json();
}).then(function(data) {
  console.log(data);
}).catch(function(e) {
  console.log("Oops, error");
});

上面这段代码让开发者只关注请求成功后的业务逻辑处理,其他的不用关心,相当简单;也比较符合现代Promise形式,比较友好。

fetch是基于Promise设计的,从上面代码也能看得出来,这就要求fetch要配合Promise一起使用。正是这种设计,fetch所带来的优点正如传统 Ajax 已死,Fetch 永生总结的一样:

  • 语法简单,更加语义化
  • 基于标准的Promise实现,支持async/await
  • 使用isomorphic-fetch可以方便同构

不过话说回来,fetch虽然有很多优点,但是使用fetch来进行项目开发时,需要的配置,以及遇到的fetch使用的常见问题。

大多数情况下,在前端发起一个网络请求我们只需关注下面几点:

  • 传入基本参数(url,请求方式)
  • 请求参数、请求参数类型
  • 设置请求头
  • 获取响应的方式
  • 获取响应头、响应状态、响应结果
  • 异常处理
  • 携带cookie设置
  • 跨域请求

mode

  • same-origin - 如果使用此模式设置对另一个原点的请求,则结果只是一个错误。您可以使用它来确保始终对您的原点提出请求。
  • no-cors- 防止该方法成为除了HEAD,GET或者POST之外的任何内容,并且标头不作为简单标题以外的任何内容。如果任何 ServiceWorkers拦截这些请求,它们可能不会添加或覆盖任何标题,除了那些简单的标题。另外,JavaScript可能无法访问Response结果的任何属性。这确保ServiceWorkers不会影响Web的语义,并防止跨域泄漏数据而导致的安全和隐私问题。
  • cors - 允许跨源请求,例如访问第三方供应商提供的各种API。这些预计将遵守CORS协议。只有一些有限的头文件暴露在Response中,但是正文是可读的。
  • navigate - 支持导航的模式。该navigate值仅用于HTML导航。导航请求仅在文档之间导航时创建。

如果未定义,cors则假定为默认值。

cache

cache 属性值
一个RequestCache值。可用的值是:

  1. default:浏览器在HTTP缓存中查找匹配的请求。

    • 如果有匹配项并且它是新的,则将从缓存中返回。
    • 如果有匹配项,但是已经过期,则浏览器将向远程服务器发出有条件的请求。如果服务器指示资源没有改变,它将从缓存中返回。否则,资源将从服务器下载,并且缓存将被更新。
    • 如果不匹配,浏览器将发出正常的请求,并用下载的资源更新缓存。
  2. no-store:浏览器从远程服务器获取资源,而不先查看缓存,并且不会使用下载的资源更新缓存。

  3. reload:浏览器从远程服务器获取资源,而不先查看缓存,然后用下载的资源更新缓存。

  4. no-cache :浏览器在HTTP缓存中查找匹配的请求。

    • 如果有新的或旧的匹配,浏览器将向远程服务器发出有条件的请求。如果服务器指示资源没有改变,它将从缓存中返回。否则,资源将从服务器下载,缓存将被更新。
    • 如果不匹配,浏览器将发出正常的请求,并用下载的资源更新缓存。
  5. force-cache:浏览器在HTTP缓存中查找匹配的请求。

    • 如果有新的或旧的匹配,它将从缓存中返回。
    • 如果不匹配,浏览器将发出正常的请求,并用下载的资源更新缓存。
  6. only-if-cached:浏览器在HTTP缓存中查找匹配的请求。

    • 如果有新的或旧的匹配,如果将从缓存返回。
    • 如果不匹配,浏览器将返回一个错误。

该"only-if-cached"模式只能用于请求的mode为"same-origin"的情况。如果请求的redirect属性是"follow",并且重定向不违反"same-origin"模式,则会遵循缓存重定向。

credentials:

  • omit:从不发送cookie。
  • same-origin:如果URL与调用脚本位于相同的源,则发送用户凭证(cookie,基本http认证等)。
  • include:始终发送用户凭据(cookie,基本http认证等),甚至用于跨源调用。

fetch默认是不发送cookie

headers

1.用法

该Headers接口允许您通过Headers()构造函数创建自己的headers对象。headers对象是名称到值的简单多重映射:

var content = "Hello World";
var myHeaders = new Headers();
myHeaders.append("Content-Type", "text/plain");
myHeaders.append("Content-Length", content.length.toString());
myHeaders.append("X-Custom-Header", "ProcessThisImmediately");

同样可以通过传递一个数组或一个对象字面值给构造函数来实现:

myHeaders = new Headers({
  "Content-Type": "text/plain",
  "Content-Length": content.length.toString(),
  "X-Custom-Header": "ProcessThisImmediately",
});

还可以:

headers:{
    "Content-Type":"application/x-www-form-urlencoded"
}

内容可以被查询和检索:

console.log(myHeaders.has("Content-Type")); // true
myHeaders.set("Content-Type", "text/html");
myHeaders.append("X-Custom-Header", "AnotherValue");

console.log(myHeaders.get("X-Custom-Header")); // "ProcessThisImmediately"
 
myHeaders.delete("X-Custom-Header");

2.配置

get不存在请求实体部分,键值对参数放置在 URL 尾部,因此请求头不需要设置 Content-Type 字段。
用于标记请求体数据的格式Content-Type

  1. 1 Content-Type:application/json
    现在越来越多的人把它作为请求头,用来告诉服务端消息主体是序列化后的 JSON字符串。实际上,由于 JSON规范的流行,除了低版本 IE之外的各大浏览器都原生支持 JSON.stringify,服务端语言也都有处理 JSON的函数,使用 JSON不会遇上什么麻烦。
// 消息主体是序列化json字符串
形式:
{"name":"小明","password":"123456"}

controller 的入参使用@RequestBody修饰,说明是要使用json的格式接收。request.getInputStream(),request.getReader() 获取。并且getInputStream获取参数后,request.getParameter() 再不能得到参数。

  1. 2 Content-Type: application/x-www-form-urlencoded
// 浏览器原生的form表单,请求体中的数据会以普通表单形式(键值对)发送到后端
形式:
key1=value1&key2=value2
后端取值方式:
request.getParameter()、request.getParameterMap()
  1. 3. multipart/form-data,将表单的数据处理为一条消息,以标签为单元,用分隔符分开。既可以上传键值对,也可以上传文件
// 发送 POST 请求,参数为:aaa=aaa,bbb=你的我的啊啊啊,file=图片
POST / HTTP/1.1
Host: www.bilibili.com
Content-Type: multipart/form-data;boundary=------FormBoundary15e896376d1
Content-Length: 19532

------FormBoundary15e896376d1
Content-Disposition: form-data; name="aaa"

aaa
------FormBoundary15e896376d1
Content-Disposition: form-data; name="bbb"

你的我的啊啊啊
------FormBoundary15e896376d1
Content-Disposition: form-data; name="file"; filename="cat-icon.png"
Content-Type: image/png

[message-part-body; type:image/png, size:19201 bytes]
------FormBoundary15e896376d1--

首先生成了一个 boundary 用于分割不同的字段,为了避免与正文内容重复,boundary 很长很复杂。
然后 Content-Type 里指明了数据是以 mutipart/form-data 来编码,本次请求的 boundary 是什么内容。
消息主体里按照字段个数又分为多个结构类似的部分,每部分都是以 –boundary 开始,紧接着内容描述信息,然后是回车,最后是字段具体内容(文本或二进制)。
如果传输的是文件,还要包含文件名和文件类型信息。消息主体最后以 –boundary– 标示结束。

  1. 4 text/xml 

它是一种使用 HTTP作为传输协议,XML 作为编码方式的远程调用规范。

body

请求和响应都可能包含body数据。一个body是以下任何一种类型的实例:

  • ArrayBuffer
  • ArrayBufferView (Uint8Array和扩展)
  • Blob/File
  • string
  • URLSearchParams
  • FormData

Body mixin定义了以下方法来提取体(由得到的Request和Response实施)。这些都会返回一个最终解决实际内容的承诺。

  • arrayBuffer()
  • blob()
var debug = {hello: "world"};
var blob = new Blob([JSON.stringify(debug, null, 2)], {type : 'application/json'});
  • json()
  • text()
  • formData()

上传JSON数据

使用fetch()开机自检JSON编码的数据。
将参数序列化json字符串进行传递

var url = 'https://example.com/profile';
var data = {username: 'example'};

fetch(url, {
  method: 'POST', // or 'PUT'
  body: JSON.stringify(data), //'{"name":"hehe","age":10}'
  headers: new Headers({
    'Content-Type': 'application/json'
  })
}).then(res => res.json())
.catch(error => console.error('Error:', error))
.then(response => console.log('Success:', response));

请求体中的数据会以普通表单形式(键值对)发送到后端

const options = {
    method: "POST",
    body: qs.stringify(data),// 'name=hehe&age=10'
    headers: {
        "Content-Type": "application/x-www-form-urlencoded",
    },
};
// URLSearchParams,插入一个指定的键/值对作为新的搜索参数。
let url = new URL('https://example.com?foo=1&bar=2');
let params = new URLSearchParams(url.search.slice(1));
//添加第二个foo搜索参数。
params.append('foo', 4);
//查询字符串变成: 'foo=1&bar=2&foo=4'

注:
当使用application/json 的时候,body直接是 JSON.stringify(paramObject) 比较简单
当使用 ‘Content-Type’: ‘application/x-www-form-urlencoded’,时候,需要将对象转换为普通表单形式(键值对)的字符串
疑惑:body已经将数据封装成想要的格式,那fetch 请求中的请求头设置Content-Type到底改变了什么

JSON.parse(用于从一个字符串中解析出json 对象)
JSON.stringify(用于从一个对象解析出字符串)
qs.parse()// 将URL解析成对象的形式
qs.stringify()// 将对象解析成URL的形式

上传文件

可以使用 HTML

 <input type="file"/>

input 元素、FormData ()将form表单元素的name与value进行组合,实现表单数据的序列化,从而减少表单元素的拼接, 和fetch()来上载文件。

var formData = new FormData();
var fileField = document.querySelector("input[type='file']");

formData.append('username', 'abc123');
formData.append('avatar', fileField.files[0]);

fetch('https://example.com/profile/avatar', {
  method: 'PUT',
  body: formData,
  headers: {'Content-Type': 'multipart/form-data'}}
})
.then(response => response.json())
.catch(error => console.error('Error:', error))
.then(response => console.log('Success:', response));

Response对象

当fetch() promise被解析时,Response实例被返回。

一个 Promise,resolve 时回传 Response 对象:

属性:

  • status (number) - HTTP请求结果参数,在100–599 范围, 包含响应状态码的整数(默认值200)
  • statusText (String) - 服务器返回的状态报告 。一个字符串(默认值“OK”),对应于HTTP状态码消息。
  • ok (boolean) - 这是一个用于检查状态是否在200-299范围内的简写,表示请求成功。这返回一个Boolean。
  • headers (Headers) - 返回头部信息,下面详细介绍
  • url (String) - 请求的地址
has(name) (boolean) - 判断是否存在该信息头
get(name) (String) - 获取信息头的数据
getAll(name) (Array) - 获取所有头部数据
set(name, value) - 设置信息头的参数
append(name, value) - 添加header的内容
delete(name) - 删除header的信息
forEach(function(value, name){ ... }, [thisContext]) - 循环读取header的信息

方法:

  • text() - 以string的形式生成请求text
  • json() - 生成JSON.parse(responseText)的结果
  • blob() - 生成一个Blob
  • arrayBuffer() - 生成一个ArrayBuffer
  • formData() - 生成格式化的数据,可用于其他的请求

其他方法:

  • clone()
  • Response.error()
  • Response.redirect()

特点:fetch请求对某些错误http状态不会reject

这主要是由fetch返回promise导致的,因为fetch返回的promise在某些错误的http状态下如400、500等不会reject,相反它会被resolve;
仅仅在发生“network error(网络错误)”才会被拒绝。如果可以服务器获得http错误状态,则表明服务器正常工作且在处理请求,而“network error(网络错误)”表示根本无法到达服务器(例如连接拒绝或名称未解析)或请求配置有错误(错误的请求地址)。

// 解决
if(response.ok){
    return response.json();
}
else { 
    return Promise.reject({
        status: response.status,
        statusText: response.statusText
    })
}

对fetch请求做一层封装。

function checkStatus(response) {
  if (response.status >= 200 && response.status < 300) {
    return response;
  }
  const error = new Error(response.statusText);
  error.response = response;
  throw error;
}
function parseJSON(response) {
  return response.json();
}
export default function request(url, options) {
  let opt = options||{};
  return fetch(url, {credentials: 'include', ...opt})
    .then(checkStatus)
    .then(parseJSON)
    .then((data) => ( data ))
    .catch((err) => ( err ));
}

其他

fetch不支持jsonp

// 解决
npm install fetch-jsonp --save-dev

fetchJsonp('/users.jsonp', {
    timeout: 3000,
    jsonpCallback: 'custom_callback'
  })
  .then(function(response) {
    return response.json()
  }).catch(function(ex) {
    console.log('parsing failed', ex)
  })

fetch不支持超时timeout处理

核心是利用建立一个超时的abortPromise和接口请求的fetchPromise传入 Promise.race() 来进行处理,哪个Promise先返回结果则最终输出这个Promise的返回值。

var oldFetchfn = fetch; //拦截原始的fetch方法
window.fetch = function(input, opts){//定义新的fetch方法,封装原有的fetch方法
    var fetchPromise = oldFetchfn(input, opts);
    var timeoutPromise = new Promise(function(resolve, reject){
        setTimeout(()=>{
             reject(new Error("fetch timeout"))
        }, opts.timeout)
    });
    retrun Promise.race([fetchPromise, timeoutPromise]) 
    //哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。
}

兼容性

伴随fetch的问世及发展,目前在生产环境使用fetch的企业越来越多。开源社区上有关fetch polyfilll已经多款供选,又因其基于Promise,以下可以完美的解决其兼容性问题(特别是IE),仅参考:

  • 由于 IE8 是 ES3,需要引入 ES5 的 polyfill: es5-shim, es5-sham
  • 引入 Promise 的 polyfill: es6-promise
  • 引入 fetch 探测库:fetch-detector
  • 引入 fetch 的 polyfill: fetch-ie8 // 探测是否存在 window.fetch 方法,如果没有则用 XHR 实现。
  • 可选:如果你还使用了 jsonp,引入 fetch-jsonp
  • 可选:开启 Babel 的 runtime 模式,现在就使用 async/await

总结

fetch 的好处无需多语,但fetch也存在一些问题,例如

  • 不主动携带cookie,
  • http错误状态处理,
  • 不支持jsonp,
  • ie兼容性,
  • 请求参数的配置,
  • 不支持timeout,
  • 不支持 progress,

特别是fetch在跨域问题上与传统跨域的处理方式的区别。

 类似资料: