10000+ 个点位轻松展示,使用 Leaflet 实现地图海量标记点聚类

一、 前言

最近在做海外地图可视化,采用 Leaflet + OpenStreetMap 作为地图展示框架。但是在数据量较大的情况下,为了提升用户体验,需要对地图上的标记点进行聚类处理。

经过一番搜索,发现了 Leaflet.markercluster 插件,它可以帮助我们实现标记点的聚合效果,使得地图上的标记点数量减少,同时也保持了标记点的可读性和交互性。

例如:演示示例渲染了 10000+ 点位,轻轻松松!

天地图效果

OpenStreetMap 地图效果

通过本文章,我将通过 Leaflet.markercluster 简单实现一个标记点聚类的案例,希望能够帮助大家更好地理解和使用该插件。

二、介绍

1. 什么是 Leaflet.markercluster?

首先,Leaflet 是一款开源的 JavaScript 地图库,它提供了丰富的地图交互功能,如缩放、平移、标记点、弹出框等。它的设计目标是简单易用,同时支持多种地图服务提供者,如 OpenStreetMapGoogle Maps 等。

关于如何使用 Leaflet 实现一个基本地图的流程,参考 从 0 到 1 快速实现海外地图接入

Leaflet.markercluster 是一款为 Leaflet 地图库开发的标记点聚类插件,旨在提供美观且动画效果的标记聚类功能。它通过将地图上靠近的标记分组来提高用户体验,特别是在标记数量众多的情况下,如下图所示:

2. 为什么需要标记点聚类?

在地图上显示大量标记点时,会导致以下问题:

  • 地图变得拥挤,信息重叠
  • 性能下降,影响用户体验
  • 难以快速定位和查看特定标记
  • 视觉混乱,降低地图可读性

标记点聚类可以将这些标记点聚合在一起,形成一个聚类点,同时显示该聚类点的数量或其他信息,从而提高地图的可读性和交互性。

3. Leaflet.markercluster 的优势

  • 性能优化:通过聚类减少实际渲染的标记点数量
  • 交互友好:提供平滑的动画效果和直观的交互方式
  • 高度可定制:支持自定义样式、行为和事件处理
  • 兼容性好:支持各种地图服务提供商
  • 维护活跃:持续更新和 bug 修复

三、如何使用 Leaflet.markercluster

1. 使用 npm 安装

适用于 Node.js 项目,通过 npm 安装 LeafletLeaflet.markercluster,同时也可以引入类型定义文件,用于 TypeScript 项目。

bash 复制代码
# 安装 Leaflet 和 Leaflet.markercluster
npm install leaflet leaflet.markercluster

# 安装类型定义文件,用于 TypeScript 项目
npm install @types/leaflet @types/leaflet.markercluster

通过以上的命令可以在项目中引入 LeafletLeaflet.markercluster,同时也可以引入类型定义文件,用于 TypeScript 项目。

在项目中引入:

js 复制代码
import L from 'leaflet'
import 'leaflet.markercluster'
import 'leaflet/dist/leaflet.css'
import 'leaflet.markercluster/dist/MarkerCluster.css'
import 'leaflet.markercluster/dist/MarkerCluster.Default.css'

本文使用的 Leaflet 相关版本是:[email protected] [email protected]

2. 或使用 CDN 引入

适用于不需要本地安装的场景,直接引入 CDN 链接,在 HTML 中引入相关的样式和脚本即可。

html 复制代码
<!-- 引入leaflet样式和js -->
<link
  rel="stylesheet"
  href="https://unpkg.com/[email protected]/dist/leaflet.css"
/>
<script src="https://unpkg.com/[email protected]/dist/leaflet-src.js"></script>
<!-- 引入leaflet.markercluster样式和js -->
<link
  rel="stylesheet"
  href="https://unpkg.com/[email protected]/dist/MarkerCluster.css"
/>
<link
  rel="stylesheet"
  href="https://unpkg.com/[email protected]/dist/MarkerCluster.Default.css"
/>
<script src="https://unpkg.com/[email protected]/dist/leaflet.markercluster-src.js"></script>

3. 在项目中使用

通过上面的步骤将插件引入成功后,接下来可以初始化地图和聚合图层

javascript 复制代码
// 初始化地图
var map = L.map('map').setView([38, -8], 7)

// 添加瓦片图层
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map)

// 创建聚合图层
var markers = L.markerClusterGroup()

// 添加标记点
for (let i = 0; i < 1000; i++) {
  const marker = L.marker([getRandom(37, 39), getRandom(-9.5, -6.5)])
  markers.addLayer(marker)
}

// 将聚合图层添加到地图
map.addLayer(markers)

// 随机坐标生成函数
function getRandom(min, max) {
  return Math.random() * (max - min) + min
}

四、可选配置选项

