📌 学习目标
- 掌握悬停显示弹窗的实现方法
- 理解相关API的使用
- 能够独立完成类似功能开发
🎯 核心概念
悬停在要素上时显示弹窗。
💻 完 整 代 码
<!DOCTYPE html>
<html lang="en">
<head>
<title>Display a popup on hover</title>
<meta property="og:description" content="当用户悬停在自定义标记上时,显示包含更多信息的弹出框。" />
<meta property="og:created" content="2006-06-25" />
<meta charset='utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel='stylesheet' href='https://unpkg.com/maplibre-gl@5.24.0/dist/maplibre-gl.css' />
<script src='https://unpkg.com/maplibre-gl@5.24.0/dist/maplibre-gl.js'></script>
<style>
body { margin: 0; padding: 0; }
html, body, #map { height: 100%; }
</style>
</head>
<body>
<style>
.maplibregl-popup {
max-width: 400px;
font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
}
</style>
<div id="map"></div>
<script>
const map = new maplibregl.Map({
container: 'map',
style: 'https://tiles.openfreemap.org/styles/bright',
center: [-77.04, 38.907],
zoom: 11.15
});
map.on('load', async () => {
const image = await map.loadImage('https://maplibre.org/maplibre-gl-js/docs/assets/custom_marker.png');
// 添加一个图像作为自定义标记
map.addImage('custom-marker', image.data);
map.addSource('places', {
'type': 'geojson',
'data': {
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'properties': {
'description':
'<strong>Make it Mount Pleasant</strong><p>Make it Mount Pleasant is a handmade and vintage market and afternoon of live entertainment and kids activities. 12:00-6:00 p.m.</p>'
},
'geometry': {
'type': 'Point',
'coordinates': [-77.038659, 38.931567]
}
},
{
'type': 'Feature',
'properties': {
'description':
'<strong>Mad Men Season Five Finale Watch Party</strong><p>Head to Lounge 201 (201 Massachusetts Avenue NE) Sunday for a Mad Men Season Five Finale Watch Party, complete with 60s costume contest, Mad Men trivia, and retro food and drink. 8:00-11:00 p.m. $10 general admission, $20 admission and two hour open bar.</p>'
},
'geometry': {
'type': 'Point',
'coordinates': [-77.003168, 38.894651]
}
},
{
'type': 'Feature',
'properties': {
'description':
'<strong>Big Backyard Beach Bash and Wine Fest</strong><p>EatBar (2761 Washington Boulevard Arlington VA) is throwing a Big Backyard Beach Bash and Wine Fest on Saturday, serving up conch fritters, fish tacos and crab sliders, and Red Apron hot dogs. 12:00-3:00 p.m. $25.</p>'
},
'geometry': {
'type': 'Point',
'coordinates': [-77.090372, 38.881189]
}
},
{
'type': 'Feature',
'properties': {
'description':
'<strong>Ballston Arts & Crafts Market</strong><p>The Ballston Arts & Crafts Market sets up shop next to the Ballston metro this Saturday for the first of five dates this summer. Nearly 35 artists and crafters will be on hand selling their wares. 10:00-4:00 p.m.</p>'
},
'geometry': {
'type': 'Point',
'coordinates': [-77.111561, 38.882342]
}
},
{
'type': 'Feature',
'properties': {
'description':
'<strong>Seersucker Bike Ride and Social</strong><p>Feeling dandy? Get fancy, grab your bike, and take part in this year\'s Seersucker Social bike ride from Dandies and Quaintrelles. After the ride enjoy a lawn party at Hillwood with jazz, cocktails, paper hat-making, and more. 11:00-7:00 p.m.</p>'
},
'geometry': {
'type': 'Point',
'coordinates': [-77.052477, 38.943951]
}
},
{
'type': 'Feature',
'properties': {
'description':
'<strong>Capital Pride Parade</strong><p>The annual Capital Pride Parade makes its way through Dupont this Saturday. 4:30 p.m. Free.</p>'
},
'geometry': {
'type': 'Point',
'coordinates': [-77.043444, 38.909664]
}
},
{
'type': 'Feature',
'properties': {
'description':
'<strong>Muhsinah</strong><p>Jazz-influenced hip hop artist Muhsinah plays the Black Cat (1811 14th Street NW) tonight with Exit Clov and Gods’illa. 9:00 p.m. $12.</p>'
},
'geometry': {
'type': 'Point',
'coordinates': [-77.031706, 38.914581]
}
},
{
'type': 'Feature',
'properties': {
'description':
'<strong>A Little Night Music</strong><p>The Arlington Players\' production of Stephen Sondheim\'s <em>A Little Night Music</em> comes to the Kogod Cradle at The Mead Center for American Theater (1101 6th Street SW) this weekend and next. 8:00 p.m.</p>'
},
'geometry': {
'type': 'Point',
'coordinates': [-77.020945, 38.878241]
}
},
{
'type': 'Feature',
'properties': {
'description':
'<strong>Truckeroo</strong><p>Truckeroo brings dozens of food trucks, live music, and games to half and M Street SE (across from Navy Yard Metro Station) today from 11:00 a.m. to 11:00 p.m.</p>'
},
'geometry': {
'type': 'Point',
'coordinates': [-77.007481, 38.876516]
}
}
]
}
});
// 添加一个显示地点的图层。
map.addLayer({
'id': 'places',
'type': 'symbol',
'source': 'places',
'layout': {
'icon-image': 'custom-marker',
'icon-overlap': 'always'
}
});
// 创建一个弹窗,但不将其添加到地图中。
const popup = new maplibregl.Popup({
closeButton: false,
closeOnClick: false
});
// 确保检测重叠标记的标记变化
// 并使用mousemove而不是mouseenter事件
let currentFeatureCoordinates = undefined;
map.on('mousemove', 'places', (e) => {
const featureCoordinates = e.features[0].geometry.coordinates.toString();
if (currentFeatureCoordinates !== featureCoordinates) {
currentFeatureCoordinates = featureCoordinates;
// 更改光标样式作为UI指示器。
map.getCanvas().style.cursor = 'pointer';
const coordinates = e.features[0].geometry.coordinates.slice();
const description = e.features[0].properties.description;
// 确保如果地图缩小到使要素的多个
// 副本可见,弹窗会出现在
// 被指向的副本上方。
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
}
// 填充弹窗并设置其坐标,基于找到的特征。
popup.setLngLat(coordinates).setHTML(description).addTo(map);
}
});
map.on('mouseleave', 'places', () => {
currentFeatureCoordinates = undefined;
map.getCanvas().style.cursor = '';
popup.remove();
});
});
</script>
</body>
</html>
🔍 代码解析
1. 加载自定义标记图像
与lesson-066不同,本课时使用自定义PNG图像作为标记。通过 map.loadImage() 加载图像,然后使用 map.addImage() 添加到地图,命名为 ‘custom-marker’。
2. 创建弹窗实例
创建一个弹窗实例并配置选项:
closeButton: false- 不显示关闭按钮closeOnClick: false- 点击地图时不自动关闭
这样弹窗会保持显示直到鼠标离开。
3. 悬停事件处理
使用 mousemove 事件(而非 mouseenter)监听悬停,这样可以检测到在不同标记之间移动的情况:
- 通过比较坐标字符串判断是否移动到了新的要素
- 当移动到新要素时,更新弹窗内容和位置
- 鼠标离开时移除弹窗
4. 状态追踪
使用 currentFeatureCoordinates 变量追踪当前悬停的要素坐标,确保只在移动到不同要素时才更新弹窗,避免频繁更新导致的闪烁。
5. 与lesson-066的区别
- 点击 vs 悬停:悬停提供更即时的反馈
- 单一弹窗 vs 多个弹窗:悬停方案复用一个弹窗实例
- 事件类型:mouseenter vs mousemove
⚙️ 参数说明
Popup 构造参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| closeButton | boolean | 否 | 关闭按钮,默认true |
| closeOnClick | boolean | 否 | 点击关闭,默认false |
mousemove 事件
| 特性 | 说明 |
|---|---|
| 触发频率 | 鼠标每次移动都触发 |
| 适用场景 | 需要检测相邻要素变化时 |
| 性能注意 | 避免在事件处理中进行重操作 |
🎨 效果说明

