用 html2canvas 导出 mapbox 失败的排查过程

前言

工作中遇到一个需求,要将页面截图保存。一番调研后使用了 html2canvas 这个库,在实际使用时发现 Mapbox 渲染的地图一直是空白的,没有被渲染上。尝试了 modern-screenshotdom-to-image 这两个库,还是同样的问题,猜测原因应该在地图本身的渲染上

排查过程

排查首先肯定是看库本身是否有反馈有效信息。在 Devtools 的控制台发现这样一条 warn 信息:

简单来说就是 canvas 无法被克隆的原因是其上的 WebGL conntextpreserveDrawingBufferfalse。那个这个 WebGL conntextpreserveDrawingBuffer 又是什么呢?

工作中使用 canvas 时往往有这样一套起手式:

tsx 复制代码
const ctx = canvas.getContext("2d");
// do something...

实际上 getContext 这个方法的定义是这样的:

tsx 复制代码
getContext(contextId, options)

第一个参数 contextId 除了 2d 外,还可以传入 webglwebgl2bitmaprendererwebgpu这 4 种。根据 contextId ,第二个参数 options 也不同,而 preserveDrawingBuffer 就是 webgl 的参数之一。MDN 的介绍是 "如果这个值为 true 缓冲区将不会被清除,会保存下来,直到被清除或被使用者覆盖。"在 WebGL 中,默认情况下,绘制缓冲会在每一帧被清除,以便进行下一次绘制。通过将 preserveDrawingBuffer 选项设置为 true,创建了一个带有保留绘图缓冲功能的 WebGL 上下文。在此之后继续进行其他的 WebGL 操作,而绘图缓冲的内容将会保留,直到手动清除它或下一次绘制发生

了解 WebGL conntextpreserveDrawingBuffer 是什么后,到 html2canvas 源码中定位抛出这个 warn 的位置

由于项目使用的是 Vite 来打包,因此可以在 Devtools 的网络面板直接找到 html2canvas 引用的源码文件地址

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

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

分析代码,这个 warn 被抛出要满足以下几个条件:

  1. canvas.getContext('2d')为假或 allowTaint 为假。allowTaint 是一个传入的配置
  2. canvas.getContext('webgl2')canvas.getContext('webgl') 为真
  3. 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

相关推荐
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang1 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、4 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤6 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser6 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la6 小时前
vue的样式知识点
前端·javascript·vue.js
别忘了微笑_cuicui6 小时前
elementUI中2个日期组件实现开始时间、结束时间(禁用日期面板、控制开始时间不能超过结束时间的时分秒)实现方案
前端·javascript·elementui