用 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

相关推荐
嘟嘟07172 分钟前
React 受控组件与非受控组件:表单数据到底归谁管?
前端·javascript·react.js
嘟嘟07174 分钟前
React 表单管理进阶:从单字段到带校验的完整登录表单
前端·javascript·react.js
光影少年12 分钟前
react navite原生事件监听、全局事件通知
前端·react native·react.js
lvv13 分钟前
不会被渲染的组件,为什么让我的首屏白屏了?(一次前端问题总结)
前端·webpack·性能优化
码上成长38 分钟前
微前端 Invalid hook call?先把「window 注入 + externals」整明白
前端·react.js·前端框架
宿67439 分钟前
vue3-DOM树
前端·javascript·vue.js
白狐_7981 小时前
408数据结构:中缀转后缀与操作符栈容量——真题精讲
前端·javascript·数据结构
谷哥的小弟1 小时前
TypeScript在Vue3中的常见写法
前端·javascript·typescript
人间凡尔赛2 小时前
2026 前端全栈新范式:Server Actions + Edge Runtime,告别传统 API 路由
前端·全栈·next.js
码林鼠4 小时前
2026前端面试题(二)
前端