背景

html2canvas 是一个 HTML 渲染器。该脚本允许你直接在用户浏览器截取页面或部分网页的“屏幕截屏”,屏幕截图是基于 DOM,因此生成的图片并不一定 100% 一致,因为它没有制作实际的屏幕截图,而是根据页面上可用的信息构建屏幕截图。

由于整个绘制过程时在客户端完成的,所以客户端的一些限制也同样应用于 html2canvas,对图片跨域问题也归咎于 canvas 对图片的跨域问题,在解决问题之前,我们先来了解一些基本知识

img 标签的 crossorigin 属性

crossorigin 属性标记浏览器使用跨域请求图片资源,需要服务器配合使用,就可以在 canvas 里使用该图片,crossorigin 可选值有两个

  • crossorigin="anonymous"

当配置为 anonymous 值时,需要在返回的图片响应头设置 Access-Control-Allow-Origin* ,否则会报以下错误

Access to img at 'xxx.jpg' from origin 'null' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
  • crossorigin="use-credentials"

当配置为 use-credentials 值时,响应头 Access-Control-Allow-Origin 则不能设置为 * ,必须指定白名单,否则会报以下错误

The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*'
when the request's credentials mode is 'include'.

在 canvas 使用到图片的场景下,如果从外部引入的 <img /><svg> ,图像源不符合跨域规则,将会被阻止从 <canvas> 中读取数据。当调用 getImageData()toBlob()toDataURL(),则会报以下错误,以 getImageData 为例

Uncaught SecurityError: Failed to execute 'getImageData' on 'CanvasRenderingContext2D':
The canvas has been tainted by cross-origin data.
at https://cdpn.io/cp/internal/boomboom/pen.js?key=pen.js-85eaccd5-18d4-330a-d25b-20997e8ad314:12

解决方法也很简单,配置 corssorigin 属性就可以了

// 加入这行代码即可成功调用 getImageData() 方法
img.setAttribute("crossorigin", "anonymous");
//or
img.crossOrigin = "Anonymous";

代理转发

如果是自己的图片服务器,那么该方法就可以解决 canvas 的图片跨域问题,但是在日常开发难免会使用第三方的图片,无法在图片响应头配置跨域字段,可以通过代理来实现。

这里我们使用一个第三方的 nodejs 服务,html2canvas-proxy-nodejs ,源码很简单

app.get("/", cors(), validUrl, (req, res, next) => {
switch (req.query.responseType) {
case "blob":
req.pipe(request(req.query.url).on("error", next)).pipe(res);
break;
case "text":
default:
request({ url: req.query.url, encoding: "binary" }, (error, response, body) => {
if (error) {
return next(error);
}
res.send(`data:${response.headers["content-type"]};base64,${Buffer.from(body, "binary").toString("base64")}`);
});
}
});

原理是服务端无跨域限制,通过 request 库请求图片资源,然后再转为 base64 后,发送给客户端,同时通过 cors 中间件设置了跨域的响应头

最后在 html2canvas 配置 proxy 选项即可

html2canvas(document.body, {
proxy: "https://www.html2canvas-server.com", // 替换成 html2canvas-proxy 服务的域名即可
}).then(function (canvas) {
document.body.appendChild(canvas);
});