OpenLayers + Vue 2 使用指南(01)

参考资料


一、环境搭建

1.2 安装 OpenLayers 依赖

在已有的 Vue 2 项目中,通过 npm 安装 OpenLayers:

bash 复制代码
npm install ol

安装完成后,可在组件中按需引入:

js 复制代码
import Map from 'ol/Map'
import View from 'ol/View'
import TileLayer from 'ol/layer/Tile'
import OSM from 'ol/source/OSM'

提示:OpenLayers 从 v6 开始使用 ES Module,支持 Tree-shaking,按需引入可减小打包体积。


二、基础集成

2.1 在 Vue 组件中初始化地图

创建一个地图组件 MapContainer.vue

vue 复制代码
<template>
  <div id="map" ref="mapRoot" class="map-container"></div>
</template>

<script>
import Map from 'ol/Map'
import View from 'ol/View'
import TileLayer from 'ol/layer/Tile'
import OSM from 'ol/source/OSM'

export default {
  name: 'MapContainer',
  data() {
    return {
      map: null
    }
  },
  mounted() {
    this.initMap()
  },
  methods: {
    initMap() {
      this.map = new Map({
        target: this.$refs.mapRoot,
        layers: [
          new TileLayer({
            source: new OSM()
          })
        ],
        view: new View({
          center: [116.397428, 39.90923], // 北京 [经度, 纬度]
          zoom: 12,
          projection: 'EPSG:4326'
        })
      })
    }
  }
}
</script>

<style scoped>
.map-container {
  width: 100%;
  height: 600px;
}
</style>

2.2 地图基本配置

配置项 说明 示例值
center 地图中心点坐标 [116.397428, 39.90923]
zoom 缩放级别 12
projection 坐标投影 'EPSG:4326''EPSG:3857'
minZoom / maxZoom 缩放范围限制 3 / 18
rotation 地图旋转角度(弧度) 0

坐标系说明

  • EPSG:4326:WGS84 地理坐标系,经纬度直接使用,适合与后端数据对接
  • EPSG:3857:Web 墨卡托投影,Web 地图默认投影,OSM 瓦片基于此投影

注意 :如果使用 EPSG:4326 作为视图投影,OpenLayers 会自动进行内部转换。但在叠加矢量数据时,需确保数据坐标系与视图一致,或通过 transform 方法转换。

2.3 地图生命周期管理

在 Vue 组件销毁时必须释放地图资源,防止内存泄漏:

vue 复制代码
<script>
import Map from 'ol/Map'

export default {
  name: 'MapContainer',
  data() {
    return {
      map: null
    }
  },
  mounted() {
    this.initMap()
  },
  beforeDestroy() {
    // 释放地图资源
    if (this.map) {
      this.map.setTarget(null)
      this.map = null
    }
  },
  methods: {
    initMap() {
      this.map = new Map({
        target: this.$refs.mapRoot,
        // ... 其他配置
      })
    }
  }
}
</script>

关键点

  • setTarget(null) 断开地图与 DOM 容器的绑定
  • map 引用设为 null,便于 GC 回收
  • 不执行清理操作会导致地图实例和事件监听器无法被回收,反复进出页面会造成内存持续增长

三、图层管理

3.1 矢量图层(Vector Layer)

矢量图层用于展示点、线、面等矢量数据:

js 复制代码
import VectorLayer from 'ol/layer/Vector'
import VectorSource from 'ol/source/Vector'
import Feature from 'ol/Feature'
import Point from 'ol/geom/Point'
import { fromLonLat } from 'ol/proj'

// 创建矢量数据源
const vectorSource = new VectorSource()

// 添加一个点要素
const feature = new Feature({
  geometry: new Point(fromLonLat([116.397428, 39.90923])),
  name: '北京'
})

vectorSource.addFeature(feature)

// 创建矢量图层
const vectorLayer = new VectorLayer({
  source: vectorSource
})

this.map.addLayer(vectorLayer)

加载 GeoJSON 数据

js 复制代码
import GeoJSON from 'ol/format/GeoJSON'
import VectorLayer from 'ol/layer/Vector'
import VectorSource from 'ol/source/Vector'

const geojsonSource = new VectorSource({
  format: new GeoJSON(),
  url: '/data/area.geojson', // GeoJSON 文件路径或接口地址
  // 也可以直接传入数据
  // features: new GeoJSON().readFeatures(geojsonData, {
  //   featureProjection: 'EPSG:3857'
  // })
})

const geojsonLayer = new VectorLayer({
  source: geojsonSource,
  style: {
    fill: { color: 'rgba(66, 133, 244, 0.2)' },
    stroke: { color: '#4285f4', width: 2 }
  }
})

this.map.addLayer(geojsonLayer)

3.2 栅格图层(Tile Layer)

