如何利用blob,避免重复的图片请求

3,765 阅读3分钟

前言

在前端项目中,有大量的图片使用,分为以下几种

  1. 打包时,通过url-loader,过滤小图,以base64的形式打包进产物
  2. 打包时,通过url-loader,过滤大图,以静态资源的形式打包进产物目录下的某个文件夹内
  3. 后端通过RESTful API,返回二进制的图片数据
  4. 后端不通过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)
}

打印的日志如下

image.png

那么,如果我们将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 image.png

以下是DOM中的情况 image.png

避免重复的图片请求

避免重复的请求需要按以下逻辑进行

  1. 第一次请求后,拿到数据,将数据作为保存Blob对象保存,并储存在内存中,可使用map或对象,通过图片id作为键
  2. 构建Blob URL,用于img的src
  3. 建立一个flag,用于是否需要重复请求。如果为true,则将API用于img的src;如果为false,则不重新调用接口,对从内存中找到Blob对象,再次构建Blob URL,用于img的src
  4. 当img被删除,再创建时,进行对flag的判断,并按3的逻辑执行

以下是代码示例,为了避免大量操作dom,希望专注把逻辑放在业务上,于是使用了petite-vue ,业务逻辑与框架无关,使用任何框架均可实现。通过setSrcdownloadImgdownloadImgCallbackrenderImg这几个方法,完成了多次重新创建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>