Openlayers图层按需分层渲染到不同Canvas画布
从事GIS开发的开发者都清楚,Openlayers的渲染机制颇具特色。在默认情况下,它采用“合成渲染”策略——通过协调Canvas 2D和WebGL两个渲染管线,将各个图层统一合并到一个或两个Canvas画布上。听起来很省心?但这种方案的利弊同样突出。
各个库版本信息如下:
"ol": "^10.9.0",
"proj4": "^2.20.8",
"vue3-openlayers": "^12.2.2"
1 合成渲染模式
Openlayers当前的渲染策略为合成渲染,即通过调度Canvas 2D与WebGL两个渲染管线,借助Composite渲染合成器将各图层(如切片图层、矢量图层等)按不同渲染机制处理,最终在浏览器DOM中对应的Canvas画布上呈现图像(默认情况下,Canvas 2D对应一个canvas,WebGL对应一个canvas)。
- 优势:在
常规简单场景中,同一渲染管线处理的数据仅呈现于一个Canvas画布,管理较为便捷。 - 劣势:一旦通过CSS样式、动画对Canvas画布进行控制或添加效果,将影响画布上的所有元素——换言之,所有合成在当前Canvas的图层都会同步变动,无法单独操控某一图层。
2 分层渲染模式
实际上,在早期版本(6.0及之前)的Openlayers中,以及早期版本的ArcGIS、Leaflet,均采用按图层分层渲染的方式。那时的DOM呈现方式非常直观:每个图层对应独立的DOM元素(Div、Canvas、Svg)。
- 优势:可直接利用CSS样式、动画对特定图层进行控制或添加效果,操作自由度极高。
- 劣势:当图层数量较多且图层元素复杂时,DOM元素会显著增加,性能压力不容小觑。
3 按需分层渲染方案
本文所讨论的“按需分层渲染”,核心目标只有一个:让Openlayers对特定图层实施分层渲染,将这些图层输出到不同的Canvas画布上,从而便于通过CSS控制Canvas的样式。听起来是不是有点“鱼与熊掌兼得”的意味?
典型应用场景:
- 选中矢量图形时,对切片图层应用黑暗模式。
- 选中矢量图形时,对目标图层使用CSS动画,平滑过渡更改透明度。
这两种需求在合成渲染模式下基本难以优雅实现,而按需分层渲染则能轻松应对。
4 实现方法
实际上,实现按需分层渲染并不复杂,因为这些功能Openlayers已经内置支持,只需对部分属性进行配置即可。满足以下任一条件即可实现:
为图层设置不同的类名className(未设置时,默认所有图层类名为ol-layer)为图层设置不同的背景色background在加入地图的顺序中间隔使用Canvas 2D图层和WebGL图层
5 对比分析(Openlayers原生)
5.1 Openlayers默认合成渲染效果
5.1.1 渲染结果展示

结合以下代码可以发现:
- tiandituImgLayer、tiandituCiaLayer、vectorLayer1、vectorLayer2、vectorLayer3被合并渲染至绿色框内的canvas中。
- webglTileLayer、webglVectorLayer被合并渲染至蓝色框内的canvas中。
直观对比来看,在DOM中直接操作:
- 删除绿色框内的canvas后:

- 删除蓝色框内的canvas后:

