用 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

相关推荐
雪芽蓝域zzs26 分钟前
第八节:Element Plus 基础、封装通用组件 + 路由守卫
前端·vue.js
码视野28 分钟前
基于 Spring Boot + Vue3 的【智慧公厕微负压排风除臭与客流人感空间占用导引中台】设计与实现(含PRD/三端高保真源码/大屏)
前端·vue.js·人工智能·spring boot·后端
计算机魔术师1 小时前
刚赔了15亿又来被告?索尼华纳联手把Anthropic告上法庭
前端
笑霸final1 小时前
不用上传服务器!用 Vue3 + ONNX Runtime Web 在浏览器本地实现智能抠图
运维·前端·图像处理·ai·onnx·canva可画·web性能优化
月月大王的3D日记1 小时前
Three.js 入门系列(9):从零搭一座“闹鬼小屋”
前端·javascript
TingTing1 小时前
Vue3动态组件库建设
前端
码上暴富2 小时前
Cursor / VS Code 自定义文件颜色
前端·vscode
开开心心就好2 小时前
电子教鞭工具支持画框写字插图片功能齐全
android·开发语言·前端·javascript·人工智能·pdf·html
CarIise3 小时前
下拉菜单HTML/CSS/JS实现
前端·css
2601_962071573 小时前
Java进阶(vue基础)
前端·javascript·vue.js