OpenLayers 实战|Vue3 + OpenLayers 完整工程案例(加载天地图、要素、弹窗、绘制、WFS)

前言

OpenLayers 是二维 WebGIS 开源天花板,专业 GIS 项目首选 ,对 OGC、多投影、WMS/WFS/WMTS 支持拉满,缺点是底层 API 多,上手门槛高。很多同学看完概念,一写代码就卡在图层、坐标、交互、跨域。

本文基于 Vue3 + Vite + OpenLayers 最新版,覆盖业务高频功能:

  • 地图初始化、加载天地图(CGCS2000 EPSG:4490)
  • 矢量图层、点线面要素渲染、自定义样式
  • Overlay 弹窗、点击要素查询属性
  • Draw 绘制、Modify 编辑、Select 选中
  • 加载 GeoJSON、对接 GeoServer WFS
  • 坐标转换、测距测面、点聚合
  • 常见坑:跨域、坐标系偏移、内存泄漏、图层管理

版本说明:openlayers@latest,vue3 + script setup,vite

一、安装依赖

复制代码
npm install ol

二、基础概念回顾(实战必懂)

  • Map:地图实例,容器,承载图层、交互、overlay
  • View:视图,中心点、缩放、投影、旋转
  • Layer:图层,Tile 瓦片图层 / Vector 矢量图层 / Image 图层
  • Source:数据源,XYZ、TileWMS、Vector、GeoJSON
  • Feature:矢量要素(点、线、面),带 geometry + 属性
  • Style:样式,填充、描边、图标、文字标注
  • Interaction:交互:Draw、Modify、Select、Translate
  • Overlay:覆盖物(DOM 弹窗、自定义 Marker,独立于图层)
  • proj :投影转换,fromLonLat、transform

⚠️ OpenLayers 内部地图坐标默认是投影坐标系,前端传进来的经纬度(EPSG:4326)必须转!

三、完整 Vue3 组件代码(直接复制)

复制代码
<template>
  <div class="map-container">
    <div id="map" class="map"></div>
    <!-- 弹窗DOM -->
    <div id="popup" class="ol-popup">
      <a href="#" id="popup-closer" class="ol-popup-closer">×</a>
      <div id="popup-content"></div>
    </div>
    <!-- 操作按钮 -->
    <div class="tool-bar">
      <button @click="startDraw('Point')">画点</button>
      <button @click="startDraw('LineString')">画线</button>
      <button @click="startDraw('Polygon')">画面</button>
      <button @click="activeModify">编辑</button>
      <button @click="clearDraw">清空绘制</button>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import Map from 'ol/Map'
import View from 'ol/View'
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer'
import { XYZ, Vector as VectorSource } from 'ol/source'
import { Style, Fill, Stroke, Circle as CircleStyle, Text } from 'ol/style'
import { Draw, Modify, Select } from 'ol/interaction'
import { Overlay } from 'ol'
import { fromLonLat, transform } from 'ol/proj'
import GeoJSON from 'ol/format/GeoJSON'
import 'ol/ol.css'

let map = ref(null)
let draw = null
let modify = null
let select = null
let overlay = null
// 矢量数据源,存放绘制/业务要素
const vectorSource = new VectorSource()
const vectorLayer = new VectorLayer({
  source: vectorSource,
  style: new Style({
    fill: new Fill({ color: 'rgba(0, 150, 255, 0.3)' }),
    stroke: new Stroke({ color: '#0096ff', width: 2 }),
    image: new CircleStyle({ radius: 6, fill: new Fill({ color: '#0096ff' }) })
  })
})