5.1.2 代码示例
<div class="map-page">
<h1>OpenLayers - 天地图影像底图h1>
<div class="info-panel">
<h3>图层说明h3>
<p><strong>TileLayer:strong> 天地图影像底图 + 影像注记p>
<p>
<strong>VectorLayer:strong> 三角形 / 矩形 / 五边形(不同描边和填充)
p>
<p><strong>WebGLTile:strong> 天地图矢量底图(透明度 0.5)p>
<p><strong>WebGLVector:strong> 城市点位渲染p>
div>
<div id="tianditu-ol-map" ref="mapContainer" class="map-container">div>
<div class="legend">
<h4>图例h4>
<div class="legend-item">
<span
class="legend-color"
style="
background-color: rgba(0, 102, 255, 0.3);
border: 3px solid #0066ff;
"
>span>
<span>三角形 - 蓝色实线描边span>
div>
<div class="legend-item">
<span
class="legend-color"
style="
background-color: rgba(255, 0, 0, 0.2);
border: 2px dashed #ff0000;
"
>span>
<span>矩形 - 红色虚线描边span>
div>
<div class="legend-item">
<span
class="legend-color"
style="
background-color: rgba(0, 204, 0, 0.25);
border: 4px solid #00cc00;
"
>span>
<span>五边形 - 绿色实线描边span>
div>
<div class="legend-item">
<span
class="legend-dot"
style="background-color: rgba(255, 165, 0, 0.8)"
>span>
<span>WebGLVector 城市点位span>
div>
<div class="legend-item">
<span class="legend-label">WebGLTile 天地图矢量底图(透明度 0.5)span>
div>
div>
div>
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import Map from "ol/Map";
import View from "ol/View";
import TileLayer from "ol/layer/Tile";
import VectorLayer from "ol/layer/Vector";
import VectorSource from "ol/source/Vector";
import { XYZ } from "ol/source";
import { Style, Fill, Stroke } from "ol/style";
import { Polygon, Point } from "ol/geom";
import Feature from "ol/Feature";
import { fromLonLat } from "ol/proj";
import WebGLTile from "ol/layer/WebGLTile";
import WebGLVector from "ol/layer/WebGLVector";// 天地图 Token(请替换为自己的 Token)
const TK = "";const mapContainer = ref<HTMLDivElement>();
let map: Map | null = null;// ===== 天地图影像底图 =====
const tiandituImgLayer = new TileLayer({
source: new XYZ({
url: `https://t{0-7}.tianditu.gov.cn/DataServer?T=img_w&x={x}&y={y}&l={z}&tk=${TK}`,
}),
});// 天地图影像注记层
const tiandituCiaLayer = new TileLayer({
source: new XYZ({
url: `https://t{0-7}.tianditu.gov.cn/DataServer?T=cia_w&x={x}&y={y}&l={z}&tk=${TK}`,
}),
});// ===== 三个矢量图层(不同形状、描边、填充) =====// 图层1: 三角形 - 蓝色实线描边, 浅蓝填充
const vectorLayer1 = new VectorLayer({
source: new VectorSource({
features: [
new Feature({
geometry: new Polygon([
[
fromLonLat([100, 28]),
fromLonLat([108, 28]),
fromLonLat([104, 36]),
fromLonLat([100, 28]),
],
]),
}),
],
}),
style: new Style({
stroke: new Stroke({ color: "#0066FF", width: 3 }),
fill: new Fill({ color: "rgba(0, 102, 255, 0.3)" }),
}),
});// 图层2: 矩形 - 红色虚线描边, 浅红填充
const vectorLayer2 = new VectorLayer({
source: new VectorSource({
features: [
new Feature({
geometry: new Polygon([
[
fromLonLat([110, 24]),
fromLonLat([118, 24]),
fromLonLat([118, 30]),
fromLonLat([110, 30]),
fromLonLat([110, 24]),
],
]),
}),
],
}),
style: new Style({
stroke: new Stroke({ color: "#FF0000", width: 2, lineDash: [10, 5] }),
fill: new Fill({ color: "rgba(255, 0, 0, 0.2)" }),
}),
});// 图层3: 五边形 - 绿色实线描边, 浅绿填充
const vectorLayer3 = new VectorLayer({
source: new VectorSource({
features: [
new Feature({
geometry: new Polygon([
[
fromLonLat([108, 38]),
fromLonLat([113, 35]),
fromLonLat([118, 38]),
fromLonLat([115, 43]),
fromLonLat([110, 43]),
fromLonLat([108, 38]),
],
]),
}),
],
}),
style: new Style({
stroke: new Stroke({ color: "#00CC00", width: 4 }),
fill: new Fill({ color: "rgba(0, 204, 0, 0.25)" }),
}),
});// ===== WebGLTile 图层 - 天地图矢量底图, 透明度 0.5 =====
const webglTileLayer = new WebGLTile({
opacity: 0.5,
source: new XYZ({
url: `https://t{0-7}.tianditu.gov.cn/DataServer?T=vec_w&x={x}&y={y}&l={z}&tk=${TK}`,
}) as any,
});// ===== WebGLVector 图层 - 城市点位 =====
const webglVectorLayer = new WebGLVector({
source: new VectorSource({
features: [
new Feature({ geometry: new Point(fromLonLat([116.4, 39.9])) }),
new Feature({ geometry: new Point(fromLonLat([121.5, 31.2])) }),
new Feature({ geometry: new Point(fromLonLat([113.3, 23.1])) }),
new Feature({ #
(注:此处代码较长,为保持文章结构工整,完整代码请参考原始文档)
5.2 Openlayers按需分层渲染实现
5.2.1 设置类名与背景色
5.2.1.1 渲染效果呈现

结合以下代码可以看出:
- tiandituImgLayer、tiandituCiaLayer,类名均为layer1,被合并渲染至红色框内的canvas中。
- vectorLayer1,类名为layer2,被单独渲染至橙色框内的canvas中。
- vectorLayer2,类名为layer3,被渲染至绿色框内的canvas中。
- vectorLayer3,类名同样为layer3,但设置了背景色rgba(0,0,0,0.5),被渲染至蓝色框内的canvas中。
- webglTileLayer,类名为layer4,被渲染至黑色框内的canvas中。
- webglVectorLayer,类名为layer5,被渲染至灰色框内的canvas中。
5.2.1.2 关键代码展示
// 其他代码保持不变,关键改动仅在于为每个图层显式指定 className
const tiandituImgLayer = new TileLayer({
source: new XYZ({
url: `https://t{0-7}.tianditu.gov.cn/DataServer?T=img_w&x={x}&y={y}&l={z}&tk=${TK}`,
}),
className: "layer1", // 关键配置
});
const tiandituCiaLayer = new TileLayer({
source: new XYZ({
url: `https://t{0-7}.tianditu.gov.cn/DataServer?T=cia_w&x={x}&y={y}&l={z}&tk=${TK}`,
}),
className: "layer1", // 与底图共享同一个canvas
});
const vectorLayer1 = new VectorLayer({
// ... 省略几何等配置
className: "layer2", // 独立canvas
});
const vectorLayer2 = new VectorLayer({
// ... 省略几何等配置
className: "layer3", // 独立canvas
});
const vectorLayer3 = new VectorLayer({
// ... 省略几何等配置
className: "layer3", // 与vectorLayer2同class,但background不同
background: "rgba(0,0,0,0.5)", // 背景色不同,也会触发独立canvas
});
const webglTileLayer = new WebGLTile({
// ...
className: "layer4",
});
const webglVectorLayer = new WebGLVector({
// ...
className: "layer5",
});
5.2.2 通过调整图层顺序实现分层
5.2.2.1 渲染效果展示

结合以下代码可以发现:
- tiandituImgLayer、tiandituCiaLayer,类名均为ol-layer(默认值),被合并渲染至红色框内的canvas中。
- webglTileLayer,同样是ol-layer,被渲染至橙色框内的canvas中——此处已被独立出来,因为它是WebGL图层,与前面的Canvas 2D图层类型不同。
- vectorLayer1,ol-layer,被渲染至绿色框内的canvas中。
- webglVectorLayer,ol-layer,被渲染至蓝色框内的canvas中。
- vectorLayer2、vectorLayer3,ol-layer,被渲染至黑色框内的canvas中。
注意:本例中所有图层均未设置className,但通过交替添加Canvas 2D和WebGL图层,使Openlayers识别到渲染管线差异,自动分配到不同的canvas中。
5.2.2.2 关键代码实例
// 关键:通过调整 layers 数组中的排列顺序
onMounted(() => {
map = new Map({
target: mapContainer.value!,
layers: [
tiandituImgLayer, // Canvas 2D
tiandituCiaLayer, // Canvas 2D
webglTileLayer, // WebGL -> 与前面不同,触发新canvas
vectorLayer1, // Canvas 2D -> 与前面不同,触发新canvas
webglVectorLayer, // WebGL -> 触发新canvas
vectorLayer2, // Canvas 2D -> 触发新canvas
vectorLayer3, // Canvas 2D (与vectorLayer2同类型,合并)
],
view: new View({
center: fromLonLat([108, 34]),
zoom: 5,
}),
});
});
5.2.3 为图层添加CSS动画效果
5.2.3.1 渲染效果展示

5.2.3.2 关键代码实现
// 为特定类名的图层添加CSS动画
// 在style中写入:
:deep(.layer1) {
animation: fadeIn 2s ease-in infinite;
}
@keyframes fadeIn {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
如此一来,只有类名为layer1的图层(天地图影像底图和注记层)会执行淡入淡出动画,其他图层完全不受干扰。
6 对比分析(Vue3-Openlayers版本)
6.1 默认合成渲染模式
6.1.1 渲染结果展示

结合代码可以发现:
- tiandituImgLayer、tiandituCiaLayer、vectorLayer1、vectorLayer2、vectorLayer3被合并渲染至绿色框内的canvas中。
- webglTileLayer、webglVectorLayer被合并渲染至蓝色框内的canvas中。
另外值得留意的一个细节:使用Vue3-Openlayers后,DOM中会额外出现一些空div,高度均为0。这算是框架封装带来的一点小代价。
6.1.2 代码示例
// 该部分代码较长,与原生写法类似,文档中已详细展示,此处不再重复
6.2 按需分层渲染实现
6.2.1 设置类名与背景色
6.2.1.1 渲染效果呈现

6.2.1.2 关键代码展示
与原生写法一致,只需在组件上绑定className属性和background属性即可达到预期效果。
6.2.2 通过调整图层顺序实现分层
6.2.2.1 渲染效果展示

6.2.2.2 关键代码实例
同样只需调整组件在模板中的排列顺序即可实现。
...
...
...
...
...
...
...
从实际效果来看,按需分层渲染的思路非常清晰,实现成本也很低,基本上只需配置几个属性即可。无论是原生Openlayers还是Vue3-Openlayers封装版本,都可以轻松实现。关键在于理解Openlayers内部判断“是否需要新建canvas”的逻辑:当两个图层的className不同、background不同,或者渲染管线类型交替出现时,就会触发分层渲染。掌握了这一要点,复杂的CSS动画和样式控制便不再是难题。
(注:所有图片、代码示例均基于Openlayers ^10.9.0、Vue3-Openlayers ^12.2.2版本测试验证。)
