dom-to-image遇到的坑

3,823 阅读4分钟

前言

项目中遇到一个需求,需要对页面进行截图,经过筛选选择了dom-to-image这个库,具体用法可以看GitHub,还是比较简单的,但是使用过程中发现了一个坑,图片的src属性不能有跨域问题

遇到的坑

因为我这个项目中都是截取一段uri地址然后最终赋值给img标签的src属性。例如这样

<img src="/uploaded/TB1qimQIpXXXXXbXFXXSutbFXXX.jpg" alt="" />

在vue中的话,如果没有给完整的url地址,调试的时候就会拼接本地起的服务的地址加img的src中的这段地址,这样子的话,我本地调试就会遇到的问题,使用dom-to-img截取出来的图片只要有img标签的地方,img都是黑色的,因为我的img最终会发送一个get请求去请求图片,然后最终图片请求的地址是这样的,即本地服务的地址 + img.src:http://localhost:8080/uploaded/TB1qimQIpXXXXXbXFXXSutbFXXX.jpg,但是该服务路径下,并没有图片,所以这个img标签截取出来的图片中就是黑色的,所以我就想把url地址补全,但是这样又会出现一个问题,就是dom-to-image调用toPng方法时会提示跨域问题,导致生成图片失败

解决办法

  1. 方法一,在赋值给img之前就是做一下base64的转换 使用完整的url地址赋值,但是如果这个url地址设置了跨域限制的话,到时候dom-to-image解决还是会报错的,所以需要把它编码成base64之后在将base64的地址赋值给img标签的src属性,如果没有做跨域限制的话,就不用转了,具体可以看这个demo,我这个demo其实不转也没有任何问题,因为没有做跨域限制
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/dom-to-image/2.6.0/dom-to-image.min.js" integrity="sha512-01CJ9/g7e8cUmY0DFTMcUw/ikS799FHiOA0eyHsUWfOetgbx/t6oV4otQ5zXKQyIrQGTHSmRVPIgrgLcZi/WMA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>
<body>
<div class="main">
    <img class="test" src="" alt="">
    <button class="toImg">toimg</button>
</div>
<script>
    const url = "https://img.alicdn.com/bao/uploaded/TB1qimQIpXXXXXbXFXXSutbFXXX.jpg";
    //const img = "http://127.0.0.1/base64/1.jpg";
    function getBase64Image(img) {
        const canvas = document.createElement("canvas");
        canvas.width = img.width;
        canvas.height = img.height;
        const ctx = canvas.getContext("2d");
        ctx.drawImage(img, 0, 0, img.width, img.height);
        const ext = img.src.substring(img.src.lastIndexOf(".")+1).toLowerCase();
        const dataURL = canvas.toDataURL("image/"+ext);
        return dataURL;
    }
    function base64ToImageUrl(imageDom) {
        // 创建一个图片,把url赋值给它
        let image = new Image();
        image.crossOrigin = '';
        image.src = url;
        image.onload = function(){
            // 该图片加载完之后
            // 将该图片转换为base64的格式,并赋值给imgDom
            let base64 = getBase64Image(image);
            imageDom.setAttribute("src", base64)
        }
    }
    const myImage = document.querySelector('.test')
    base64ToImageUrl(myImage)
    const btn =  document.querySelector('.toImg')
    btn.addEventListener('click', function() {
      const main = document.querySelector('.main')
      domtoimage.toPng(main).then(res => {
        console.log(res);
      })
    })
</script>
</body>
</html>

但是这样的话也会有一个严重的问题,因为所有的img的src的值都变成了base64,我们都知道base64的体积是比一个url的体积大非常多的,这样把值给后端存在数据是非常不合理的,所以该方法不推荐使用

  1. 方法二,在调用dom-to-image的时候将图片转为base64重新赋值给img标签 方法二的话我们还是赋值一段uri给img的src属性,然后在调用dom-to-image的时候之前写一个方法,将所有的img标签都做一下base64处理,这样处理的话只是在调用dom-to-image层面对图片进行一下处理,而不是先将url处理为base64,在赋值给图片的src,这样做能保证传给后端的值还是那段uri,但是这样子的话,就是调试的时候会有问题,toDataUrl提示crossOrigin跨域问题,dom-to-image的里面的img标签都是黑色的,但是部署上去的话,把publicPath设置为空,nginx做反向代理,toDataUrl是不会报错提示跨云问题,建议使用此方法
methods: {
  startScreenShot() {
    // change all image element to base64,then set base64 to img.src before generate image use dom-to-image
    const imageList = document.querySelectorAll("img");
    for (let i = 0; i < imageList.length; i++) {
      imageList[i].src = this.imgCompress(imageList[i]);
    }
    // start to screen shot use dom-to-image library
    const dom = document.querySelector(".dom-to-image");
    let base64Url = "";
    domToImage.toPng(dom).then((res) => {
      base64Url = res;
    });
  },
  // define a function to resolve image element to base64
  imgCompress(dom) {
    const canvas =  document.createElement("canvas")
    const context = canvas.getContext("2d")
    let originWidth = dom.width
    let originHeight = dom.height
    // define max width
    let maxWidth = 800
    let maxHeight = 0
    if (originWidth > originHeight) {
      maxHeight = 800 * originWidth / originHeight
    } else {
      maxWidth = originWidth
      maxHeight = originHeight
    }
    canvas.width = maxWidth
    canvas.height = maxHeight
    context.clearRect(0, 0, maxWidth, maxHeight)
    context.drawImage(dom, 0, 0, maxWidth, maxHeight)
    return canvas.toDataURL("image/jpeg", 0.8)
  },
},