// 初始化地图
const initMap = () => {
  // 弹窗Overlay
  const popupDom = document.getElementById('popup')
  const popupContent = document.getElementById('popup-content')
  const popupCloser = document.getElementById('popup-closer')
  overlay = new Overlay({
    element: popupDom,
    autoPan: true,
    autoPanAnimation: { duration: 250 }
  })
  popupCloser.onclick = () => {
    overlay.setPosition(undefined)
    return false
  }

  map.value = new Map({
    target: 'map',
    overlays: [overlay],
    layers: [
      // 天地图矢量底图(需要自己去天地图申请key)
      new TileLayer({
        source: new XYZ({
          url: 'https://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=你的天地图key',
          crossOrigin: 'anonymous'
        })
      }),
      vectorLayer
    ],
    view: new View({
      center: fromLonLat([108.95, 34.27]), // 西安经纬度转3857
      zoom: 10,
      projection: 'EPSG:3857'
    })
  })

  // 点击要素查询 + 弹窗
  map.value.on('singleclick', (evt) => {
    const feature = map.value.forEachFeatureAtPixel(evt.pixel, f => f)
    if (feature) {
      const props = feature.getProperties()
      popupContent.innerHTML = `
        <div>名称:${props.name || '无'}</div>
        <div>编码:${props.code || '无'}</div>
      `
      overlay.setPosition(evt.coordinate)
    } else {
      overlay.setPosition(undefined)
    }
  })
}

// 开启绘制
const startDraw = (type) => {
  removeInter()
  draw = new Draw({ source: vectorSource, type })
  draw.on('drawend', (e) => {
    // 绘制结束,可以拿到feature,传给后端保存
    const feature = e.feature
    feature.setProperties({ name: '新建要素' })
  })
  map.value.addInteraction(draw)
}

// 开启编辑
const activeModify = () => {
  removeInter()
  modify = new Modify({ source: vectorSource })
  map.value.addInteraction(modify)
}

// 清空绘制交互
const removeInter = () => {
  if (draw) map.value.removeInteraction(draw)
  if (modify) map.value.removeInteraction(modify)
}
const clearDraw = () => {
  removeInter()
  vectorSource.clear()
}

// 加载GeoJSON示例
const loadGeoJson = () => {
  const geojson = {
    type: 'FeatureCollection',
    features: [
      {
        type: 'Feature',
        properties: { name: '测试点位' },
        geometry: { type: 'Point', coordinates: [108.95, 34.27] }
      }
    ]
  }
  const features = new GeoJSON().readFeatures(geojson, {
    dataProjection: 'EPSG:4326',
    featureProjection: 'EPSG:3857'
  })
  vectorSource.addFeatures(features)
}

onMounted(() => {
  initMap()
  // loadGeoJson()
})
onUnmounted(() => {
  map.value?.dispose()
})
</script>

<style scoped>
.map-container { width: 100%; height: 700px; position: relative; }
.map { width: 100%; height: 100%; }
.tool-bar { position: absolute; top:10px; left:10px; z-index:99; }
button { margin: 0 4px; padding:4px 8px; }
</style>
<style>
.ol-popup {
  position: absolute;
  background-color: white;
  padding:12px;
  border-radius: 4px;
  border:1px solid #ccc;
  bottom:12px;
  min-width:160px;
}
.ol-popup-closer {
  position:absolute;
  top:4px;
  right:6px;
  text-decoration:none;
  color:#666;
}
</style>

四、高频功能拆解

1. 坐标转换(重中之重)

OpenLayers 地图视图默认 EPSG:3857(Web 墨卡托,米单位)

  • fromLonLat([lng, lat]):4326 → 3857(最常用)
  • toLonLat(coord):3857 → 4326(拿到经纬度传给后端)
  • transform(coord, 'EPSG:4490','EPSG:3857'):CGCS2000 转墨卡托

政务项目天地图矢量是 CGCS2000,底图和数据投影必须统一,否则偏移!

2. 加载 GeoServer WFS(获取后端矢量)

复制代码
const wfsSource = new VectorSource({
  url: (extent) => {
    return `http://localhost:8080/geoserver/wfs?service=WFS&version=1.1.0&request=GetFeature&typeNames=workspace:layerName&outputFormat=application/json&bbox=${extent.join(',')},EPSG:3857`
  },
  format: new GeoJSON(),
  strategy: bboxStrategy
})

注意:GeoServer 必须配置 CORS,否则前端跨域报错

3. 自定义图标 Marker、文字标注

复制代码
const iconStyle = new Style({
  image: new Icon({ src: '/marker.png', scale: 0.6 }),
  text: new Text({
    text: '站点A',
    offsetY: -22,
    fill: new Fill({ color: '#000' }),
    stroke: new Stroke({ color: '#fff', width:2 })
  })
})
feature.setStyle(iconStyle)

4. 测距、测面积(使用 ol-ext,项目里非常常用)

原生 OpenLayers 没有量算控件,推荐 ol-ext

