前言
工作中遇到一个需求,要将页面截图保存。一番调研后使用了 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