Fetch API
Fetch API 是目前最新的异步请求解决方案,它在功能上与 XMLHttpRequest(XHR)类似,都是从服务器端异步获取数据或者资源的方法。 对于有过 AJAX 开发经验的读者应该深有体会,基于 XHR 的异步请求方法在实现上比较复杂。 下面简单演示如何通过 XHR 发送异步请求:
// 实例化 XMLHttpRequest
let xhr = new XMLHttpRequest()
// 定义加载完成回调函数,打印结果
xhr.onload = function(){
console.log("请求成功");
}
// 定义加载出错时的回调函数,打印错误
xhr.onerror = function(err){
console.error("请求失败");
}
// 设置请求目标
xhr.open("GET","/path/to/text", true)
// 开始发起请求
xhr.send()
从上面的代码当中可以感受到,基于事件回调机制的 XHR 在编程实现的思路上非常反思维, 要实现这样一个简单的 GET 请求所需代码较多,一旦功能变得负责会很容易造成混乱。 因此在实际应用当中,一般会选择封装好的函数进行使用, 例如 jQuery 提供的 $.ajax
方法。
接下来使用 Fetch API 来实现上述功能:
fetch("/path/to/text", { method: "GET" })
.then((response) => {
console.log("请求成功");
})
.catch((err) => {
console.log("请求失败");
});
Fetch API 代码逻辑清晰,代码量少。
1. fetch()
Fetch API 提供了 fetch()
用来发起网络请求并获得资源响应。它的使用方法非常简单:
fetch(request).then(response=>{/* 响应结果处理 */})
接受一个 Request 对象作为参数,fetch()
发起网络请求,由于网络请求是一个异步过程,因此 fetch()
返回一个 Promise 对象,当请求响应时 Promise 执行 resolve 并传回一个 Response 对象。
除了直接以 Request 对象作为参数之外, fetch()
还支持传入请求 URL 和请求配置项的方式, fetch()
会自动根据这些参数实例化 Request 对象之后再去发起请求,因此以下代码是等价的:
fetch(new Request('/path/to/resource',{ method: "GET" }))
// 等价于
fetch('/path/to/resource',{ method: "GET" })
需要注意的是,fetch()
只有在网络请求终端的时候才会抛出异常,此时 Promise 对象会执行 reject 并返回错误信息。 因此对于 fetch()
来说,服务端返回的 HTTP 404、500 等状态码并不认为是网络错误,因此处理检查 Promise 是否 resolve 之外,还需要检查 Response.status、Response.ok 等对象属性以确保请求是否成功响应。 示例:
fetch('/paht/to/resource/').then(response => {
if (response.status === 200){
// 请求成功
}else{
// 请求失败
}
}).catch(err => {
// 网络请求失败或者请求被中断
})
2. Request
Request 是一个用于描述资源请求的类,通过 Request()
构造函数,可以实例化一个 Request 对象:
let request = new Request(input,init)
其中,input
代表想要请求的资源,可以是 URL, 或者是描述资源请求的 Request 对象;init
为可选参数,可以用来定义请求中的其他选项。 可以注意到,Request
构造函数所需要的参数和fetch()
方法的参数是一样的。 示例:
-
GET 请求, 请求参数需要写到 URL 中:
let getRequest = new Request("/api/hello?name=lilei", { method: "GET", });
-
POST 请求, 请求参数需要写到 body 中:
let postRequest = new Request("/api/hello", { method: "POST", body: JSON.stringify({ name: "lilei", }), });
-
自定义请求的 Headers 信息
let customeRequest = new Request("/api/hello", { headers: new Headers({ "Content-Type": "text/plain", }), });
-
设置发起资源请求时带上 cookie
let cookieRequest = new Request('/api/hello',{ credentials:'include' })
init 对象还可配置其他参数,这里不展开讲,介绍以下 Request 对象常用的几个属性:
-
url: String 类型,只读,请求的url;
-
method: String 类型,只读,请求的方法;
-
headers: Headers 类型,只读,请求的头部信息,可通过
get()
方法获取信息,例如:if(request.headers.get('Content-Type') === 'text/html'){ // ... }
3. Response
Response 类,用于描述请求响应数据,通过 Response()
构造函数实例化,
let response = new Response(body,init)
其中 body 参数代表请求响应的资源内容,可以是字符串,FormData, Blob 等等; init为可选参数对象,可以用来设置响应的 status、statusText、headers 等内容,示例:
// 如何构造一个 iudex.js 的响应:
let jsResponse = new Response(
// index.js 的内容
'console.log("Hello World!)',
{
status: 200,
headers: new Headers({
"Content-Type": "application/x-javascript",
}),
},
);
在实际应用当中,我们一般会通过 fetch()
、Cache API 等等获得请求响应对象,然后再对响应对象进行操作。
3.1 读取响应体
Response 的 body 属性 暴露了一个 ReadableStream 类型的响应体内容,Response 提供了一些方法来读取响应体:
text()
: 解析为字符串;json()
: 解析为 JSON 对象;blob()
: 解析为 Blob 对象;formData()
: 解析为 FormData 对象;arrayBuffer()
: 解析为 ArrayBauffer 对象;
这些方法读取并解析响应体的数据流属于异步操作,因此这些方法均返回 Promise 对象,当读取数据流并解析完成时, Promise 对象将 resolve 并同时返回解析好的结果。 示例:
// 沟槽 Response 对象
let response = new Response(JSON.stringify({ name: "lilei" }));
// 通过 response.json() 读取请求体
response.json().then((data) => {
console.log(data.nam); //lilei
});
由于Response 的响应体是以数据流的形式存在的,因此只允许进行一次读取操作。 通过检查 bodyUsed 属性可以知道 当前的 Response 对象是否已经被读取:
let response = new Response(JSON.stringify({ name: "lilei" }));
console.log(response.bodyUsed); //false
response.json().then((data) => {
console.log(response.bodyUsed);// true
});
由于二次读取响应体内容会导致报错,因此为了保险起见,可以在进行响应体读取前首先判断 bodyUsed
属性再决定下一步操作。
3.2 拷贝 Response
Response 提供了 clone()
方法来实现对 Response 对象的拷贝:
let clonedResponse = response.clone()
clone()
是一个同步方法,克隆得到的 新对象再所有方面与原对象都是相同的。 在这里需要注意的是,如果 Response 对象的响应体已经被读取, 那么在调用 clone()
方法的时候会报错,因此需要在读取响应体读取之前进行克隆操作。
4. Fetch API 处理跨域请求
当涉及到前后端通信问题的时候,就不得不提请求跨域的情况。由于受到 Web 同源策略的影响,在使用 Fetch API 默认配置情况下发送异步请求,会受到跨域访问限制而导致资源请求失败。
我们通常采用跨域资源共享机制(CORS)来解决这个问题。在跨域服务端支持 CORS 的前提下,通过将 fetch()
的请求模式设置为“cors”,就可以简单地实现跨域请求。在这种请求模式下,返回的请求响应是完全可访问的:
// 假设当前页面 URL 为 https://current.com
fetch('https://other.com/data.json', {
mode: 'cors'
})
.then(response => {
console.log(response.status) // 200
console.log(response.type) // 'cors'
console.log(response.bodyUsed) // false
return response.json()
})
.then(data => {
console.log(data.name) // 'lilei'
})
⭐ 对于图片、JS、CSS 等等这些类型的静态资源,如果通过对应的 HTML 标签加载这类跨域资源,是不会受到同源策略限制的,因此一般来说,存放静态资源的服务器并不需要设置 CORS。这就会对 Fetch API 请求这类静态资源带来影响。在默认情况下 fetch()
的请求模式为“no-cors”,在这种模式下请求跨域资源并不会报错,但是返回的 Response 对象将变得不透明,type 属性将变成“opaque”,无论服务端所返回的真实 status 是多少,在这种情况下都会变成 0,其他属性也都无法正常访问:
// 假设当前页面 URL 为 https://current.com
fetch('https://other.com/data.json', {
mode: 'no-cors'
})
.then(response => {
console.log(response.status) // 0
console.log(response.type) // 'opaque'
console.log(response.headers) // Headers {}
console.log(response.body) // null
})
此时唯一能正常工作的方法是 clone(),即实现对 Response 对象的拷贝,当然拷贝得到的新对象也同样是不透明的。这种模式比较适用于在 Service Worker 线程中拦截静态资源请求并复制一份缓存到本地,只要将这类不透明的请求响应返回主线程,依然是能够正常工作的。下面的代码演示了 Service Worker 拦截跨域图片资源并将资源缓存到本地,然后在 fetch()
出错的时候再从缓存中读取资源:
self.addEventListener("fetch", (event) => {
// 判断当前拦截到的请求为跨域图片资源
if (event.request.url === "https://other-site.com/pic.jpg") {
event.respondWith(
// 优先发送网络请求获取最新的资源
fetch(event.request.url, { mode: "no-cors" })
.then((response) => {
// 将请求得到的响应进行缓存
// 此时缓存的资源是不透明的
const cloneReq = response.clone();
caches
.open("cache-storage")
.then((cache) => cache.put(event.request.url, cloneReq));
// 返回请求响应结果
return response;
})
.catch(
// 请求失败时在使用缓存资源进行兜底
() =>
caches
.open("cache-storage")
.then((cache) => cache.match(event.request.url))
.then((e) => e),
),
);
}
});
@jayce 原文这里写的有点问题,
cache.match(event.request.url)
返回的不是 Response 对象,而是一个 Promise ,所以要进一步处理。加一个.then()
在这种情况下,图片资源的 Response 对象是不透明的,因此整个操作过程无法对图片资源响应做任何检查判断,只能直存直取。这就有可能将真实状态码为 404、500 等错误响应给缓存下来,因此在“no-cors”模式下缓存的跨域资源的可信度不高,最好作为各类请求策略的兜底资源进行使用。
上面这段代码,具体在做什么?
实际上它希望,我先拦截三方站点的 图片 请求,然后尝试去请求,然后缓存下来。但是,如果请求不到, fetch()
会报错,这时候 被 catch
到后,会从先前的缓存中去取出缓存。 注意,缓存的是一个 Response 对象,所以在取得时候直接返回对应的 Response 对象即可。
为了更加清楚以上的原理,下面用一个实例来说明。
关于Service Worker 的内容,后面有介绍,这里提前用一下,为了说明这个地方在做什么,为什么要这么做,有什么效果。
有这样一个页面,
<img src="http://placeimg.com/640/480/nature" alt="" />
为了直观的观察,这个 API 也有特殊的地方,每次刷新,都会去加载不同的图片:
这个API 是一个三方站点的 API, 它并不会触发跨域。 但是会被下面的 Service Worker 拦截:
self.addEventListener("fetch", (event) => {
// 判断当前拦截到的请求为跨域图片资源
if (event.request.url === "http://placeimg.com/640/480/nature") {
event.respondWith(
// 优先发送网络请求获取最新的资源
fetch(event.request.url, { mode: "no-cors" })
.then((response) => {
throw Error()
// 将请求得到的响应进行缓存
// 此时缓存的资源是不透明的
const cloneReq = response.clone();
caches
.open("cache-storage")
.then((cache) => cache.put(event.request.url, cloneReq));
// 返回请求响应结果
return response;
})
.catch(
// 请求失败时在使用缓存资源进行兜底
() =>
caches
.open("cache-storage")
.then((cache) => cache.match(event.request.url))
.then((e) => e),
),
);
}
});
注意,为了触发 catch
中的方法,这里刻意通过 throw Error()
抛出了异常,
可以看到,现在,请求被拦截并且由错误处理,返回了缓存中的资源。