基于AntV L7封装的拓扑渲染

0 阅读7分钟

数万设备流畅渲染:我用 AntV L7 封装 Vue3 地图组件的血泪经验

3万条管道数据,第一次渲染花了8秒。优化后,800毫秒。这篇文章记录了我踩过的所有坑。

效果图

局部截取_20260513_124618.png

一个真实的问题

去年接手一个智慧供热项目。需求很明确:在地图上展示全市的供热管网——管道、阀门、换热站,还要有流向动画。

最终选了 AntV L7。原因很简单:国产开源,符合信创要求;底图可以随意切换,高德、百度、天地图都行;最关键的是——支持完全离线部署

这个决定,让我踩了三个月的坑。

为什么 L7 适合这类项目

在深入代码之前,先说说我理解的 L7 核心优势。

国产化不是口号,是刚需。政府、能源、金融项目,信创是硬指标。L7 是蚂蚁金服开源的,中文文档完善,社区活跃,不用担心"用着用着突然收费"。

底图切换是个隐藏福利。开发阶段用高德地图,上线后切天地图或自建瓦片服务,代码几乎不用改。我们项目就是这样:内网环境用本地瓦片,外网演示用高德,一套代码两套环境。

离线部署能力救了我们的命。项目部署在政务内网,完全断网。L7 的 BlankMap 配合本地瓦片服务,完美解决。

// 离线部署的核心代码
import { Scene } from '@antv/l7'
import { Map as BlankMap } from '@antv/l7-maps'

const scene = new Scene({
  id: 'map',
  map: new BlankMap({
    center: [116.397428, 39.90923],
    zoom: 12,
  }),
})

// 加载本地瓦片
scene.on('loaded', () => {
  const tileLayer = new TileLayer({
    source: {
      data: 'http://内网服务器/tiles/{z}/{x}/{y}.png',
      parser: { type: 'rasterTile' },
    },
  })
  scene.addLayer(tileLayer)
})

组件架构:从混乱到清晰

最初的混乱

一开始,我把所有逻辑都塞在一个 Vue 文件里。2000 行代码,改一个地方,三个地方报错。

重构后的结构

l7mapRender/
├── index.vue              # 主组件(300行)
├── composables/           # 图层渲染逻辑
│   ├── useDeviceRender.ts      # 设备图层
│   ├── useLineRender.ts        # 管道图层
│   └── ...
├── types/                 # TypeScript 类型
└── utils/                 # 工具函数
    ├── filter.ts          # 过滤逻辑
    └── color.ts           # 颜色计算

核心思路:每个图层一个 composable,主组件只负责调度。

// 主组件的核心逻辑
function referRender() {
  // 清除旧图层
  layerMap.forEach((layer) => scene.removeLayer(layer))
  
  // 按需渲染
  for (const name of props.renderName) {
    switch (name) {
      case 'topologyDevices':
        processDeviceData(scene, topologyKey)
        break
      case 'topologyLinesFlow':
        processLineFlowData(scene, true, topologyKey)
        break
    }
  }
}

性能优化:从8秒到800毫秒

这是我踩坑最多的地方。

坑一:ref 导致的卡顿

L7 的 Scene 对象内部属性极多。我用 ref 包了一层,Vue 对每个属性都做了响应式处理。

结果:每次地图移动,页面卡顿2秒。

解决:改用 shallowRef

// ❌ 错误
const scene = ref<Scene | null>(null)

// ✅ 正确
const scene = shallowRef<Scene | null>(null)

性能提升:70%

坑二:频繁创建图层

用户缩放地图时,我直接销毁旧图层、创建新图层。

结果:缩放5次,GPU 内存占用飙到 2GB。

解决:用 setData 更新数据,而不是重建图层。

// ❌ 错误
scene.removeLayer(oldLayer)
const newLayer = new LineLayer().source(newData)
scene.addLayer(newLayer)

// ✅ 正确
layer.setData(newData, { parser: { type: 'json', coordinates: 'latLng' } })

坑三:WebGL 内存泄漏

组件卸载后,GPU 内存没有释放。用户反复进出地图页面,浏览器直接崩溃。

解决:手动清理 WebGL 上下文。

onBeforeUnmount(() => {
  if (scene.value) {
    scene.value.removeAllLayer()
    
    // 关键:释放 GPU 资源
    const canvas = container?.querySelector('canvas')
    if (canvas) {
      const gl = canvas.getContext('webgl')
      const ext = gl?.getExtension('WEBGL_lose_context')
      ext?.loseContext()
      canvas.remove()
    }
    
    scene.value.destroy()
  }
})

坑四:没有过滤机制

3万条管道全部渲染,地图直接卡死。

解决:多维度过滤。

function filterDevice(item: TopologyDevice, bounds: MapBounds): boolean {
  // 1. 状态过滤
  if (item.userData?.status?.delete) return false
  
  // 2. 视图范围过滤(只渲染可见区域)
  if (!isInView(item.latLng, bounds)) return false
  
  // 3. 渲染等级过滤(根据缩放级别)
  if (mapZoom < item.mapGrade) return false
  
  // 4. 业务过滤(供回水类型、区域等)
  if (!matchBusinessFilter(item)) return false
  
  return true
}

过滤后,平均渲染数量从 30000 降到 2000。

坑五:短管合并为长管渲染

这是最有效的一个优化,也是我之前在 ArcGIS + ECharts 管网渲染优化 中分享过的技巧。

问题:城市管网中,短管数量远多于长管。一条主干道上可能有几十条短管,每条都要单独渲染一个 LineLayer 图元。

思路:在低缩放级别时,把多条短管合并为一条长管显示。