运行代码后,地图上会显示自定义的标记图标。当鼠标悬停在任意标记上时,该位置会立即显示一个信息弹窗,展示对应的活动详情。鼠标从一个标记移动到另一个标记时,弹窗内容会自动更新为新标记的信息。鼠标离开标记区域后,弹窗会自动消失。整个交互过程流畅自然,适合展示需要快速预览的信息。
💡 常 见 问 题
Q1: 弹窗闪烁怎么办?
A: 这是因为 mousemove 事件触发频率很高。代码中使用 currentFeatureCoordinates 来检测是否真的移动到了新要素,避免不必要的更新。
Q2: 为什么使用 mousemove 而不是 mouseenter?
A: mouseenter 不会检测在不同标记之间的移动。如果要在多个标记间移动时弹窗内容能正确更新,就需要使用 mousemove。
Q3: 如何让悬停弹窗也可以点击关闭?
A: 设置 closeOnClick: true 即可让点击地图其他位置时关闭弹窗。
📝 练习任务
- 基础练习:将自定义标记图片更换为自己的图标
- 进阶挑战:添加延迟效果,鼠标悬停0.5秒后才显示弹窗
- 拓展思考:如何实现同时显示多个弹窗(每个悬停的标记一个)?
- 综合实践:创建一个地点预览系统,支持鼠标悬停快速预览
🌟 最佳实践
- 性能优化: 使用坐标比较避免频繁更新弹窗
- 用户体验: 使用mousemove检测相邻要素变化
- 状态管理: 复用单一弹窗实例减少DOM操作
- 交互优化: 设置closeButton: false和closeOnClick: false保持弹窗稳定
- 可访问性: 为弹窗内容添加适当的ARIA属性
🔗 延伸阅读
🌟 最佳实践
- 动画流畅性: 使用requestAnimationFrame优化动画
- 用户反馈: 为用户提供清晰的交互反馈
- 性能监控: 监控帧率,避免卡顿
- 无障碍: 考虑键盘导航和屏幕阅读器
🔗 延伸阅读
-
[下一课预告]:将继续学习地图图层的基础知识
本文是MapLibre GL JS实践课程系列的一部分,欢迎关注收藏

536

被折叠的 条评论
为什么被折叠?



