用 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

相关推荐
程序视点3 分钟前
2023最新HitPaw免注册版下载:一键去除图片视频水印的终极教程
前端
小只笨笨狗~1 小时前
el-dialog宽度根据内容撑开
前端·vue.js·elementui
weixin_490354342 小时前
Vue设计与实现
前端·javascript·vue.js
烛阴3 小时前
带你用TS彻底搞懂ECS架构模式
前端·javascript·typescript
卓码软件测评3 小时前
【第三方网站运行环境测试:服务器配置(如Nginx/Apache)的WEB安全测试重点】
运维·服务器·前端·网络协议·nginx·web安全·apache
龙在天3 小时前
前端不求人系列 之 一条命令自动部署项目
前端
开开心心就好3 小时前
PDF转长图工具,一键多页转图片
java·服务器·前端·数据库·人工智能·pdf·推荐算法
国家不保护废物4 小时前
10万条数据插入页面:从性能优化到虚拟列表的终极方案
前端·面试·性能优化
文心快码BaiduComate4 小时前
七夕,画个动态星空送给Ta
前端·后端·程序员
web前端1234 小时前
# 多行文本溢出实现方法
前端·javascript