复制代码
npm install ol-ext

ol-ext 自带 Measure、卷帘、图例、右键菜单,国内 OL 项目标配。

5. 点聚合(上万点位防卡顿)

使用ol/source/Cluster,把距离近的点合并成聚合圆,放大再散开,大屏、监控点位必备。

五、图层管理最佳实践(工程化)

项目里图层多(管线、地块、行政区、监测点),建议:

  1. 每个业务图层单独一个 VectorLayer + VectorSource,不要全部塞一个图层
  2. 给 layer 设置setVisible(true/false)控制显隐,不要频繁 add/removeLayer
  3. 图层分层级:底图层 → 行政区 → 业务矢量 → 标注层
  4. 大数据不要一次性 addFeatures,用 WFS 分页、bbox 策略(视口内加载)

六、OpenLayers 踩坑清单(血泪总结)

  1. 坐标系不统一:后端返回 4326,地图 3857,不转换直接渲染 → 要素飞到非洲
  2. 跨域 CORS:GeoServer、瓦片服务、WFS 没有配置跨域,请求被拦截
  3. 要素一次性加载太多:几千条 GeoJSON 直接 add,页面卡顿,用 BBox 按需加载
  4. 内存泄漏:页面销毁时没有 map.dispose ()、交互没有 remove,路由切换内存暴涨
  5. 天地图 key 过期 / 配额超限:底图白屏;内网项目改用离线 WMTS 瓦片
  6. Draw 交互重复添加:多次点击绘制按钮,多个 draw 交互叠加,点一下画多个要素
  7. 弹窗 Overlay 和 Feature 弹窗混淆:Overlay 是 DOM,Feature 的 style 是矢量文字,性能差异很大
  8. 矢量瓦片:OpenLayers 原生对 Mapbox Vector Tile(pbf)支持弱,复杂矢量瓦片场景优先 MapLibre

七、OpenLayers vs Mars2D 怎么选

  • 如果你需要高度定制、对接 WMS/WFS、底层逻辑自己写、国产化政务项目 → OpenLayers
  • 如果你不想写底层封装、二维三维一套代码、快速交付业务 → Mars2D(基于 OL+Cesium 封装)

Mars2D 底层就是 OpenLayers,学会 OL,看 Mars2D 源码非常轻松

八、学习路线 & 资源

  1. 官方示例:https://openlayers.org/en/latest/examples/(重点看 Draw、Popup、WFS、GeoJSON)
  2. ol-ext:扩展控件库,量算、卷帘、图例、打印
  3. QGIS:预处理 shp、配图、发布 GeoServer 服务
  4. Turf.js:前端空间计算(缓冲区、相交),小数据量使用
  5. PostGIS:大数据空间查询交给后端

九、小结

OpenLayers 的核心不是记住一堆 API,而是图层 - 数据源 - 要素 - 样式 - 交互 这套模型。

它是 WebGIS 基本功,学会 OL,再去看 Mars2D、MapLibre 都会非常轻松。

适合水利、自然资源、市政管线、应急这类传统 GIS 项目。

相关推荐
陈随易43 分钟前
下载神器!Motrix 停更三年后重写回归
前端·后端·程序员
前端的日常1 小时前
一个人+AI做情侣食谱小程序,0 费用的服务器
前端·后端
计算机魔术师2 小时前
谷歌搜索广告踩刹车,AI广告成为救命稻草
前端
总要学点什么2 小时前
第81篇:数据可视化-PM2.5分布——让圆点的胖瘦替每座城市报出浓度
前端·信息可视化·echarts·cesium·空间分析
IT_陈寒2 小时前
Vue的v-for里用了个临时变量,页面直接挂了
前端·人工智能·后端
hunter_0762 小时前
一次 iOS 真机签名 409 的完整排查:证书、设备、描述文件为什么总是"对不上"
前端
枣把儿2 小时前
开源!公众号排版+一键发布工具懒人版
前端
2501_916007472 小时前
抓包工具功能对比,TraceEagle、Charles、Fiddler、Wireshark、mitmproxy 差别
前端·网络协议·ios·adb·https·fiddler·wireshark
Flynt2 小时前
AI 写的页面一眼假?有人把"假指纹"写成 62 条规则,不用大模型就能跑
前端·ai编程·claude