前言
之前基于 vue-print-np
改造开发了新版的 Vue 打印插件: vue-print-next
,虽然只在一篇文章里提了一句,但也陆续收获了几十个 star。尽管 npm 每周下载量仅有 20-30 次,但能有人使用,我还是感到非常开心。
前段时间有一位用户提了一个 issue,表示在打印地图(如 GIS,类似百度地图、高德地图)时,打印结果是空白的。定位这个问题花费了不少时间,最终找到了原因并解决。本文将复现问题,并详细分析其原因与解决方法。
问题复现
以下是一个用于复现问题的简单 Demo:
<!--
* @Author: zi.yang
* @Date: 2024-12-22 23:00:26
* @LastEditors: zi.yang
* @LastEditTime: 2024-12-23 08:25:18
* @Description: 地图打印问题复现
* @FilePath: /demo/webgl-print/index.html
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>地图打印问题复现</title>
</head>
<body>
<div id="app">
<button @click="printMap">打印地图</button>
<div id="map" ref="mapContainer"
style="width: 500px; height: 400px; background-color: lightgray;">
<!-- 模拟地图区域 -->
<canvas id="mapCanvas" width="500" height="400"></canvas>
</div>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script type="module">
import { VuePrintNext } from 'https://unpkg.com/vue-print-next/dist/vue-print-next.js';
function draw() {
const canvas = document.getElementById('mapCanvas');
const gl = canvas.getContext('webgl');
if (gl) {
gl.clearColor(0.0, 0.5, 0.5, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
}
}
const app = Vue.createApp({
mounted() {
draw()
},
methods: {
printMap() {
new VuePrintNext({ el: '#map' })
}
}
});
app.mount('#app');
</script>
</body>
</html>
上述代码中的 canvas
在页面显示时有一个绿色背景方块,但打印结果却是灰白的。这只是一个简单例子,实际上无论 canvas
绘制什么内容,打印结果都只会显示为灰白背景。
如下是打印后的效果:
问题分析
要分析这个问题,首先需要了解目前常用的局部打印方案及其对 canvas
的处理方式。
局部打印原理
局部打印通常通过以下步骤实现:
- 获取需要打印的 DOM 元素。
- 将该元素克隆到一个新的
iframe
中。 - 调用浏览器的打印功能打印该
iframe
。
对于 canvas
元素,为了确保打印效果,打印库一般会调用 canvas.toDataURL
方法将 canvas
内容转换为 base64
格式的图片,然后将其替换为 img
标签,从而正常显示。
由此可以推断,地图打印为空白的原因很可能是 toDataURL
输出的内容有问题。
尝试直接输出
在控制台中直接调用 toDataURL
输出地图的内容:
const canvas = document.getElementById('mapCanvas');
console.log(canvas.toDataURL());
通过工具解码后,结果是一个空白图片。这表明 toDataURL
方法并未正确转换 canvas
内容。
为了进一步排查,我们尝试使用其他绘图工具(如 ECharts)生成 canvas
并导出 base64
,结果是正常的。这说明问题与 WebGL 绘图方式有关。
问题根源
通过查阅资料发现,WebGL 模式下,preserveDrawingBuffer
参数默认值为 false
。这是出于性能和内存优化的考虑,在每帧渲染完成后会自动清除画布缓存。对于需要保留画布数据的场景(如打印、截图),这种行为会导致 toDataURL
方法无法获取画布内容,从而输出空白。
解决方法
有两种常见的解决方法:打印前重绘一次,或者开启 preserveDrawingBuffer
配置
方法一:打印前重绘
最简单的一种方式,如果你使用的库支持,或者本身就是你自己写的 webgl 方法。 那么可以在打印之前调用一次渲染方法,这样就可以成功打印了。
const app = Vue.createApp({
mounted() {
draw()
},
methods: {
printMap() {
// 打印前重新绘制 webgl
draw()
new VuePrintNext({ el: '#map' })
}
}
});
在调用打印功能前调用一次绘制方法 draw()
,确保 canvas
内容在打印时是完整的。
方法二:开启配置
在初始化 WebGL 上下文时,将 preserveDrawingBuffer
参数设置为 true
,以确保每次绘制后画布内容不会被清除。
可以通过重写 getContext
方法实现:
// 重写 getContext 方法
HTMLCanvasElement.prototype.getContext = (function (origFn) {
return function (type, attributes) {
if (type === 'webgl') {
attributes = Object.assign({}, attributes, {
preserveDrawingBuffer: true,
});
}
return origFn.call(this, type, attributes);
};
})(HTMLCanvasElement.prototype.getContext);
在此方法中,我们拦截了 canvas.getContext
方法的调用,将 preserveDrawingBuffer
设置为 true
后再返回上下文。这样在打印时无需额外重绘即可获取正确的内容。
总结
WebGL 中 preserveDrawingBuffer
默认值为 false
是导致打印空白的根本原因。对于需要打印或截图的场景,可以根据需求选择以下方案:
- 打印前重绘:适合对性能要求高、不希望永久开启
preserveDrawingBuffer
的场景。 - 启用
preserveDrawingBuffer
:适合打印或截图需求频繁、对性能要求不高的场景。
如果使用的库(webgl 地图)支持重新渲染方法,还是推荐仅在打印前重绘,避免因为开启preserveDrawingBuffer
导致造成性能问题。
相关链接
- WebGL 小技巧: webglfundamentals.org/webgl/lesso…
- MDN: developer.mozilla.org/en-US/docs/…
- vue-print-next: github.com/Alessandro-…