Leaflet.MarkerCluster 的常用配置选项有很多,在这里我用 Cursor 罗列了几个常用的配置项,在项目中体验一下如何进行配置

1. 常用配置

javascript 复制代码
const markerClusterLayer = L.markerClusterGroup({
  showCoverageOnHover: false, // 鼠标悬停时是否显示覆盖范围
  zoomToBoundsOnClick: true, // 点击聚合点时是否缩放至合适级别
  spiderfyOnMaxZoom: true, // 最大缩放级别时是否展开聚合点
  disableClusteringAtZoom: 18, // 超过该缩放级别时禁用聚合
  maxClusterRadius: 50, // 聚合半径(像素)
  chunkedLoading: true, // 是否启用分块加载
  chunkInterval: 200, // 分块加载的时间间隔
  chunkDelay: 50, // 分块加载的延迟时间
  animate: true, // 是否启用动画效果
  animateAddingMarkers: true, // 是否启用添加标记时的动画
  removeOutsideVisibleBounds: true, // 是否移除可视区域外的标记
  spiderLegPolylineOptions: { weight: 1.5, color: '#222', opacity: 0.5 } // 蜘蛛线样式
})

2. 自定义聚合样式

css 复制代码
/* 自定义聚合点样式 */
.marker-cluster-small div {
  background-color: rgba(4, 241, 205, 0.7) !important;
  color: #fff;
}
.marker-cluster-medium div {
  background-color: rgba(0, 167, 254, 0.7) !important;
  color: #fff;
}
.marker-cluster-large div {
  background-color: rgba(254, 179, 0, 0.7) !important;
  color: #fff;
}

/* 自定义聚合点悬停效果 */
.marker-cluster-small:hover div {
  background-color: rgba(4, 241, 205, 0.9) !important;
}
.marker-cluster-medium:hover div {
  background-color: rgba(0, 167, 254, 0.9) !important;
}
.marker-cluster-large:hover div {
  background-color: rgba(254, 179, 0, 0.9) !important;
}

配置完以上的代码,可以看到效果图更新了,如下图所示:

五、Vue 项目中使用

1. 安装依赖

bash 复制代码
# 安装 Leaflet 和 Leaflet.markercluster
npm install leaflet leaflet.markercluster

# 安装类型定义文件,用于 TypeScript 项目
npm install @types/leaflet @types/leaflet.markercluster

2. 组件中引入

javascript 复制代码
import L from 'leaflet'
import 'leaflet/dist/leaflet.css'
import 'leaflet.markercluster/dist/MarkerCluster.css'
import 'leaflet.markercluster/dist/MarkerCluster.Default.css'
import 'leaflet.markercluster'

3. Vue 组件示例

vue 复制代码
<template>
  <div id="map" style="height: 500px;"></div>
</template>

<script>
export default {
  mounted() {
    this.initMap()
  },
  methods: {
    initMap() {
      // 地图初始化代码
      const map = L.map('map').setView([39.9042, 116.4074], 10)
      L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(
        map
      )

      const markers = L.markerClusterGroup()

      // 添加标记点数据
      // ...
      map.addLayer(markers)
    }
  }
}
</script>

六. 完整源码示例

注意:源码加载的地图图层是 OpenStreetMap,要使用 🪜 才能正确渲染地图!也可以自己修改为天地图图层。

html 复制代码
<script setup lang="ts">
  import { onMounted, onUnmounted } from 'vue'
  import L from 'leaflet'
  import 'leaflet.markercluster'
  import 'leaflet/dist/leaflet.css'
  import 'leaflet.markercluster/dist/MarkerCluster.css'
  import 'leaflet.markercluster/dist/MarkerCluster.Default.css'

  let map: L.Map | null = null

  // 随机点位
  const getRandomLatLng = (): L.LatLng => {
    if (!map) return L.latLng(0, 0)
    const bounds = map.getBounds()
    const southWest = bounds.getSouthWest()
    const northEast = bounds.getNorthEast()
    const lngSpan = northEast.lng - southWest.lng
    const latSpan = northEast.lat - southWest.lat

    return L.latLng(
      southWest.lat + latSpan * Math.random(),
      southWest.lng + lngSpan * Math.random()
    )
  }

  // 初始化地图
  const initMap = () => {
    const tiles = L.tileLayer(
      'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
      {
        maxZoom: 10,
        minZoom: 1,
        attribution:
          '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Points &copy 2012 LINZ'
      }
    )

    const latlng = L.latLng(39.904_53, 116.424_391)
    const baseLayers = L.layerGroup([tiles])

    map = L.map('map', { center: latlng, zoom: 3 })
    map.addLayer(baseLayers)

    const markers = L.markerClusterGroup({
      showCoverageOnHover: false,
      zoomToBoundsOnClick: true,
      spiderfyOnMaxZoom: true,
      disableClusteringAtZoom: 18,
      maxClusterRadius: 50,
      chunkedLoading: true,
      chunkInterval: 200,
      chunkDelay: 50,
      animate: true,
      animateAddingMarkers: true,
      removeOutsideVisibleBounds: true,
      spiderLegPolylineOptions: { weight: 1.5, color: '#222', opacity: 0.5 }
    })

    // 随机生成2000个点位
    for (let i = 0; i < 2000; i++) {
      const latlng = getRandomLatLng()
      const title = `第${i}个点`
      const marker = L.marker(latlng, { title })
      marker.bindPopup(title)
      markers.addLayer(marker)
    }

    map.addLayer(markers)
  }

  onMounted(() => {
    initMap()
  })

  onUnmounted(() => {
    if (map) {
      map.remove()
      map = null
    }
  })