除了默认的 OSM 瓦片,还可以接入其他地图服务:

天地图

js 复制代码
import TileLayer from 'ol/layer/Tile'
import XYZ from 'ol/source/XYZ'

const tiandituLayer = new TileLayer({
  source: new XYZ({
    url: 'http://t{s}.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=你的天地图密钥',
    subdomains: ['0', '1', '2', '3', '4', '5', '6', '7']
  })
})

this.map.addLayer(tiandituLayer)

高德地图

js 复制代码
const gaodeLayer = new TileLayer({
  source: new XYZ({
    url: 'http://webr{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}',
    subdomains: ['1', '2', '3', '4']
  })
})

this.map.addLayer(gaodeLayer)

3.3 图层切换与控制

js 复制代码
// 保存图层引用以便切换
const layers = {
  osm: new TileLayer({ source: new OSM(), visible: true }),
  gaode: new TileLayer({ source: gaodeSource, visible: false })
}

this.map.addLayer(layers.osm)
this.map.addLayer(layers.gaode)

// 切换到高德图层
switchLayer(name) {
  Object.keys(layers).forEach(key => {
    layers[key].setVisible(key === name)
  })
}

四、覆盖物与标记

4.1 添加点标记(Marker)

使用 ol/Overlay 实现自定义 DOM 标记:

vue 复制代码
<template>
  <div id="map" ref="mapRoot" class="map-container"></div>
</template>

<script>
import Overlay from 'ol/Overlay'

export default {
  mounted() {
    this.initMap()
    this.addMarker(116.397428, 39.90923, '北京')
  },
  methods: {
    addMarker(lon, lat, title) {
      // 创建 DOM 元素
      const el = document.createElement('div')
      el.className = 'marker'
      el.title = title
      el.innerHTML = '<div class="marker-icon">📍</div>'

      const marker = new Overlay({
        position: fromLonLat([lon, lat]),
        positioning: 'center-center',
        element: el,
        stopEvent: false
      })

      this.map.addOverlay(marker)
    }
  }
}
</script>

<style>
.marker {
  position: absolute;
  cursor: pointer;
}
.marker-icon {
  font-size: 24px;
  transform: translate(-50%, -100%);
}
</style>

使用自定义图片标记

vue 复制代码
<template>
  <div id="map" ref="mapRoot" class="map-container"></div>
</template>

<script>
import Overlay from 'ol/Overlay'

export default {
  mounted() {
    this.initMap()
  },
  methods: {
    addImageMarker(lon, lat, iconUrl) {
      const el = document.createElement('img')
      el.src = iconUrl
      el.style.width = '32px'
      el.style.height = '32px'
      el.style.transform = 'translate(-50%, -100%)'

      const marker = new Overlay({
        position: fromLonLat([lon, lat]),
        positioning: 'center-center',
        element: el,
        stopEvent: false
      })

      this.map.addOverlay(marker)
      return marker
    }
  }
}
</script>

4.2 信息弹窗(Overlay)

点击标记弹出信息窗口:

vue 复制代码
<template>
  <div id="map" ref="mapRoot" class="map-container">
    <!-- 弹窗容器 -->
    <div ref="popup" class="popup">
      <div class="popup-closer" @click="closePopup">&times;</div>
      <div ref="popupContent" class="popup-content"></div>
    </div>
  </div>
</template>

<script>
import Overlay from 'ol/Overlay'

export default {
  data() {
    return {
      popupOverlay: null
    }
  },
  mounted() {
    this.initMap()
    this.initPopup()
  },
  methods: {
    initPopup() {
      this.popupOverlay = new Overlay({
        element: this.$refs.popup,
        autoPan: true,
        autoPanAnimation: { duration: 250 }
      })
      this.map.addOverlay(this.popupOverlay)

      // 点击要素时显示弹窗
      this.map.on('singleclick', (evt) => {
        const feature = this.map.forEachFeatureAtPixel(evt.pixel, f => f)
        if (feature) {
          const coordinate = evt.coordinate
          const name = feature.get('name')
          this.$refs.popupContent.innerHTML = `<p>${name}</p>`
          this.popupOverlay.setPosition(coordinate)
        } else {
          this.closePopup()
        }
      })
    },
    closePopup() {
      this.popupOverlay.setPosition(undefined)
    }
  }
}
</script>

<style>
.popup {
  position: absolute;
  background: white;
  border-radius: 4px;
  padding: 10px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
  min-width: 150px;
}
.popup-closer {
  position: absolute;
  right: 5px;
  top: 2px;
  cursor: pointer;
  font-size: 18px;
  color: #999;
}
.popup-content {
  padding-top: 5px;
}
</style>

4.3 批量渲染与性能优化

当需要渲染大量标记时(数百甚至数千个),直接创建 Overlay 会导致性能问题。推荐使用 Canvas 渲染的矢量图层

