参考资料
一、环境搭建
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">×</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 |