OpenLayers图层按需分层渲染至不同Canvas画布

作者:技术拾光

时间:2026-06-04

浏览:446

Openlayers默认采用合成渲染,所有图层合并在一个Canvas上,难以单独控制。按需分层渲染通过设置图层不同类名、背景色或间隔使用Canvas2D与WebGL图层,使特定图层渲染到独立Canvas,便于CSS样式与动画控制。该实现基于Openlayers已有属性,简单有效。

文章目录

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 分层渲染

早期版本的Openlayers(6.0及之前),以及早期版本的ArcGIS、Leaflet,均采用按图层分层渲染的方式。那时的DOM呈现方式非常直观:每个图层对应不同的DOM元素(Div、Canvas、Svg)。

  • 优点:可以直接使用CSS样式、动画对单个图层进行控制或添加效果,灵活性高。
  • 缺点:当图层较多且图层上的元素复杂时,DOM元素会大量增加,性能压力不容忽视。

3 按需分层渲染

本文讨论的“按需分层渲染”,核心目标是:让Openlayers对特定图层进行分层渲染,将这些图层渲染到不同的Canvas画布上,从而方便通过CSS控制Canvas的样式。这听起来像是在追求“鱼和熊掌兼得”的效果。

典型场景包括:

  1. 选中矢量图形时,对切片图层应用黑暗模式。
  2. 选中矢量图形时,对目标图层使用CSS动画,过渡更改透明度。

在合成渲染模式下,这两种需求难以优雅实现,而按需分层渲染则可以轻松解决。

4 实现

实际上,实现按需分层渲染并不复杂,因为这些功能已在Openlayers中内置,只需对某些属性进行设置。满足以下任一条件即可:

  1. 为图层设置不同的类名className(若不设置,默认所有图层类名为ol-layer)
  2. 为图层设置不同的背景色background
  3. 在加入地图的顺序上间隔使用Canvas 2D图层和WebGL图层

5 对比(Openlayers原生)

5.1 Openlayers默认合成渲染

5.1.1 渲染结果

OpenLayers图层按需分层渲染至不同Canvas画布

结合以下代码可以发现:

  • tiandituImgLayer、tiandituCiaLayer、vectorLayer1、vectorLayer2、vectorLayer3被合并渲染到绿色框内的canvas中。
  • webglTileLayer、webglVectorLayer被合并渲染到蓝色框内的canvas中。

在DOM中直观对比:

  • 删除绿色框内的canvas:

OpenLayers图层按需分层渲染至不同Canvas画布

  • 删除蓝色框内的canvas:

OpenLayers图层按需分层渲染至不同Canvas画布

5.1.2 代码
<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/DataServerT=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/DataServerT=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/DataServerT=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({ geometry: new Point(fromLonLat([120.2, 30.3])) }),
    ],
  }),
});

(注:此处代码较长,为保持文章工整,后续完整代码请参考原始文档)

5.2 Openlayers按需分层渲染

5.2.1 设置类名和背景色
5.2.1.1 渲染结果

OpenLayers图层按需分层渲染至不同Canvas画布

结合以下代码可以发现:

  • 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/DataServerT=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/DataServerT=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 渲染结果

OpenLayers图层按需分层渲染至不同Canvas画布

结合以下代码可以发现:

  • 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 渲染结果

OpenLayers图层按需分层渲染至不同Canvas画布

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 渲染结果

OpenLayers图层按需分层渲染至不同Canvas画布

结合代码可以发现:

  • 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 渲染结果

OpenLayers图层按需分层渲染至不同Canvas画布

6.2.1.2 关键代码

与原生写法一样,只需在组件上绑定className属性和background属性即可达到效果。




  




  




  
    
      
      
    
  




  
    
      
      
    
  




  
    
      
      
    
  




  




  

6.2.2 设置加入地图的顺序
6.2.2.1 渲染结果

OpenLayers图层按需分层渲染至不同Canvas画布

6.2.2.2 关键代码

同样只需调整组件在模板中的排列顺序即可。



...
...
...  
...                                
...  
...                                
...                                

从实际效果来看,按需分层渲染的思路非常清晰,实现成本也低,基本上就是几个属性的设置。不管是原生Openlayers还是Vue3-Openlayers封装版本,都可以轻松做到。关键就在于理解Openlayers内部判断“是否需要新建canvas”的逻辑:当两个图层的className不同、background不同,或者渲染管线类型交替出现时,就会触发分层。掌握了这一点,复杂的CSS动画和样式控制就不再是难题。

(注:所有图片、代码示例均基于Openlayers ^10.9.0、Vue3-Openlayers ^12.2.2版本测试验证。)

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章