更好的请求方式 — Fetch
关于 Fetch
首先,因为我们不能使用jQuery(应主题要求),所以我们并不能使用.ajax来实现请求
这不正好,Fetch API提供了一个JavaScript接口,我们可以通过Fetch来实现无刷新评论
好处就是任何使用过 XMLHttpRequest 的人都能轻松上手,而且新的 API 提供了更强大和灵活的功能集
请注意,fetch 规范与 jQuery.ajax() 主要有三种方式的不同:
- 当接收到一个代表错误的 HTTP 状态码时,从
fetch()返回的 Promise 不会被标记为 reject, 即使响应的 HTTP 状态码是 404 或 500。相反,它会将 Promise 状态标记为 resolve (但是会将 resolve 的返回值的ok属性设置为 false ),仅当网络故障时或请求被阻止时,才会标记为 reject。 fetch()可以不会接受跨域 cookies;你也可以不能使用fetch()建立起跨域会话。其他网站的Set-Cookie头部字段将会被无视。fetch不会发送 cookies。除非你使用了credentials 的初始化选项。(自 2017 年 8 月 25 日以后,默认的 credentials 政策变更为same-origin。Firefox 也在 61.0b13 版本中进行了修改)
了解Fetch
基本用法
一个基本的 fetch 请求设置起来很简单。这是他的基本用法
fetch(url)
.then(...)
.catch(...)
fetch('<https://api.github.com/users/wibus-wee>')
.then(response => response.json())
.then(json => console.log(json))
.catch(err => console.log('Request Failed', err));这里我们获取了GitHub API的数据(好吧其实这样子获取不到),之后将json打印至console,如果出现问题依然是打印到console
最简单的话就是直接给一个URL给fetch(),如果只有这样子的话,我们无法真实获得JSON内容,因为这样子只是一个HTTP响应,因为我们还要用json()来真正获取到这个数据;根据阮一峰的说法,fetch()接收到的response是一个 Stream 对象,response.json()是一个异步操作,取出所有内容,并将其转为 JSON 对象。
根据MDN的说法,json()方法在 Body mixin 中定义,被 Request 和 Response 对象实现
Promise -> await 改进
当然,上面的例子promise可以使用await进行改写,啊对还要async
async function fetchGetJson(url){
try{
let response = await fetch(url) //得到Stream对象
return await response.json(); //转化为json对象
}catch(err){
console.log('Request Failed: '+ err) //输出错误
}
}上面示例中,await语句必须放在try...catch里面,这样才能捕捉异步操作中可能发生的错误。
说实话我真的觉得用了await代码好看多了,也不至于明天在看这个代码就不懂()
因此,受到阮一峰大佬的启发,接下来将会使用这一套改进的写法
Response 对象
上面有讲过,Response是一个Stream对象
Response 对象的同步属性
- Response.ok
Response.ok可以返回true/false,表示请求是否成功,true对应 HTTP 请求的状态码 200 到 299,false对应其他的状态码。 - Response.status
Response.status属性返回一个数字,表示 HTTP 回应的状态码(例如200,表示成功请求)可以对应与.ok使用 - Response.statusText
Response.statusText属性返回一个字符串,表示 HTTP 回应的状态信息(例如请求成功以后,服务器返回"OK")。 - Response.url
Response.url属性返回请求的 URL。如果 URL 存在跳转,该属性返回的是最终 URL。 - Response.type
Response.type属性返回请求的类型。可能的值如下:basic:普通请求,即同源请求。cors:跨域请求。error:网络错误,主要用于 Service Worker。opaque:如果fetch()请求的type属性设为no-cors,就会返回这个值,详见请求部分。表示发出的是简单的跨域请求,类似<form>表单的那种跨域请求。opaqueredirect:如果fetch()请求的redirect属性设为manual,就会返回这个值,详见请求部分。 - Response.redirected
Response.redirected属性返回一个布尔值,表示请求是否发生过跳转。
Fetch 判断请求是否成功
通过上面的同步属性我们了解到两个东西:.ok, .status,fetch会出现error是在出现无法连接的时候,因此如果需要判断是否真的请求成功了,我们还需要做一个if判断
async function getJSON(url){
let response = await fetch(url);
if (response.status >= 200 && response.status < 300){
return await response.json()
}else{
console.error(response.statusText)
}
}response.status属性只有等于 2xx (200~299),才能认定请求成功。这里不用考虑网址跳转(状态码为 3xx),因为fetch()会将跳转的状态码自动转为 200。
另一种方法是判断response.ok是否为true。
async function getJSON(url){
let response = await fetch(url);
if (response.ok){
return await response.json()
}else{
console.error(response.statusText)
}
}Fetch Heade
Response还具有一个属性:.headers,这将可以对应HTTP回应的所有标头(通过指向一个Headers对象)就像这样:
const response = await fetch(url) //得到Streams对象
for (let [name,value] of response.headers){
console.log(`${name}:${value}`)
}对于Headers对象,它具有以下的方法:
Headers.get():根据指定的键名,返回键值。Headers.has(): 返回一个布尔值,表示是否包含某个标头。Headers.set():将指定的键名设置为新的键值,如果该键名不存在则会添加。Headers.append():添加标头。Headers.delete():删除标头。Headers.keys():返回一个遍历器,可以依次遍历所有键名。Headers.values():返回一个遍历器,可以依次遍历所有键值。Headers.entries():返回一个遍历器,可以依次遍历所有键值对([key, value])。Headers.forEach():依次遍历标头,每个标头都会执行一次参数函数。
可以看到有一些方法比如说:.append()可以用来添加标头,他们可以修改标头(因为从Headers接口继承而来的哈哈哈)
不过对于HTTP请求来说,没意思,毕竟浏览器不允许修改大部分返回的标头
这些方法中,最常用的是response.headers.get(),用于读取某个标头的值。
let response = await fetch(url);
response.headers.get('Content-Type')
// return application/json; charset=utf-8Fetch 读取
Response提供了对应不同内容的读取方法
response.text():得到文本字符串。response.json():得到 JSON 对象。response.blob():得到二进制 Blob 对象。response.formData():得到 FormData 表单对象。response.arrayBuffer():得到二进制 ArrayBuffer 对象。
上面5个读取方法都是异步的,返回的都是 Promise 对象。必须等到异步操作结束,才能得到服务器返回的完整数据。
.text
它可以读取返回的文本数据,譬如HTML
async function fetchGETText(url){
try{
let response = await fetch(url)
let text = await response.text();
return text
}catch(e){
console.log('Request Failed: ' + e)
}
}这样,就封了一个function来用,我们就可以这样子调用
// 譬如说
let HTML = fetchGETText('/comment.html');
document.body.innerHTML = HTML是不是很拽,是不是很搞笑的写法(直接点不就好了。。)对自己无语了
.json
其实前面已经说过了,不多说了
async function fetchGetJson(url){
try{
let response = await fetch(url) //得到Stream对象
return await response.json(); //转化为json对象
}catch(err){
console.log('Request Failed: '+ err) //输出错误
}
}.formData
它主要与 service workers 有关。如果用户提交表单并且 service workers 拦截请求,您可以调用 formData() 方法来获取键值映射,修改某些字段,然后将表单发送到服务器(或在本地使用)
它并没有参数,是这样子写的(示范):
const response = await fetch(url)
form_Data = await response.formData()
function formData(form_Data){
//做一些事情
}Body mixin 的 formData() 方法采取 Response 流并读取完成。它返回一个以 FormData 对象解决的 promise。
.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;其实,这个例子,懂得都懂(又可以手撸lazyload(?别埋坑吧)
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.arrayBuffer()获取音频文件song.ogg,然后在线播放的例子。
Fetch 读取后无法继续
没错,就是无法读取,你要不信你去试下
Stream对象只能读一次,也就是说,读一次,他就没了;也就是说,前面的什么.blob(), json(),只能在一个对象里面用一次,没事,response具有一个方法用来克隆对象:response.clone()
response.clone
let response1 = await fetch('1.jpg')
let response2 = response1.clone()
const Blob1 = await response1.blob();
const Blob2 = await response2.blob();
image1.src = URL.createObjectURL(Blob1);
image2.src = URL.createObjectURL(Blob2);response1.clone()复制了一份 Response 对象,然后将同一张图片读取了两次
response.redirect
好吧,也是一个和Service Worker有关的方法,它可以返回一个可以重定向到指定URL的Response,这里就简单讲讲吧,毕竟我不是很常用
// 语法
let response = Response.redirect(url, status);
// 示例
responseObj.redirect('<https://www.example.com>', 302);Response.bo
Response.body属性是 Response 对象暴露出的底层接口,返回一个 ReadableStream 对象,供用户操作。
它可以用来分块读取内容,应用之一就是显示下载的进度。
const response = await fetch('flower.jpg');
const reader = response.body.getReader();
while(true) {
const {done, value} = await reader.read();
if (done) {
break;
}
console.log(`Received ${value.length} bytes`)
}上面示例中,response.body.getReader()方法返回一个遍历器。这个遍历器的read()方法每次返回一个对象,表示本次读取的内容块。
这个对象的done属性是一个布尔值,用来判断有没有读完;value属性是一个 arrayBuffer 数组,表示内容块的内容,而value.length属性是当前块的大小。

https://gitee.com/wibus/blog-assets-goo/raw/master/asset-pic/20210718232928.png
optionObj — 第二參數
我们第一节知道了fetch()的第一个参数: url
,这是用来设置他的请求地址的,那么fetch还能够接受第二个参数,就是我们熟知的自定义HTTP请求(POST/GET这些的)

https://gitee.com/wibus/blog-assets-goo/raw/master/asset-pic/20210718235412.jpg
HTTP 请求的方法、标头、数据体都在这个对象里面设置,比如说
POST
let response = await fetch(url, {
method: 'POST',
headers: {
"Content-type": 'application/json;charset=utf-8'
},
body: 'foo=bar&lorem=ipsum',
})
let json = await response.json()以上使用了三个属性:
method:HTTP 请求的方法,POST、DELETE、PUT都在这个属性设置。headers:一个对象,用来定制 HTTP 请求的标头。body:POST 请求的数据体
有些标头不能通过headers属性设置,比如Content-Length、Cookie、Host等等。它们是由浏览器自动生成,无法修改。
POST 提交 JSON
const user = { name: 'John', surname: 'Smith' };
const response = await fetch('/article/fetch/post/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(user)
});POST 提交form
const form = document.querySelector('form');
const response = await fetch('/users', {
method: 'POST',
body: new FormData(form)
})POST 上传文件
如果表单里面有文件选择器,可以用前一个例子的写法,上传的文件包含在整个表单里面,一起提交。
另一种方法是用脚本添加文件,构造出一个表单,进行上传,请看下面的例子。
const input = document.querySelector('input[type="file"]');
const data = new FormData();
data.append('file', input.files[0]);
data.append('user', 'foo');
fetch('/avatars', {
method: 'POST',
body: data
});上传二进制文件时,不用修改标头的Content-Type,浏览器会自动设置。
optionObj 完整 API
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
});fetch()请求的底层用的是 Request() 对象的接口,参数完全一样,因此上面的 API 也是Request()的 API。
至于每个属性的解释我就不多讲了,因为我怕错误地引导大家导致大家也错了
取消 Fetch 请求
Fetch请求了,他也有一个对应的取消方式,那就是AbortController对象
let controller = new AbortController()
let signal = controller.signal; //AbortController接受发送的信号
fetch(url{
signal: controller.signal
})
signal.addEventListener('abort',
() => console.log("Fetch Abort")
)
controller.abort();我们要取消请求要做的几件事情:
- 新建一个AbortController实例
- 发送 fetch 请求
- 配置
signal属性为实例发送的信号(.signal) .abort()发出取消信号,可以使用监听来检测是否取消了请求、
我们可以这样子来实现一个1s后就取消请求
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;
}
}