补充案例

今天遇到项目更改,需要把嵌套的iframe标签页截取到图片当中,然后iframe的dom元素中也有img标签,我们都知道iframe只有在同源的前提下才能拿到它的dom,如果不是同源的情况下,去拿dom是会报错的,解决办法就是使用nginx进行代理,让iframe的src的链接和项目在统一域下面

遇到的坑

我自己本来想写一个demo,但是在写的过程中,遇到一个小坑,就是拿到iframe的标签然后进行页面重组,重组之后进行截图,但是生成的图片里面的img标签的图片出不来,总是一个块空白,我自己也将img的src转为了base64,后面才发现是截图的时机不对,因为我截图的方法是一个同步任务,然后将url改为base64是在img的onload事件里面执行的,是一个异步任务,这就会造成我截图的方法执行时,新的图片可能并没有加载完成,就会造成img显示未空白

解决办法

目前我自己的解决办法,就是拿到重组后的页面,然后把之前iframe里面所有的img标签拿到,用promise包一下放到一个promise的数组中,再用promiseAll做处理,这样就能保证所有的图片都加载完成了在执行domtoimage的toPng方法,就能看到图片了,下面是代码,所有的src引用都用了nginx做了代理,抹除了一切的跨域问题

<template>
  <div id="app">
    <div class="wrapper">
      <div>
        <img alt="Vue logo" src="./assets/logo.png" />
      </div>
      <div class="iframe">
        <--这里到时候放从iframe拿到的dom,使用prepend方法-->
        <iframe
          v-if="show"
          id="framename"
          src="http://localhost:8099/test.html"
        ></iframe>
      </div>
      <div class="test">1334</div>
      <div @click="imageGenerate">图片生成</div>
    </div>
  </div>
</template>

<script>
import domtoimage from "dom-to-image";
export default {
  name: "App",
  data() {
    return {
      show: true,
    };
  },
  mounted() {
    this.adjustIframe();
    // 这么写不生效
    // window.οnresize = () => {
    //   console.log("onresize")
    //   this.adjustIframe();
    // };
    window.addEventListener("resize", () => {
      this.adjustIframe();
    });
  },
  methods: {
    adjustIframe() {
      var ifm = document.getElementById("framename");
      if (ifm !== null) {
        ifm.height = document.documentElement.clientHeight;
        ifm.width = document.documentElement.clientWidth;
      }
    },
    async imageGenerate() {
      // 拿到嵌套的iframe标签里面html标签body下面的一个元素
      const iframeDom = document
        .getElementById("framename")
        .contentWindow.document.querySelector("body").firstElementChild;
      const iframeDomWrapper = document.querySelector(".iframe");
      iframeDomWrapper.prepend(iframeDom);
      const imageList = document.querySelectorAll("div.iframe img");
      // 定义一个promise数组,并保证图片onload完之后在resolve,确保图片已经加载完,防止截图的时候图片未加载完后产生空白
      let promiseAll = [];
      for (let i = 0; i < imageList.length; i++) {
        let url = imageList[i].src;
        let image = new Image();
        image.crossOrigin = "";
        image.src = url;
        promiseAll[i] = new Promise((resolve) => {
          image.onload = () => {
            // 该图片加载完之后
            // 将该图片转换为base64的格式,并重新赋值给图片
            let base64 = this.getBase64Image(image);
            imageList[i].setAttribute("src", base64);
            resolve();
          };
        });
      }
      // 隐藏iframe标签
      this.show = false;
      // 生成图片
      let dom = document.querySelector(".wrapper");
      await Promise.all(promiseAll);
      domtoimage.toPng(dom).then((res) => {
        console.log(res);
        const link = document.createElement("a");
        link.href = res;
        link.download = "test.jpeg";
        link.click();
      });
      // Promise.all(promiseAll).then(() => {
      //   domtoimage.toPng(dom).then((res) => {
      //     console.log(res);
      //     const link = document.createElement("a");
      //     link.href = res;
      //     link.download = "test.jpeg";
      //     link.click();
      //   });
      // });
    },
    getBase64Image(img) {
      const canvas = document.createElement("canvas");
      canvas.width = img.width;
      canvas.height = img.height;
      const ctx = canvas.getContext("2d");
      ctx.drawImage(img, 0, 0, img.width, img.height);
      const ext = img.src.substring(img.src.lastIndexOf(".") + 1).toLowerCase();
      const dataURL = canvas.toDataURL("image/" + ext);
      return dataURL;
    },
  },
  destroyed() {
    window.removeEventListener("resize", this.adjustIframe);
  },
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

生成图片之前的结构

image.png

生成图片之后的结构

image.png

生成的截图

image.png

参考