谈一谈 Fetch API 中的 “res.blob()”
这篇文章主要回答了三个问题:
- 如何处理 Fecth API 从后端获取的字节数据?
- URL.createObjectURL() 做了什么?
- “res.blob()” 做了什么?
当我们使用 Fetch API 获取到后端返回的字节流一般都会通过 “res.blob()” 转换成 blob 对象再进一步处理( Fetch API ),那么问题来了 --- ”res.blob()” 又做了什么?
至于为什么想谈这个,是因为之前小伙伴问我:怎么下载后端返回的 zip 文件?所以就想写篇文章来谈一谈这里涉及到的知识点,其中最关键的就是 “res.blob()”。
处理 Fecth API 从后端获取的字节数据
先回答下小伙伴提的问题:怎么下载后端返回的 zip 文件?
这个问题还是比较好解决的,从后端返回的字节数据,都可以通过调用 response 对象的 blob() 方法来将它转换成返回 blob 的 Promise。
那 blob 对象又是什么?我们可以把它当作原始数据对象,它保存着从后端返回的原始数据以及相关信息(比如字节数以及类型)。我们可以通过它获得 Base64 URL 或者 blob URL。然后通过 a 标签的 download 属性来设置文件名,将获得的 URL 赋给 a 标签的 href 属性就大功告成了,当然别忘了调用 click() 开始下载。
示例代码如下:
fetch('example.zip')
.then(res => res.blob())
.then(blob => {
// 通过 blob 对象获取对应的 url
const url = URL.createObjectURL(blob)
let a = document.createElement('a')
a.download = 'example.zip'
a.href = url
document.body.appendChild(a)
a.click()
a.remove() // document.body.removeChild(a)
.catch(err => {
console.error(err)
URL.createObjectURL() 做了什么?
问题看起来是解决了,但是有人可能会问:URL.createObjectURL() 方法到底做了什么,为什么我们能通过它来获取到文件下载的地址?
这里简单说一下,每次调用 URL.createObjectURL() 方法都会生成一个地址,这个地址代表着根据 blob 对象生成的资源入口,而这个资源入口存放于浏览器维护的一个 blob URL store 中。生成的 URL 由四部分组成:
- ‘blob: ’
- origin
- ‘/’
- UUID
生成的 URL 大概都长这样:
blob:https://www.example.com/7914f88e-cac5-4af5-96a1-287584d518b5
这里要注意两个地方:
- 即使是同一个 blob 对象,每次调用都会生成不同的 URL。
- URL 不再用的时候,需要调用 URL.revokeObjectURL() 来撤销这个入口。(如果不撤销,URL 会一直被保留,直到当前 document 对象 unload)
“res.blob()”
剩下的,就是这篇文章的主角了,点题的一节 — “res.blob()”。
对于 res.blob(),我最想知道的是它到底做了什么?这一节就来谈一谈。
res.blob() 到底做了什么?
每次调用 res.blob() 方法都会执行 “consume body” 动作,“consume body” 的流程大概是这样的:
- 获取字节流的读取器
- 通过读取器读取所有的数据
- 把数据包装成 blob 对象并返回
既然 res.blob() 可以处理字节流,那么我们能不能自己去处理后端返回的字节流,毕竟我们有时候也会遇到这种情况吧(没有的事,吃饱了撑着)。
接下来就直接放代码,模拟 res.blob(),这里需要注意的是兼容性,可以参考: Can I use... Support tables for HTML5, CSS3, etc
fetch('example.zip')
.then((res) => {
// 获取读取器
const reader = res.body.getReader()
const type = res.headers.get('Content-Type')
const data = []
return new Promise((resolve) => {
// 读取所有数据
function push() {
reader.read().then(({done, value}) => {
data.push(value)
if (done) {
// 包装成 blob 对象并返回
resolve(new Blob(data, { type }))
} else {
push()
push()
.then(blob => {
const url = URL.createObjectURL(blob)
let a = document.createElement('a')
a.download = 'example.zip'