html2canvas 处理跨域图片的解决方案

最近接了个开发需求,要在前端实现将页面上的部分 DOM 内容生成为一张图片的功能。调研后发现了 html2canvas 库,使用它可以非常简便的实现上述功能,它的基本原理是将要生成为图片的 DOM 进行解析,然后将其在 canvas 上进行重绘,最后使用 canvas 的 toDataURL 方法导出为图片。但在使用 html2canvas 的过程中遇到了一个图片资源跨域的问题,复现如下:


当在页面中使用 标签或者 CSS 背景引入同源的图片时,

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <link rel="stylesheet" href="./style.css" />
  </head>
  <body>
    <div class="container">
      <h3>原 DOM 内容:</h3>
      <!-- div#proto-dom 是要生成为图片的 DOM -->
      <div class="wrap" id="proto-dom">
        <p>HTML img 标签引入:</p>
        <!-- img标签引入同源图片 -->
        <img class="img-import" src="./demo.png" />
        <p>CSS background-image 属性引入:</p>
        <!-- CSS背景引入同源图片 -->
        <div class="bg-import" style="background-image: url(./demo.png);"></div>
      </div>
    </div>
    <div class="container">
      <h3>html2canvas 生成的图片:</h3>
      <img id="gener-image" />
    </div>
    <script src="./node_modules/html2canvas/dist/html2canvas.js"></script>
    <script>
      window.onload = function () {
        html2canvas(document.getElementById("proto-dom")).then((canvas) => {
          document.getElementById("gener-image").src = canvas.toDataURL(
            "image/png"
          );
        });
      };
    </script>
  </body>
</html>

html2canvas 可以把包括图片在内的 DOM 完整解析生成为图片:
在这里插入图片描述

但当 img 标签及 CSS 背景引入的是跨域的图片时,

<!-- ... -->
		<!-- 当前域http://foo.xxx:5000 -->
    <div class="container">
      <h3>原 DOM 内容:</h3>
      <!-- div#proto-dom 是要生成为图片的 DOM -->
      <div class="wrap" id="proto-dom">
        <p>HTML img 标签引入:</p>
        <!-- img标签引入跨域图片 -->
        <img class="img-import" src="//bar.xxx:5001/demo.png" />
        <p>CSS background-image 属性引入:</p>
        <!-- CSS背景引入跨域图片 -->
        <div
          class="bg-import"
          style="background-image: url(//bar.xxx:5001/demo.png);"
        ></div>
      </div>
    </div>
<!-- ... -->

html2canvas 最后解析生成的图片会缺失引入的跨域图片:
在这里插入图片描述
从以往的开发经验认知,不论是 标签里的图片还是 CSS 背景里的图片,浏览器都是允许从任意源引入而不会做跨域限制的,但为什么要把它们引入到 canvas 并导出为图片时,就不行了呢?这得从同源策略说起。

说到同源策略(Same-origin policy, SOP),每位程序员都不会陌生。它是一种重要的浏览器安全机制,规定了一个源(origin,也可称为域)的 HTML 文档或它加载的脚本如何能与另一个源的资源进行交互。它可以防止某个网页上的恶意脚本通过该页面的文档对象模型访问另一网页上的敏感数据。

一个源访问另一个源的资源又称为跨域网络访问(Cross-origin network access),同源策略将跨域网络访问分为了三种场景:

  1. 跨域写操作(Cross-origin writes)。常见的有表单提交、使用链接(links)访问其他域的网页等。 跨域写操作默认是被允许的。
  2. 跨域资源嵌入(Cross-origin embedding)。使用 link script img iframe 等标签对其他域的资源进行引用就属于这用情况。跨域资源嵌入默认也是被允许的。
  3. 跨域读操作(Cross-origin reads)。最常见的就是发起访问其他域的 Ajax 请求。跨域读操作默认情况下是不被允许的。

根据这个分类可以解释上文示例中的问题。在使用 img 标签和 CSS 背景引用其他域的图片时,是跨域资源嵌入的场景,此时浏览器是默认允许的,图片也能在浏览器上正常显示;但是在使用 canvas 读取其他域的图片时,场景就变为跨域读操作了,在这种情况下浏览器就默认不允许访问了。如果 canvas 引入了其他域的图片,那么浏览器就认为这个 canvas “被污染(tainted)”了,从而会在调用 canvas 的 toDataURL() 等方法时抛出安全错误。

浏览器这么做的目的是为了预防一些恶意行为,但是合理的跨域网络访问对于互联网应用来说也是很重要的。上文中的示例就是一种常见的跨域情况,大部分网站都会把图片等静态资源放在 CDN 上以优化网站的访问速度。
根据上面的分析,可以得到解决图片不显示问题的两个思路:一是将图片“跨域”变“同域”;另一个是合理配置 CORS(跨域资源共享,Cross-Origin Resource Sharing) 使得跨域图片能够被 canvas 正常访问。

