Fetch API 初探

JavaScript 通过XMLHttpRequest(XHR)来执行异步请求,设计上不符合职责分离原则,将输入、输出和用事件来跟踪的状态混杂在一个对象里。而且,基于事件的模型与最近JavaScript流行的Promise以及基于生成器的异步编程模型不太搭。新的 Fetch API打算修正上面提到的那些缺陷。

接着上一篇 重拾Ajax,继续一探异步请求的接班人Fetch API…

概念和用法

Fetch 提供了对 RequestResponse (以及其他与网络请求有关的)对象通用的定义。它之后能够被使用到很多场景中:service workers、Cache API、其他处理请求和响应的方式,甚至任何需要生成自己的响应的方式。参考资料WHATWG Fetch 规范MDN FetchAPI

Fetch 是一个很先进的概念,类似于 XMLHttpRequest。它提供了很多 XMLHttpRequest 拥有的功能,不过它被设计成具有更强的可扩展性和更高效。

Chrome, Opera, FirefoxAndroid 浏览器的最新版本都支持Fetch API。 对于不支持的浏览器,你可以借助于fetch-polyfill来提供辅助实现。

fetch() 方法用于发起获取资源的请求。它返回一个 promise,这个 promise 会在请求响应后被 resolve,并传回 Response 对象。

认识fetch之前,先来介绍一下与fetch相关的三个对象 Request、Body、Response、Headers

Headers

请求头对象,Fetch API 的Headers类允许你去对HTTP requestresponse 的headers执行各种操作。 这些操作包括:检索, 设置, 添加和删除。 一个Headers类里包含一个header列表, 它的初始值为空或者是零个或多个键值对。你可以使用 append()方法添加, 这个类中所有的方法, 其 header的名字顺序匹配并不区分大小写。MDN

方法
append() 添加一个header信息
delete() 删除指定的header
entries() 返回headers对象中的所有键值对,是一个 iterator 对象
get() 从Headers对象中返回指定的值
getAll() 返回全部的header
has() 检测指定的header,返回布尔值
keys() 返回所有的header的键,是一个iterator对象
set() 修改或添加header
values() 返回所有的header的值 ,是一个iterator对象

可以是一个简单的多映射的名-值表

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

也可以是一个json对象

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

Headers的内容可以被检索

myHeaders = new Headers({
"Content-Type": "text/plain",
"Content-Length": content.length.toString(),
"X-Custom-Header": "ProcessThisImmediately",
});
console.log(myHeaders.has("Content-Type")); // true
console.log(myHeaders.has("Set-Cookie")); // false
myHeaders.set("Content-Type", "text/html");
myHeaders.append("X-Custom-Header", "AnotherValue");

console.log(myHeaders.get("Content-Length")); // 11
console.log(myHeaders.getAll("X-Custom-Header")); // ["ProcessThisImmediately", "AnotherValue"]

myHeaders.delete("X-Custom-Header");
console.log(myHeaders.getAll("X-Custom-Header")); // [ ]

fetch(myRequest).then(function(response) {
if(response.headers.get("content-type") === "application/json") {
return response.json().then(function(json) {
// process your JSON further
});
} else {
console.log("Oops, we haven't got JSON!");
}
});

Request

FetchAPI的资源请求对象MDN

构造器创建一个实例

var req  = new Request('data.json', {
method:'POST',
headers:{},
body:new FormData(document.getElementById('login-form')),
cache:'default',
})

属性

method 请求的方法POST/GET等
url 请求的地址
headers 请求头(可以是Headers对象,也可是JSON对象)
context 请求的上下文
referrer 指定请求源地址
mode 请求的模式(是跨域cors还是正常请求no-cors)
credentials 跨域请求时,是否携带cookie信息(omit跨域携带/same-origin同源携带)
redirect 重定向
integrity 一个散列值,用于检验请求资源的完整性MDN
cache 是否缓存这个请求

方法

clone() 复制一个当前request对象的实例

Body

Fetch mixin 对象,提供了关联 response/request 中 body 的方法,可以定义它的文档类型以及请求如何被处理。

RequestResponse 对象都实现了Body的接口,所以都拥有Body的方法和属性,用于指定请求体中的body或响应体的内容的数据类型(arrayBuffer/blob /json/text) 主要是做数据类型的转换。

属性

bodyUsed 用于判断是否在响应体中是否设置过body读取类型

方法

arrayBuffer() 将响应流转换为buffer数组的promise对象,并将bodyUsed状态改为已使用
blob() 将响应流转换为大的二进制的promise对象,并将bodyUsed 状态改为已使用,一般用于文件读取(下载大文件或视频)
formData() 将响应流转换为formData的promise对象,并将bodyUsed状态改为已使用
json() 将响应流转换为json的promise对象,并将bodyUsed状态改为已使用
text() 将响应流转换为文本字符串的promise对象,并将bodyUsed状态改为已使用

Response

FetchAPI的响应对象MDN

属性(只读)

type 响应的类型 basic/cors等
url 包含Response的URL.
useFinalURL 包含了一个布尔值来标示这是否是该Response的最终URL
status 响应的状态码 1xx-5xx
ok 表示响应成功
statusText 状态码的信息
headers 响应头的Headers对象
bodyUsed 是否设置过响应内容的类型

方法

clone() 创建一个Response对象的克隆
error() 返回一个绑定了网络错误的新的Response对象
redirect() 用另一个URL创建一个新的 response.

