前言
在前端项目中,有大量的图片使用,分为以下几种
- 打包时,通过
url-loader
,过滤小图,以base64的形式打包进产物 - 打包时,通过
url-loader
,过滤大图,以静态资源的形式打包进产物目录下的某个文件夹内 - 后端通过
RESTful API
,返回二进制的图片数据 - 后端不通过
RESTful API
志杰返回图片,而是通过返回URL
,让前端去CDN服务
或者对象存储
或者后端的文件服务器
下载到本地
对于静态资源,我们可以通过nginx或者server设置强缓存或者协商缓存来实现避免重复的图片请求,在本文中不多赘述
图片接口
假设后端提供了一个这样的RESTful API接口
GET <https://domain/api/resource/v1/thumbnail?id=1&width=100&height=100> HTTP/1.1
content-type: pplication/json; charset=UTF-8
前端使用图片接口
前端一般的行为是将这个API写在img标签的src里
<img src="<https://domain/api/resource/v1/thumbnail?id=1&width=100&height=100>" alt="img">
那么就会出现一个问题,如果这个img被移除再创建的时候,这个请求就会重复的又发出一次
如何解决这个问题,请看下文
Blob
在此处不过多介绍blob的相关概念,有兴趣的可以查阅详细文档
blob有一个使用场景是,可以从互联网下载数据。那么如果我们将后端通过API返回的数据,储存在Blob对象里,比如
const downloadImg = (url, callback) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.responseType = "blob";
xhr.onload = () => {
callback(xhr.response);
};
xhr.send(null);
};
当然,使用fetch
或者axios
也是可以实现以流的方式获取二进制数据
我们在callback内打印以下xhr返回的response
const callback = (response)=>{
const blob = response
console.info(blob)
}
打印的日志如下
那么,如果我们将Blob对象和img标签建立联系呢?
Blob URL
在此处简单的介绍Blob URL的相关概念,有兴趣的可以查阅详细文档
Blob URL/Object URL 是一种伪协议,允许 Blob 和 File 对象用作图像,下载二进制数据链接等的 URL 源。在浏览器中,我们使用 URL.createObjectURL
方法来创建 Blob URL,该方法接收一个 Blob
对象,并为其创建一个唯一的 URL,其形式为 blob:<origin>/<uuid>
,对应的示例如下:
blob:<https://example.org/40a5fb5a-d56d-4a33-b4e2-0acf6a8e5f641>
URL.createObjectURL()
静态方法会创建一个 DOMString
,其中包含一个表示参数中给出的对象的URL。这个 URL 的生命周期和创建它的窗口中的 document
绑定。这个新的URL 对象表示指定的 File
对象或 Blob
对象。
但是,需要注意的是,在每次调用 createObjectURL()
方法时,都会创建一个新的 URL 对象,即使你已经用相同的对象作为参数创建过。当不再需要这些 URL 对象时,每个对象必须通过调用 URL.revokeObjectURL()
方法来释放。
接下来,我们来看一下通过Blob URL将Blob对象用作图片
const downloadImg = (url, callback) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.responseType = "blob";
xhr.onload = () => {
callback(xhr.response);
};
xhr.send(null);
};
const callback = (response) => {
const blob = response;
const img = document.createElement("img");
img.addEventListener("load", () => {
window.URL.revokeObjectURL(that.$refs.img.src);
});
img.src = window.URL.createObjectURL(blob);
document.body.appendChild(img);
};
downloadImg("<https://cn.vuejs.org/images/logo.svg>",callback)
可以看到第一个请求为实际的图片请求,第二个请求为Blob URL
以下是DOM中的情况
避免重复的图片请求
避免重复的请求需要按以下逻辑进行
- 第一次请求后,拿到数据,将数据作为保存Blob对象保存,并储存在内存中,可使用map或对象,通过图片id作为键
- 构建
Blob URL
,用于img的src - 建立一个flag,用于是否需要重复请求。如果为true,则将API用于img的src;如果为false,则不重新调用接口,对从内存中找到
Blob
对象,再次构建Blob URL
,用于img的src - 当img被删除,再创建时,进行对flag的判断,并按3的逻辑执行
以下是代码示例,为了避免大量操作dom,希望专注把逻辑放在业务上,于是使用了petite-vue
,业务逻辑与框架无关,使用任何框架均可实现。通过setSrc
,downloadImg
,downloadImgCallback
,renderImg
这几个方法,完成了多次重新创建img标签时,避免重复图片请求的流程。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<script type="module">
import { createApp } from "<https://unpkg.com/petite-vue?module>";
createApp({
isShow: false,
imgMap: { idOne: { blob: null }, idTwo: { blob: null } },
getImgUrl(id) {
return id === "idOne"
? "<https://cn.vuejs.org/images/tidelift.png>"
: "<https://cn.vuejs.org/images/neds.png>";
},
async setSrc(id) {
this.isShow = !this.isShow;
if (!this.isShow) return;
await this.$nextTick();
// 创建flag,判断需要使用api还是使用blob url
const isUseBlobUrl = this.imgMap[id].blob == null;
// 兼容ie
window.URL = window.URL || window.webkitURL;
// 判断浏览器兼容性,框架可使用@babel/preset-env,帮助我们转换成代码的目标运行环境支持的语法
if (typeof history.pushState == "function") {
// 判断需要使用api还是使用blob url
if (isUseBlobUrl) {
// 获取需要加载的图片的url
const url = this.getImgUrl(id);
// 下载图片数据,通过第二个参数callback,创建Blob URL,进行图片的渲染
this.downloadImg(url, this.downloadImgCallback.bind(this, id));
} else {
// 直接使用内存中的Blob对象,创建Blob URL,进行图片的渲染
this.renderImg(this.imgMap[id].blob);
}
}
},
//下载图片数据,并转换成Blob对象
downloadImg(url, callback) {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.responseType = "blob";
xhr.onload = () => {
callback(xhr.response);
};
xhr.send(null);
},
// 使用Blob URL,渲染图片
renderImg(blob) {
this.$refs.img.addEventListener("load", () => {
window.URL.revokeObjectURL(this.$refs.img.src);
});
this.$refs.img.src = window.URL.createObjectURL(blob);
},
downloadImgCallback(id, response) {
const blob = response;
this.renderImg(blob);
this.imgMap[id].blob = blob;
},
}).mount();
</script>
<div>
<div>
<img src="" alt="" ref="img" class="img" v-if="isShow" />
</div>
<div>
<button @click="setSrc('idOne')">show/hidden 图片1</button>
<button @click="setSrc('idTwo')">show/hidden 图片2</button>
</div>
</div>
</html>