前言
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,把距离近的点合并成聚合圆,放大再散开,大屏、监控点位必备。
五、图层管理最佳实践(工程化)
项目里图层多(管线、地块、行政区、监测点),建议:
- 每个业务图层单独一个 VectorLayer + VectorSource,不要全部塞一个图层
- 给 layer 设置
setVisible(true/false)控制显隐,不要频繁 add/removeLayer - 图层分层级:底图层 → 行政区 → 业务矢量 → 标注层
- 大数据不要一次性 addFeatures,用 WFS 分页、bbox 策略(视口内加载)
六、OpenLayers 踩坑清单(血泪总结)
- 坐标系不统一:后端返回 4326,地图 3857,不转换直接渲染 → 要素飞到非洲
- 跨域 CORS:GeoServer、瓦片服务、WFS 没有配置跨域,请求被拦截
- 要素一次性加载太多:几千条 GeoJSON 直接 add,页面卡顿,用 BBox 按需加载
- 内存泄漏:页面销毁时没有 map.dispose ()、交互没有 remove,路由切换内存暴涨
- 天地图 key 过期 / 配额超限:底图白屏;内网项目改用离线 WMTS 瓦片
- Draw 交互重复添加:多次点击绘制按钮,多个 draw 交互叠加,点一下画多个要素
- 弹窗 Overlay 和 Feature 弹窗混淆:Overlay 是 DOM,Feature 的 style 是矢量文字,性能差异很大
- 矢量瓦片:OpenLayers 原生对 Mapbox Vector Tile(pbf)支持弱,复杂矢量瓦片场景优先 MapLibre
七、OpenLayers vs Mars2D 怎么选
- 如果你需要高度定制、对接 WMS/WFS、底层逻辑自己写、国产化政务项目 → OpenLayers
- 如果你不想写底层封装、二维三维一套代码、快速交付业务 → Mars2D(基于 OL+Cesium 封装)
Mars2D 底层就是 OpenLayers,学会 OL,看 Mars2D 源码非常轻松
八、学习路线 & 资源
- 官方示例:https://openlayers.org/en/latest/examples/(重点看 Draw、Popup、WFS、GeoJSON)
- ol-ext:扩展控件库,量算、卷帘、图例、打印
- QGIS:预处理 shp、配图、发布 GeoServer 服务
- Turf.js:前端空间计算(缓冲区、相交),小数据量使用
- PostGIS:大数据空间查询交给后端
九、小结
OpenLayers 的核心不是记住一堆 API,而是图层 - 数据源 - 要素 - 样式 - 交互 这套模型。
它是 WebGIS 基本功,学会 OL,再去看 Mars2D、MapLibre 都会非常轻松。
适合水利、自然资源、市政管线、应急这类传统 GIS 项目。