</script>

<template>
  <div class="map-container">
    <div id="map"></div>
  </div>
</template>

<style scoped>
  .map-container {
    position: relative;
    width: 100%;
    height: 100%;
  }

  #map {
    width: 100%;
    height: 100%;
    overflow: hidden;
  }

  /* 自定义聚合点样式 */
  .marker-cluster-small div {
    background-color: rgba(4, 241, 205, 0.7) !important;
    color: #fff;
  }
  .marker-cluster-medium div {
    background-color: rgba(0, 167, 254, 0.7) !important;
    color: #fff;
  }
  .marker-cluster-large div {
    background-color: rgba(254, 179, 0, 0.7) !important;
    color: #fff;
  }

  /* 自定义聚合点悬停效果 */
  .marker-cluster-small:hover div {
    background-color: rgba(4, 241, 205, 0.9) !important;
  }
  .marker-cluster-medium:hover div {
    background-color: rgba(0, 167, 254, 0.9) !important;
  }
  .marker-cluster-large:hover div {
    background-color: rgba(254, 179, 0, 0.9) !important;
  }
</style>

七、性能优化建议

  1. 数据加载优化
  • 使用分块加载(chunkedLoading)
  • 设置合适的加载间隔(chunkInterval)和延迟(chunkDelay)
  • 考虑使用虚拟滚动或分页加载
  1. 渲染优化
  • 移除可视区域外的标记(removeOutsideVisibleBounds)
  • 使用合适的聚合半径(maxClusterRadius)
  • 设置合适的缩放级别(disableClusteringAtZoom)
  1. 内存管理
  • 及时清理不需要的标记点
  • 使用 WeakMap 存储标记点数据
  • 在组件卸载时清理地图实例
  1. 事件处理
  • 使用事件委托
  • 防抖和节流处理
  • 及时移除事件监听器

八、总结

Leaflet.markerclusterLeaflet 地图库的一个强大扩展,通过简单的配置即可实现标记点的聚类功能。它不仅提升了大量标记点场景下的地图性能,还通过动画效果和自定义样式增强了用户体验。无论是在原生 JS 项目还是 Vue 等框架中,都能方便地集成和使用。

Leaflet 作为一款轻量级、高性能的开源地图库,凭借其简洁的 API 和丰富的插件生态,在 Web 地图开发领域广受青睐。而 Leaflet.markercluster 插件则进一步扩展了 Leaflet 的能力,通过智能聚合算法将空间上邻近的标记点合并为聚类组(Cluster),并随着地图缩放级别动态调整聚合状态------缩放级别较低时展示聚合组概览,缩放级别提高后自动拆分显示单个标记点。

这种方式不仅有效解决了标记点密集重叠的问题,还通过平滑的动画过渡和可定制的聚合样式,提升地图交互的直观性。

九、参考资料

  1. Leaflet 官方文档
  2. Leaflet.markercluster 官方文档
  3. OpenStreetMap 官方文档
  4. Vue 3 官方文档
  5. TypeScript 官方文档
相关推荐
前端工作日常2 小时前
我理解的`npm pack` 和 `npm install <local-path>`
前端
李剑一2 小时前
说个多年老前端都不知道的标签正确玩法——q标签
前端
嘉小华3 小时前
大白话讲解 Android屏幕适配相关概念(dp、px 和 dpi)
前端
姑苏洛言3 小时前
在开发跑腿小程序集成地图时,遇到的坑,MapContext.includePoints(Object object)接口无效在组件中使用无效?
前端
奇舞精选3 小时前
Prompt 工程实用技巧:掌握高效 AI 交互核心
前端·openai
Danny_FD3 小时前
React中可有可无的优化-对象类型的使用
前端·javascript
用户757582318553 小时前
混合应用开发:企业降本增效之道——面向2025年移动应用开发趋势的实践路径
前端
P1erce3 小时前
记一次微信小程序分包经历
前端
LeeAt3 小时前
从Promise到async/await的逻辑演进
前端·javascript
等一个晴天丶3 小时前
不一样的 TypeScript 入门手册
前端