有两种方式让图片“跨域”变“同域”。一是配置反向代理,将图片资源请求转发,html2canvas 提供了一个代理库来支持这种方式。二是使用对图片进行 base64 编码,并写入 HTML 或 CSS 代码中,这样做的一个缺点是 base64 的编码会导致 HTML 和 CSS 文件体积大大增加且可读性降低。

使用 base64 引入图片示例如下。

<!-- ... -->
    <div class="container">
      <h3>原 DOM 内容:</h3>
      <!-- div#proto-dom 是要生成为图片的 DOM -->
      <div class="wrap" id="proto-dom">
        <p>HTML img 标签引入:</p>
        <!-- img标签引入跨域图片 -->
        <img
          class="img-import"
          src="data:image/png;base64,****大量的眼花缭乱的base64码****"
        />
        <p>CSS background-image 属性引入:</p>
        <!-- CSS背景引入跨域图片 -->
        <div
          class="bg-import"
          style="
            background-image: url(data:image/png;base64,****大量的眼花缭乱的base64码****);
          "
        ></div>
      </div>
    </div>
<!-- ... -->

在这里插入图片描述
base64 编码的解决方案并非万能,如果 canvas 要读取的图片不是固定的,而是变化的,比如用户的头像,base64的方案就无能为力了。


如果不想使用“同域”的两种解决方案,可以使用配置 CORS 的方式。这种方案的原理和 Ajax 的 CORS 方案的原理基本一致,都是添加与 CORS 相关的 HTTP 头信息让浏览器与服务器进行沟通,来决定请求是否成功。在发送图片获取请求时,给请求头增加一个 Origin 头信息 Origin: http://a.xxx:5000 ,服务器解析并判断该头部,如果认为这个源可以接受,就在响应头中增加 Access-Control-Allow-Origin 头信息,并且其值与 Origin 中的相同: Access-Control-Allow-Origin: http://a.xxx:5000 ,浏览器解析响应头,如果有这个头信息并且其值匹配,浏览器就会接受这个响应,canvas 也能正常处理这张图片了。另外还有一种更“省事”的方式,就是后端配置响应头 Access-Control-Allow-Origin 信息的值为通配符: Access-Control-Allow-Origin: * ,此时也不需要请求头增加 Origin 的信息了,但这么做存在一定的安全风险,会导致任何域都可以读取该图片资源,要确保这个图片是可以被任意域读取的,否则会造成信息泄露。

从这里可以了解到,前端是禁止通过直接编码的方式给请求头增加 Origin 的,那么如何给图片请求增加 Origin 头信息呢?答案是对 img 元素增加 crossorigin 属性。如果请求不需要携带 cookie,则给 img 元素直接增加该属性或者给该属性设置值为 anonymous 即可: img标签添加 crossorigin ,如需要携带 cookie 则设置该属性值为 use-credentials。html2canvas 库内部就是通过上述方式实现跨域图片请求的,而使用者只需要将 html2canvas 的配置项 useCORS 值为 true 即可。

使用 CORS 的方式引入图片示例如下,

<!-- ... -->
    <div class="container">
      <h3>原 DOM 内容:</h3>
      <!-- div#proto-dom 是要生成为图片的 DOM -->
      <div class="wrap" id="proto-dom">
        <p>HTML img 标签引入:</p>
        <!-- img标签引入跨域图片 -->
        <img class="img-import" src="//bar.xxx:5001/demo.png" />
        <p>CSS background-image 属性引入:</p>
        <!-- CSS背景引入跨域图片 -->
        <div
          class="bg-import"
          style="background-image: url(//bar.xxx:5001/demo.png);"
        ></div>
      </div>
    </div>
    <div class="container">
      <h3>html2canvas 生成的图片:</h3>
      <img id="gener-image" />
    </div>
    <script src="./node_modules/html2canvas/dist/html2canvas.js"></script>
    <script>
      window.onload = function () {
        html2canvas(document.getElementById("proto-dom"), {
          useCORS: true, // 配置 useCORS 为 true,后端也需要做 CORS 对应配置
        }).then((canvas) => {
          document.getElementById("gener-image").src = canvas.toDataURL(
            "image/png"
          );
        });
      };
    </script>
<!-- ... -->

响应头为 Access-Control-Allow-Origin: * 时:
在这里插入图片描述
响应头为 Access-Control-Allow-Origin: http://a.xxx:5000 时:
在这里插入图片描述


最后对各解决方案及其优缺点进行总结:

在这里插入图片描述

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