更好的请求方式 — 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-8
Fetch 读取
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
属性是当前块的大小。
optionObj — 第二參數
我们第一节知道了fetch()的第一个参数: url
,这是用来设置他的请求地址的,那么fetch还能够接受第二个参数,就是我们熟知的自定义HTTP请求(POST/GET这些的)
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;
}
}