0%

html2canvas实现ArcGIS API for JavaScript 4.X截图功能

主要介绍ArcGIS API for JavaScript 4.X实现地图截图的两种方式,解决普通地图截图是底图空白的问题。

需求描述

在我们项目开发过程中,有时候需要将地图上面绘制的元素或添加的一些图标之类的小元素进行截图保存或者展示,这时候就需要我们实现关于地图的截图功能。目前在ArcGIS API for JavaScript中其实已经提供了地图截图的API,但是该API对地图底图和一些自定义的需求支持度并不高,所以我们平时项目开发时建议使用第三方截图模块,今天就给大家介绍下关于地图截图的两种方式,最终效果如下:

html2canvas地图截图

实现方法

一、ArcGIS API for JavaScript自带的截图方式

介绍的第一种方式就是ArcGIS API for JavaScript自带的“esri/widgets/Print”截图微件去做,具体代码如下:

1
2
3
4
5
6
7
8
9
const print = new Print({
view: view,
printServiceUrl:
"https://utility.arcgisonline.com/arcgis/rest/services/Utilities/PrintingTools/GPServer/Export%20Web%20Map%20Task"
});

view.ui.add(print, {
position: "top-left"
});

微件截图的使用其实很简单,只需要几行代码就可以搞定,但是随之而来的问题就是自由度并不高,因为它已经自带了一份写好的微件UI,如下图:

html2canvas地图截图

在我们正常的项目开发中其实API自带的UI我们基本是不用的,需要跟着公司UI设计稿来做,所以我们就需要自己写UI,写完UI之后背后的交互逻辑就可以借鉴API自带的逻辑了,这个时候可以用到“esri/widgets/Print”这个API底层使用的一个API模块“esri/tasks/PrintTask”来做了,具体的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const { view } = props;
const [PrintTask, PrintTemplate, PrintParameters] = await loadModules(['esri/tasks/PrintTask', 'esri/tasks/support/PrintTemplate', 'esri/tasks/support/PrintParameters'], gconfig.arcgis_options);
let printTask = new PrintTask({
url: 'https://utility.arcgisonline.com/arcgis/rest/services/Utilities/PrintingTools/GPServer/Export%20Web%20Map%20Task'
});

let template = new PrintTemplate({
format: "pdf",
exportOptions: {
dpi: 300
},
layout: "a4-portrait",
layoutOptions: {
titleText: "pdf标题",
authorText: "作者信息"
}
});

let params = new PrintParameters({
view: view,
template: template
});

printTask.execute(params).then((printResult) => {
if (printResult.url) {
message.success('pdf报告生成成功');
window.open(printResult.url);
} else {
message.error('pdf报告生成失败');
}
});

上述代码中大家可以看到,我们借用PrintTask来自己实现了一个截图功能,截图所需的各类参数直接在代码中写好了,你也可以将你自己写的UI界面的用户输入值传递到我们定义的打印模板信息中去实现用户自定义打印,这一块的代码较简单,就不给大家介绍了。

接下来我们说说这种方式实现截图的问题:

具体的一些细小的问题的话大家可以自己手动尝试去观察,无非就是一些涉及到跨域啊,参数值输入不一致形成的图片变形之类的,但是最大的一个问题就是:当我们的底图如果不使用ArcGIS自带的底图,这种方式实现截图的话会造成底图丢失。解决方法的话目前我也没时间去处理,所以直接采用了第二种实现方式,就是纯前端实现截图,摆脱ArcGIS技术体系。

二、html2canvas实现地图截图

摆脱了ArcGIS的技术体系,跳到整个大前端的领域再看截图这个功能的话,其实是一个很简单的问题,无非就是将所要截取的DOM节点转换为图片这样一个需求,所以我们就找到了html2canvas这个插件模块,选择它主要是参考文档较多,而且它的github活跃量较高,所以不担心一时半会出现停止维护的情况,其中最主要的就是它提供了npm下载引入方式,接下来就看看如何去实现。

html2canvas的官网信息大家可以看一下,其实使用很简单,就是下述几行代码:

1
2
3
4
5
6
7
8
npm install html2canvas  //安装

import html2canvas from 'html2canvas'; //引入

//使用
html2canvas(document.body).then(function(canvas) {
document.body.appendChild(canvas);
});