js 复制代码
import VectorLayer from 'ol/layer/Vector'
import VectorSource from 'ol/source/Vector'
import Feature from 'ol/Feature'
import Point from 'ol/geom/Point'
import { fromLonLat } from 'ol/proj'
import { Style, Icon } from 'ol/style'

// 批量创建要素
function batchCreateFeatures(points) {
  return points.map(p => {
    const feature = new Feature({
      geometry: new Point(fromLonLat([p.lng, p.lat])),
      name: p.name
    })
    feature.setStyle(new Style({
      icon: new Icon({
        src: '/marker.png',
        scale: 0.5,
        anchor: [0.5, 1]
      })
    }))
    return feature
  })
}

// 使用 Canvas 渲染模式(默认即为 Canvas)
const clusterLayer = new VectorLayer({
  source: new VectorSource({
    features: batchCreateFeatures(largeDataArray)
  }),
  // 开启渲染缓冲,减少重绘频率
  renderBuffer: 100,
  // 可见性检测,超出视口不渲染
  declutter: true
})

this.map.addLayer(clusterLayer)

性能优化要点

  • 数据量超过 500 时,避免使用 DOM Overlay,改用 Canvas 矢量图层
  • 启用 declutter: true 可在密集区域自动隐藏重叠标签
  • 大量数据可考虑使用 聚合图层ol/source/Cluster)进行聚合展示

五、交互功能

5.1 点击获取坐标

js 复制代码
import { toLonLat } from 'ol/proj'

this.map.on('singleclick', (evt) => {
  // 获取地理坐标(EPSG:4326)
  const coord = toLonLat(evt.coordinate)
  const lng = coord[0].toFixed(6)
  const lat = coord[1].toFixed(6)
  console.log(`坐标:经度 ${lng}, 纬度 ${lat}`)
})

获取像素坐标

js 复制代码
this.map.on('pointermove', (evt) => {
  if (evt.dragging) return
  const pixel = this.map.getEventPixel(evt.originalEvent)
  const coordinate = this.map.getCoordinateFromPixel(pixel)
  console.log('像素坐标:', pixel, '地理坐标:', coordinate)
})

5.2 绘制图形(点、线、面)

js 复制代码
import Draw from 'ol/interaction/Draw'
import VectorSource from 'ol/source/Vector'
import VectorLayer from 'ol/layer/Vector'

// 创建绘制图层
const drawSource = new VectorSource()
const drawLayer = new VectorLayer({
  source: drawSource,
  style: {
    fill: { color: 'rgba(255, 0, 0, 0.2)' },
    stroke: { color: '#ff0000', width: 2 }
  }
})
this.map.addLayer(drawLayer)

// 添加绘制交互
let drawInteraction = null

startDraw(type) {
  // 先移除之前的绘制交互
  if (drawInteraction) {
    this.map.removeInteraction(drawInteraction)
  }

  drawInteraction = new Draw({
    source: drawSource,
    type: type // 'Point', 'LineString', 'Polygon', 'Circle', 'MultiPoint'
  })

  this.map.addInteraction(drawInteraction)
}

// 调用示例
// this.startDraw('Polygon')

绘制完成事件

js 复制代码
drawInteraction.on('drawend', (evt) => {
  const feature = evt.feature
  const geometry = feature.getGeometry()

  // 获取绘制结果的坐标
  const coords = geometry.getCoordinates()
  console.log('绘制完成:', coords)

  // 获取面积(面要素)
  if (geometry.getType() === 'Polygon') {
    const area = geometry.getArea()
    console.log(`面积:${(area / 1000000).toFixed(2)} km²`)
  }
})

5.3 测距与测面

js 复制代码
import { getDistance, getArea } from 'ol/sphere'
import { fromLonLat } from 'ol/proj'

// 计算两点间距离
function calcDistance(coord1, coord2) {
  // coord 为 EPSG:4326 坐标
  const c1 = toLonLat(coord1)
  const c2 = toLonLat(coord2)
  const distance = getDistance(c1, c2) // 返回米
  return distance
}

// 计算多边形面积
function calcArea(polygonCoords) {
  const coords = polygonCoords.map(c => toLonLat(c))
  const area = getArea({ type: 'Polygon', coordinates: [coords] })
  return area // 返回平方米
}

// 格式化距离显示
function formatDistance(meters) {
  if (meters >= 1000) {
    return (meters / 1000).toFixed(2) + ' km'
  }
  return meters.toFixed(2) + ' m'
}

结合绘制实现测距