关键约束:只有单条路径上的短管才能合并。为什么?因为单条路径上的管道流向必然一致,合并后不会出现流向矛盾。

// 短管合并的核心逻辑
function mergeShortLines(lines: TopologyLine[], mapZoom: number): LongLine[] {
  const longLines: LongLine[] = []
  const merged = new Set<string>()

  for (const line of lines) {
    if (merged.has(line.tpId)) continue

    // 寻找可以合并的连续短管
    const chain = findContinuousChain(line, lines)
    
    if (chain.length >= 3) {
      // 合并为长管
      const longLine = {
        tpId: `long_${line.tpId}`,
        latLng: chain.flatMap(l => l.latLng),
        userData: {
          children: chain,
          mapGrade: calculateMapGrade(chain), // 根据总长度计算显示等级
        },
      }
      longLines.push(longLine)
      chain.forEach(l => merged.add(l.tpId))
    }
  }

  return longLines
}

渲染策略

function processLineFlowData(scene: Scene, hasFlow: boolean, topologyKey?: string) {
  const mapZoom = scene.getZoom()
  const longLineConfig = ProjectConfig.renderConfig.longLineConfig
  
  // 低缩放级别 → 使用长管模式
  const useLongLineMode = longLineConfig.start && mapZoom <= longLineConfig.mapGrade

  let lineList = []
  
  if (useLongLineMode) {
    // 合并短管,渲染长管
    const longLines = publicTopology.getLongLines()
    for (const longLine of longLines) {
      lineList.push({
        tpId: longLine.tpId,
        latLng: longLine.latLng,  // 合并后的坐标
        color: getLongLineColor(longLine),
        width: getLongLineWidth(longLine),
      })
    }
  } else {
    // 高缩放级别 → 渲染原始短管
    const lines = publicTopology.getTopologyData().lines
    for (const line of lines) {
      lineList.push({
        tpId: line.tpId,
        latLng: line.latLng,
        color: getLineColor(line),
        width: getLineWidth(line),
      })
    }
  }

  // 创建图层...
}

效果对比

场景短管数量合并后长管数量渲染图元减少
全市视图(zoom=8)28000350087%
区域视图(zoom=12)15000800047%
街道视图(zoom=16)300030000%(不合并)

这个优化让低缩放级别的渲染性能提升了 5 倍

流向动画:L7 的杀手锏

管道流向动画是 L7 的亮点。实现原理很简单:

  1. 基础层:渲染所有管道,设置低透明度
  2. 流向层:只渲染有流向的管道,添加动画
// 基础层
const baseLayer = new LineLayer()
  .source(lineData, { parser: { type: 'json', coordinates: 'latLng' } })
  .shape('line')
  .color('color')
  .style({ opacity: 0.3 })  // 降低透明度

// 流向动画层
const flowLayer = new LineLayer()
  .source(flowData, { parser: { type: 'json', coordinates: 'latLng' } })
  .shape('line')
  .animate({
    interval: 1,
    trailLength: 0.5,
    duration: 1,
  })

scene.addLayer(baseLayer)
scene.addLayer(flowLayer)

关键点:流向方向由坐标数组顺序决定。[起点, 终点][终点, 起点] 的动画方向相反。

底图切换:一套代码,多种环境

L7 的底图适配器设计得很优雅。切换底图只需改一行代码:

// 高德地图
import { GaodeMap } from '@antv/l7-maps'

// 百度地图
import { BaiduMap } from '@antv/l7-maps'

// 天地图
import { TMap } from '@antv/l7-maps'

// 空白底图(离线)
import { Map as BlankMap } from '@antv/l7-maps'

我们的项目配置:

const mapConfig = {
  center: [87.700286, 43.759448],
  zoom: 10.64,
  // ... 其他配置
}

// 根据环境选择底图
let MapAdapter
if (isOffline) {
  MapAdapter = new BlankMap(mapConfig)
} else if (useGaode) {
  MapAdapter = new GaodeMap(mapConfig)
} else {
  MapAdapter = new TMap(mapConfig)
}

const scene = new Scene({
  id: 'map',
  map: MapAdapter,
})

最终效果

优化后的性能数据:

指标优化前优化后
首次渲染8秒800毫秒
缩放响应2秒卡顿流畅
GPU 内存持续增长稳定在 200MB
支持数据量5000条100000条
低缩放级别图元数280003500(短管合并后)

组件使用示例

完整代码已上传 GitHub:weiweiweigang/topo-l7map

<template>
  <L7MapRender
    ref="mapRef"
    :render-name="['topologyDevices', 'topologyLinesFlow']"
    :map-config="{ center: [116.4, 39.9], zoom: 12 }"
    @layer-click="onDeviceClick"
  />
</template>

<script setup lang="ts">
import { L7MapRender } from '@/components/l7mapRender'

const mapRef = ref()

function onDeviceClick(data) {
  console.log('点击了设备:', data.tpId)
}

// 外部触发重新渲染
function refresh() {
  mapRef.value?.referRender()
}
</script>

总结

三个月踩坑,换来这些经验:

  1. shallowRef 不是可选项:L7 对象必须用 shallowRef,否则性能灾难
  2. setData 而非重建:更新数据用 setData,避免频繁创建销毁图层
  3. WebGL 必须手动清理:组件卸载时释放 GPU 资源,否则内存泄漏
  4. 过滤是性能的关键:只渲染可见区域的数据
  5. 短管合并是最有效的优化:低缩放级别下,合并短管可减少 80% 以上的渲染图元
  6. 底图切换是隐藏福利:一套代码适配多种环境

L7 不是完美的,文档有坑,API 有变动。但对于国产化要求、离线部署需求的项目,它是目前最好的选择。


相关链接

有问题欢迎评论区交流,我看到都会回复。