上述四行代码就完成了安装、引入、使用三个环节,是不是很简单,除了通过import模块化加载之外,还提供<script>标签的普通加载引入,看大家需求。上述使用环节的代码大致思路就是将我们所要截取的DOM节点传入到html2canvas()这个的方法作为第一个参数,这个方法提供第二个参数,就是定义一些截图时的参数,根据需要大家可以根据官网介绍添加一些所需参数,然后在方法的then()回调里面我们就可以拿到截取之后的元素,此时的元素是一个canvas的DOM节点,我们可以直接将它添加到所要展示的区域或者将它转成图片直接打印输出。

介绍完html2canvas的一些基本信息之后,我们就来看看如何用它来实现我们的地图截图。html2canvas实现地图截图其实很简单,因为我们通过ArcGIS API for JavaScript实例化地图的时候需要传入一个存放和展示地图的div,如下:

1
2
3
4
5
6
7
const map = new Map({
basemap,
});
const view = new SceneView({
container: 'map-view', //地图div的id值
map,
});

所以我们截图的时候只需要通过js原生获取DOM节点的方式通过id获取到这个div,然后将它传入html2canvas()这个方法即可,最后在它的回调函数里面拿到截图,如下:

1
2
3
4
5
6
7
8
9
10
const element = document.getElementById('map-view');
const options = {};
html2canvas(element, options).then((canvas) => {
const png = canvas.toDataURL("image/png"); //拿到截图后转换为png图片

const img = document.createElement('img');
img.setAttribute('src', png);
window.document.body.appendChild(img); //将png图片添加到页面验证
console.log(png)
});

通过上述方式我们拿到了截图,然后将其添加到了页面上,但是当我们去查看页面的时候发现底图并没有截到,只有这样一个空白界面:

html2canvas地图截图

这就让人很是惊讶了是不是,我们查看控制台,也并没有报错,但是仔细观看的话会有这样一行警告信息:

html2canvas地图截图

1
#1 133ms Unable to clone WebGL context as it has preserveDrawingBuffer=false <canvas style=​"width:​ 100%;​ height:​100%;​ display:​block;​" width=​"1023" height=​"601">

然后我们分析整个页面结构可以发现一些端倪,如下图所示:

html2canvas地图截图

在通过ArcGIS API for JavaScript 4.X版本实例化地图的时候,我们的底图是通过canvas元素绘制出来的,它并不是之前3.X通过svg的形式绘制的,这就意味着html2canvas在截取的元素中已经包含有另一个canvas元素。结合告警信息不难猜出,ArcGIS API for JavaScript 4.X绘制的canvas元素的绘制句柄肯定是人家做了一定的限制,就想告警信息提示一样,它里面的preserveDrawingBuffer这个属性值是false,这就导致了截图时底图空白的问题,因为html2canvas截图的思路就是将所传入的DOM节点转换为canvas,但是既然传入的元素里面已经包含了一个canvas的话,它内部的转换逻辑肯定就会出错了,那怎么解决这个问题呢?谷歌和百度出来的资料都是千篇一律,说是在html2canvas()这个方法中增加配置信息,例如下面这些:

1
2
3
4
5
6
const options = {
useCORS: true,
// preserveDrawingBuffer: true,
//foreignObjectRendering: true,
allowTaint: true,
};

但其实这种解决方式可能只针对于ArcGIS API for JavaScript 3.X版本实例化出来的底图空白问题有效,并不能结局4.X版本出现的问题,所以我们就来看看针对4.X版本如何解决这一问题。既然告警信息中提示了preserveDrawingBuffer属性值为false,那我们只需要将其设置为true,应该就可以解决,按照这个思路,网上又是一顿搜索操作,最后在Stack Overflow找到了解决方法,大家有兴趣的话可以看看:Canvas toDataURL() returns blank image

其实就是在我们地图实例化的后面,增加一个立即执行函数,在函数里面将preserveDrawingBuffer属性值设置为true即可,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const map = new Map({
basemap,
});
const view = new SceneView({
container: 'map-view',
map,
});
props.setMapView(view);

//解决html2canvas截图空白问题
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);

增加上述代码之后,我们发现截图正常,这样就实现了一个地图截图功能了,以上推荐的就是关于截图空白的最简单的解决方法,其实还有另一种思路:既然传入html2canvas()方法中的元素中包含有另一个canvas元素导致的底图空白,那我们可以在截图之前先将这个canvas转换为一个img标签的DOM节点替换掉现有的canvas,然后再截图,这样其实也可以解决此问题,但是这种方式可能对于动手能力不高的小伙伴来说就不太愿意了,大家有兴趣的话可以尝试一下。