用 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

相关推荐
J不A秃V头A1 小时前
Vue3:编写一个插件(进阶)
前端·vue.js
司篂篂1 小时前
axios二次封装
前端·javascript·vue.js
姚*鸿的博客2 小时前
pinia在vue3中的使用
前端·javascript·vue.js
宇文仲竹2 小时前
edge 插件 iframe 读取
前端·edge
Kika写代码2 小时前
【基于轻量型架构的WEB开发】【章节作业】
前端·oracle·架构
天下无贼!3 小时前
2024年最新版Vue3学习笔记
前端·vue.js·笔记·学习·vue
Jiaberrr3 小时前
JS实现树形结构数据中特定节点及其子节点显示属性设置的技巧(可用于树形节点过滤筛选)
前端·javascript·tree·树形·过滤筛选
赵啸林3 小时前
npm发布插件超级简单版
前端·npm·node.js
罔闻_spider4 小时前
爬虫----webpack
前端·爬虫·webpack
吱吱鼠叔4 小时前
MATLAB数据文件读写:1.格式化读写文件
前端·数据库·matlab