前言
工作中遇到一个需求,要将页面截图保存。一番调研后使用了 html2canvas 这个库,在实际使用时发现 Mapbox 渲染的地图一直是空白的,没有被渲染上。尝试了 modern-screenshot 和 dom-to-image 这两个库,还是同样的问题,猜测原因应该在地图本身的渲染上
排查过程
排查首先肯定是看库本身是否有反馈有效信息。在 Devtools 的控制台发现这样一条 warn 信息:

简单来说就是 canvas 无法被克隆的原因是其上的 WebGL conntext 的 preserveDrawingBuffer 为 false。那个这个 WebGL conntext 和 preserveDrawingBuffer 又是什么呢?
工作中使用 canvas 时往往有这样一套起手式:
tsx
const ctx = canvas.getContext("2d");
// do something...
实际上 getContext 这个方法的定义是这样的:
tsx
getContext(contextId, options)
第一个参数 contextId 除了 2d 外,还可以传入 webgl 、webgl2 、bitmaprenderer、webgpu这 4 种。根据 contextId ,第二个参数 options 也不同,而 preserveDrawingBuffer 就是 webgl 的参数之一。MDN 的介绍是 "如果这个值为 true 缓冲区将不会被清除,会保存下来,直到被清除或被使用者覆盖。"在 WebGL 中,默认情况下,绘制缓冲会在每一帧被清除,以便进行下一次绘制。通过将 preserveDrawingBuffer 选项设置为 true,创建了一个带有保留绘图缓冲功能的 WebGL 上下文。在此之后继续进行其他的 WebGL 操作,而绘图缓冲的内容将会保留,直到手动清除它或下一次绘制发生
了解 WebGL conntext 和 preserveDrawingBuffer 是什么后,到 html2canvas 源码中定位抛出这个 warn 的位置
由于项目使用的是 Vite 来打包,因此可以在 Devtools 的网络面板直接找到 html2canvas 引用的源码文件地址

打开这个文件,搜索 warn 信息,发现只有一个位置,就是这个 createCanvasClone 方法

这个方法接收一个 canvas 参数,根据上下文可以基本确定就是要画的 canvas 元素


分析代码,这个 warn 被抛出要满足以下几个条件:
canvas.getContext('2d')为假或allowTaint为假。allowTaint是一个传入的配置canvas.getContext('webgl2')或canvas.getContext('webgl')为真preserveDrawingBuffer === false
首先前两个条件一定是确定的,因为 canvas 是用 webgl 类型初始化的,所以canvas.getContext('2d') 永远返回 null,而canvas.getContext('webgl')会返回对应的上下文。我们通过 demo 来验证这一点
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<canvas id="canvas1"></canvas>
<canvas id="canvas2"></canvas>
</div>
<script>
// 先 webgl 再 2d,发现 gl1 有值,dd1 为 null
const canvas1 = document.getElementById("canvas1");
const gl1 = canvas1.getContext('webgl');
console.log('canvas1', 'webgl', gl1); // canvas1 webgl WebGLRenderingContext
const dd1 = canvas1.getContext('2d');
console.log('canvas1', '2d', dd1); // null
// 先 2d 再 webgl,发现 dd2 有值,gl2 为 null
const canvas2 = document.getElementById("canvas2");
const dd2 = canvas2.getContext('2d');
console.log('canvas2', '2d', dd2); // canvas2 2d CanvasRenderingContext2D
const gl2 = canvas2.getContext('webgl');
console.log('canvas2', 'webgl', gl2); // null
</script>
</body>
</html>
那么为什么preserveDrawingBuffer === false 会成立呢?尝试新建一个 canvas,手动去获取,发现实际上是返回的 true,那么可以排除浏览器支持性的问题。之后尝试修改 html2canvas 的源码。注意 vite 是有依赖缓存的,可以通过在 vite.config.ts 里将 optimizeDeps.force 设置为 true 来取消

但是运行后,发现 preserveDrawingBuffer 还是 false,这是为什么呢?
实际上,根据 canvas 的 html 标准,<canvas> 元素的渲染上下文类型是在调用 getContext() 方法时确定的,而在第一次调用后,每次都会返回相同的上下文对象

至此,我们可以推断整个问题的原因为 mapbox 的库在初始化的时候基于性能考虑,将preserveDrawingBuffer 设置为 false,导致 html2canvas 在获取的时候数据已经被清除。那么接下来目标就是如何在初始化的时候将preserveDrawingBuffer 设置为 true
在 mapbox-react-gl issue 中搜索 canvas, 根据 github.com/visgl/react... 猜测可以直接传入设置解决。最后也证实了这一点

在注释中我们也看到,设置为 false 主要还是基于性能的考虑,如果需要导出为图片,则需要设置为true