Fetch 语法 和 示例

语法

fetch('api/data.json', {
method:'POST', //请求类型GET、POST
headers:{},// 请求的头信息,形式为 Headers 对象或 ByteString。
body:{},//请求发送的数据 blob、BufferSource、FormData、URLSearchParams(get或head方法中不能包含body)
mode:'',//请求的模式,是否跨域等,如 cors、 no-cors 或者 same-origin。
credentials:'',//cookie的跨域策略,如 omit、same-origin 或者 include。
cache:'', //请求的 cache 模式: default, no-store, reload, no-cache, force-cache, or only-if-cached.
}).then(function(response) { ... });

mode

  • no-cors 允许来自CDN的脚本、其他域的图片和其他一些跨域资源,但是首先有个前提条件,就是请求的method只能是”HEAD”,”GET”或者”POST”。此外,任何 ServiceWorkers 拦截了这些请求,它不能随意添加或者改写任何headers,除了这些。第三,JavaScript不能访问Response中的任何属性,这保证了 ServiceWorkers 不会导致任何跨域下的安全问题而隐私信息泄漏。
  • cors 通常用作跨域请求来从第三方提供的API获取数据。这个模式遵守CORS协议。只有有限的一些headers被暴露给Response对象,但是body是可读的。
  • same-origin 如果一个请求是跨域的,那么返回一个简单的error,这样确保所有的请求遵守同源策略。

cache

  • default 缓存相同的请求
  • no-store 不缓存任何请求
  • reload 创建一个正常的请求,并用响应更新HTTP缓存
  • no-cache 如果HTTP缓存中有响应,并且不是正常请求,则Fetch创建条件请求。然后,它使用响应更新HTTP缓存。
  • force-cache Fetch使用HTTP缓存中与请求匹配的任何响应,不管是否过期。如果没有响应,则会创建正常请求,并使用响应更新HTTP缓存。
  • only-if-cached Fetch使用HTTP缓存中与请求匹配的任何响应,不管是否过期。如果没有响应,则返回网络错误。 (只有当请求的模式为“same-origin”时,才能使用任何缓存重定向,假设请求的重定向模式为“follow”,重定向不会违反请求的模式)。

如果header中包含名称为“If-Modified-Since”“If-None-Match”“If-Unmodified-Since”“If-Match”“If-Range”之一,如果是“default”,fetch 会将 cache 自动设置为 “no-store”

Fetch 示例

var myHeaders = new Headers();

var myInit = { method: 'GET',
headers: myHeaders,
mode: 'cors',
cache: 'default' };

var myRequest = new Request('flowers.jpg',myInit);

fetch(myRequest,myInit)
.then(function(response) {
return response.blob();
})
.then(function(myBlob) {
var objectURL = URL.createObjectURL(myBlob);
myImage.src = objectURL;
});

简单封装

前面有了ajax的封装过程,fetch的封装就更简单了,因为fetch()方法本身返回的是promise对象,那就不需要使用promise再包装了,代码也简洁很多。

class AjaxFetch{
constructor(opts, params){
const isUrl = typeof opts === 'string';
this.defaults = {
method:'GET',
headers:{},
data:{},
credentials:'include', //默认不带cookie,指定inlude,始终携带cookie
cache:'default',
// mode:''//请求时会自动识别是否跨域,不需要手动设置
};

this.options = Object.assign(this.defaults, (isUrl ? params : opts) || {});
this.methods = ['GET','PUT','PATCH','DELETE','POST'];
this.url = isUrl ? opts : this.options.url;
this.init();

return isUrl ? this : this[this.options.method.toLowerCase()](this.options.data)
}

init(){
this.methods.forEach(method=>{
this[method.toLowerCase()] = data => {
if('GET' == method){
this.url += (this.url.includes('?')?'&':'?' + this.transformData(data))
}else{
if(data instanceof FormData){
this.options.headers['Content-Type'] = 'multipart/form-data;';
}else{
this.options.headers['Content-Type'] = 'application:/x-www-form-urlencoded:charset=UTF-8';
}
this.options.body = this.transformData(data);
}
delete this.options.data;
this.options.method = method;
return fetch(this.url, this.options);
}
})
}
transformData(obj){
// 这里还需要做更多的处理
if(obj instanceof FormData) return obj;

var params = [];
for(var i in obj){
params.push(`${i}=${obj[i]}`);
}
return params.join('&');
}
}

function http(opt, pms){
if(!opt) return;
return new AjaxFetch(opt, pms);
}

使用

var api = {
news: http('api/d1.json'),
users: http('api/d2.json')
}

//get
api.news.get({uname:'xxx', pwd:123}).then(res=>{
console.log(res);
}).catch(err=>{
console.log(err);
})

//post
var frmData = new FormData(document.getElementById('frm1'));
api.news.post(frmData).then(res=>{
console.log(res);
}).catch(err=>{
console.log(err);
})

//类似$.ajax
http({
url:'api/d1.json',
method:'POST',
headers:{'Content-Type':'multipart/form-data; boundary=----'+new Date().getTime()},
data: new FormData(document.getElementById('frm1'))
}).then(res=>{
console.log(res)
debugger;
})

结合Async

async function test2(){
let r1 = await api.news.get({'name':'aa'});
let r2 = await api.users.post(new FormData(document.getElementById('frm1')))
return [r1, r2];
}
var res = test2();
console.log(res);