js 复制代码
startMeasure(type = 'distance') {
  if (drawInteraction) {
    this.map.removeInteraction(drawInteraction)
  }

  const drawType = type === 'distance' ? 'LineString' : 'Polygon'
  drawInteraction = new Draw({
    source: drawSource,
    type: drawType,
    // 允许继续绘制
    freehand: false
  })

  this.map.addInteraction(drawInteraction)

  drawInteraction.on('drawend', (evt) => {
    const geometry = evt.feature.getGeometry()
    let result = ''
    if (type === 'distance') {
      const length = geometry.getLength()
      result = formatDistance(length)
    } else {
      const area = geometry.getArea()
      result = (area / 1000000).toFixed(2) + ' km²'
    }
    console.log(`测量结果:${result}`)
  })
}

六、常见问题与解决方案

6.1 地图容器大小问题

问题:地图不显示或只显示一部分。

原因:地图容器未设置明确的宽高。

解决方案

css 复制代码
/* 确保地图容器有明确的宽高 */
.map-container {
  width: 100%;
  height: 600px; /* 必须有明确高度 */
}

/* 避免使用 display:none,会导致地图计算尺寸为 0 */
/* 如需隐藏,使用 visibility:hidden 或先销毁再重建 */

/* 动态容器场景下,手动更新地图尺寸 */
this.map.updateSize()

6.2 内存泄漏排查

常见原因

  • beforeDestroy 中未调用 map.setTarget(null)
  • 事件监听器未在销毁时移除
  • Overlay 引用未释放

排查方法

js 复制代码
// Chrome DevTools → Memory → Take Heap Snapshot
// 对比两次快照,观察 Map、Overlay 对象是否持续增长

// 在 beforeDestroy 中完整清理
beforeDestroy() {
  // 移除所有事件监听
  this.map.un('singleclick', this.handleClick)
  
  // 移除所有 Overlay
  const overlays = this.map.getOverlays().getArray()
  overlays.forEach(overlay => this.map.removeOverlay(overlay))
  
  // 清空矢量图层数据
  if (this.vectorSource) {
    this.vectorSource.clear()
  }

  // 释放地图
  this.map.setTarget(null)
  this.map = null
}

6.3 矢量数据加载性能

问题:加载大量 GeoJSON 数据时页面卡顿。

解决方案

js 复制代码
// 1. 使用 Web Worker 解析大型 GeoJSON
// geojson-worker.js
import GeoJSON from 'ol/format/GeoJSON'

self.onmessage = function(e) {
  const format = new GeoJSON()
  const features = format.readFeatures(e.data.geojson, {
    featureProjection: 'EPSG:3857'
  })
  self.postMessage({ features: features })
}

// 2. 分片加载
async function loadGeoJSONInChunks(url, chunkSize = 500) {
  const response = await fetch(url)
  const data = await response.json()
  const features = new GeoJSON().readFeatures(data, {
    featureProjection: 'EPSG:3857'
  })

  for (let i = 0; i < features.length; i += chunkSize) {
    const chunk = features.slice(i, i + chunkSize)
    vectorSource.addFeatures(chunk)
  }
}

// 3. 使用 BBOX 策略按视口加载
const vectorSource = new VectorSource({
  format: new GeoJSON(),
  url: (extent) => {
    return `/api/geodata?bbox=${extent.join(',')}`
  },
  strategy: bboxStrategy // 按视口范围请求
})

七、总结与参考资料

总结

章节 核心要点
环境搭建 npm 安装 ol,按需引入模块
基础集成 new Map() + target 绑定 DOM,beforeDestroy 释放资源
图层管理 TileLayer 管瓦片底图,VectorLayer 管矢量数据
覆盖物 DOM Overlay 做自定义标记,Canvas 矢量做批量渲染
交互功能 Draw 交互实现绘制,ol/sphere 实现测距测面
常见问题 容器必须设宽高,销毁时必须清理,大数据用分片/Worker
相关推荐
暖和_白开水1 小时前
数据分析agent (七):contextvars 模块上下文request_id
java·前端·数据分析
程序员黑豆2 小时前
鸿蒙应用开发:Flex 组件从入门到实战
前端·华为·harmonyos
大爱编程♡2 小时前
Vue3+TypeScript+element-plus的文章管理项目(配后端接口)-退出及文章管理页面
前端·javascript·typescript
先吃饱再说2 小时前
React 组件设计:从 Props 传递到组件封装
前端·react.js
独立开阀者_FwtCoder2 小时前
最近做了一个健身小程序:智形健身助手,健身的佬们来提点意见
前端·javascript·github
达子6662 小时前
第8章_HarmonyOs开发图解 剪贴板
vue.js
舒灿2 小时前
喵ing Tab 1.0.0 正式发布
前端·ai编程
葡萄城技术团队2 小时前
当表格数据来自外部系统:SpreadJS 如何让异步公式一键刷新
前端
小徐_23332 小时前
Open Wot 1.0.5 发布:让 AI 接入 wot-ui,只需要两条命令
前端·uni-app·ai编程