本文基于 Vue 3 + TypeScript 项目中的实际代码,介绍高德地图 JS API 2.0 中骑行导航(Riding)的核心用法,涵盖地图初始化、路径规划、自定义标记、区域遮罩、轮询刷新等完整实践。
一、概述
高德地图 JS API 2.0 提供了多种导航服务,包括:
| 导航方式 | 对应类 | 适用场景 |
|---|---|---|
| 步行导航 | AMap.Walking | 步行路线规划 |
| 骑行导航 | AMap.Riding | 自行车/电动自行车路线 |
| 驾车导航 | AMap.Driving | 机动车辆路线 |
| 公交导航 | AMap.Transfer | 公共交通换乘 |
| 货车导航 | AMap.TruckDriving | 货车路线规划 |
本文主要介绍 AMap.Riding(骑行导航),一个典型的应用场景是:在派单系统中,实时展示工人到电站的骑行路径和预计到达时间。
二、SDK 引入与配置
2.1 在 HTML 中引入 SDK
在 index.html 中通过
html
<!-- 安全密钥配置(必须在使用地图前声明) -->
<script type="text/javascript">
window._AMapSecurityConfig = {
securityJsCode: "你的安全密钥"
};
window.global = window;
</script>
<!-- 加载 JS API,同时通过 plugin 参数加载骑行导航插件 -->
<script src="https://webapi.amap.com/maps?v=2.0&key=你的Key&plugin=AMap.Riding"></script>
关键参数说明:
- v=2.0:使用 JS API 2.0 版本
- key:在高德开放平台申请的 Web 端 Key
- plugin=AMap.Riding:按需加载骑行导航插件,如果有多个插件用逗号分隔
- window._AMapSecurityConfig.securityJsCode:安全密钥,配合 Key 使用(2021年底之后申请的 Key 必须配置)
2.2 安装 TypeScript 类型声明
bash
npm install @types/amap-js-api
在 tsconfig.json 中确保类型被正确引用。
三、地图初始化
创建地图实例是使用所有功能的基础:
typescript
const mapContainer = ref(null);
const mapInstance = new window.AMap.Map(mapContainer.value!, {
center: [120.433904, 31.329341], // 地图中心点 [经度, 纬度]
zoom: 11, // 缩放级别(3-20)
viewMode: "2D", // 视图模式:2D 或 3D
mapStyle: "amap://styles/xxx", // 自定义地图样式(可在高德控制台创建)
scrollWheel: true, // 允许鼠标滚轮缩放
resizeEnable: true // 容器大小变化时自动调整地图
});
常用配置项清单:
| 配置项 | 类型 | 说明 |
|---|---|---|
| center | lng, lat | 地图中心点经纬度 |
| zoom | number | 缩放级别(3-20) |
| viewMode | "2D" | "3D" |
| mapStyle | string | 自定义地图样式 URL |
| scrollWheel | boolean | 是否允许滚轮缩放 |
| resizeEnable | boolean | 容器大小变化时自动调整 |
| layers | TileLayer\[\] | 自定义图层 |
| features | string\[\] | 地图要素显示控制 |
模板部分只需要一个容器元素:
html
<template>
<div ref="mapContainer" class="amap-container" />
</template>
<style scoped>
.amap-container {
width: 100%;
height: 100%;
}
</style>
四、骑行导航(Riding)核心用法
4.1 创建骑行导航实例
为每个需要导航的工人创建一个 Riding 实例:
typescript
const riding = new (window.AMap as any).Riding({
map: mapInstance, // 绑定的地图实例
policy: (window.AMap as any).RidingPolicy.ELECTRIC_BIKE, // 骑行策略:电动车
autoFitView: false, // 不自动调整视野
showTraffic: false, // 不显示路况
hideMarkers: true // 隐藏默认的起终点标记(使用自定义标记)
});
构造函数参数详解:
| 参数 | 类型 | 说明 |
|---|---|---|
| map | AMap.Map | 绑定地图实例,路径将绘制在此地图上 |
| policy | RidingPolicy | 骑行策略,决定路线规划偏好 |
| autoFitView | boolean | 是否自动调整地图视野以包含整条路线 |
| showTraffic | boolean | 是否显示实时路况 |
| hideMarkers | boolean | 是否隐藏默认的起点/终点标记(启用自定义标记时设为 true) |
骑行策略(RidingPolicy)枚举:
| 值 | 说明 |
|---|---|
| RidingPolicy.DEFAULT | 默认模式(普通自行车) |
| RidingPolicy.ELECTRIC_BIKE | 电动车模式 |
| RidingPolicy.RECOMMENDED | 推荐路线(最快到达) |
| RidingPolicy.SHORTEST_DISTANCE | 最短距离 |
4.2 路径搜索
调用 search 方法进行路径规划,传入起点和终点坐标:
typescript
riding.search(
[Number(worker.lng), Number(worker.lat)], // 起点:工人当前位置 [lng, lat]
[Number(worker.station.lng), Number(worker.station.lat)], // 终点:电站位置 [lng, lat]
(status: string, result: any) => {
// status 取值:
// "complete" - 搜索成功
// "error" - 搜索失败
// "no_data" - 无结果
if (status === "complete") {
const routes = result.routes; // 路线方案数组
const firstRoute = routes[0]; // 第一条推荐路线
const distance = firstRoute.distance; // 总距离(米)
const time = firstRoute.time; // 预计耗时(秒)
const steps = firstRoute.steps; // 路线步骤详情
}
}
);
回调参数 result 结构:
result
├── info: string // 状态说明
├── origin: LngLat // 起点坐标
├── destination: LngLat // 终点坐标
├── start: Poi // 起点 POI 信息
├── end: Poi // 终点 POI 信息
└── routes: Route\[\] // 路线方案数组
└── 0
├── distance: number // 总距离(米)
├── time: number // 预计耗时(秒)
├── policy: string // 使用的策略
└── steps: Step\[\] // 每段路线步骤详情
4.3 批量路径搜索(并发优化)
在实际业务中,往往需要同时计算多个工人到各自电站的路径。使用 Promise.all 实现并发搜索,避免串行等待:
typescript
const workerArray = Array.from(state.workers.values());
// 将每个 search 调用包装成 Promise
const pros = workerArray.map(
worker =>
new Promise<[any, any, WorkerTracing]>(resolve =>
worker.riding.search(
[Number(worker.lng), Number(worker.lat)],
[Number(worker.station.lng), Number(worker.station.lat)],
(status, result) => {
resolve([status, result, worker]);
}
)
)
);
// 并发执行所有搜索
const results = await Promise.all(pros);
// 提取预计到达时间并转换为分钟(保留一位小数)
const etimeMap = results.reduce<Map<ID, number>>((map, [status, result, worker]) => {
const timeSeconds = result?.routes?.[0]?.time || 0;
const timeMinutes = Math.round((timeSeconds / 60) * 10) / 10;
map.set(worker.id, timeMinutes);
return map;
}, new Map());
4.4 清除导航路径
当工人下线或不再需要某条路径时,调用 clear 方法清除地图上的路径线:
typescript
// 清除单个导航实例的路径
worker.riding.clear();
// 批量清除所有路径
state.workers.forEach((worker) => worker.riding.clear());
五、轮询刷新机制
采用定时轮询的方式持续更新工人位置和导航路径,保证数据的实时性:
typescript
let timer = true; // 轮询控制标志
async function loadWorkers() {
if (timer === false) return; // 组件卸载时停止轮询
try {
const res = await APIMap.listByReceived(); // 从后端获取工人列表
const works = new Map();
res.data.forEach(worker => {
const oldWorker = state.workers.get(worker.id);
if (oldWorker && state.stations.has(oldWorker.station.id)) {
// 复用已有实例,只更新数据
works.set(worker.id, {
...state.workers.get(worker.id)!,
...worker
});
state.workers.delete(worker.id);
} else {
// 新工人,创建新的导航实例
const station = state.stations.get(worker.stationId);
if (!station) return;
const w = {
...worker,
station: station,
riding: new (window.AMap as any).Riding({
map: mapInstance,
policy: (window.AMap as any).RidingPolicy.ELECTRIC_BIKE,
autoFitView: false,
showTraffic: false,
hideMarkers: true
})
};
works.set(w.id, w);
}
});
// 清理失效的导航实例
state.workers.forEach((worker) => worker.riding.clear());
state.workers = works;
// 并发路径搜索
const workerArray = Array.from(state.workers.values());
const pros = workerArray.map(/* ... 路径搜索 ... */);
const results = await Promise.all(pros);
// 更新地图标记
workerMarkers.update(Array.from(state.workers.values()), etimeMap, mapInstance!);
} catch (err) {
console.error("获取接单人失败:", err);
}
setTimeout(loadWorkers, 3000); // 每 3 秒轮询一次
}
轮询策略设计要点:
- 增量更新:对已存在的工人复用已有实例,只更新位置信息
- 清理失效数据:对不再出现在列表中的工人,调用 riding.clear() 清除路径
- 并发搜索:使用 Promise.all 同时发起所有路径请求,减少总等待时间
- 生命周期控制:组件卸载时通过 timer 标志位停止轮询
六、自定义标记(Marker)与路径联动
不使用高德地图默认的起终点标记,而是通过自定义 DOM 元素实现更丰富的标记效果。
6.1 通用标记管理器
封装一个通用的标记管理器,支持增量更新和 DOM 复用:
typescript
type ID = number | string;
function markerManager() {
const markers: Map<ID, AMap.Marker> = new Map();
function update<Item extends { id: ID }>(
items: Item[],
mapInstance: AMap.Map,
getPosition: (ite: Item) => AMap.LocationValue,
createData: (item: Item, reuseContent?: HTMLElement | string) => {
content: HTMLElement | string;
title: string;
},
onClick?: (item: Item) => void
) {
// 1. 更新已有标记或创建新标记
items.forEach(item => {
const position = getPosition(item);
let marker = markers.get(item.id);
if (marker) {
// 更新已有标记的位置和内容
marker.setPosition(position);
const data = createData(item, marker.getContent());
marker.setTitle(data.title);
if (marker.getContent() !== data.content) {
marker.setContent(data.content);
}
} else {
// 创建新标记
const data = createData(item);
marker = new window.AMap.Marker({
position: position,
title: data.title,
content: data.content,
anchor: "center",
zIndex: 100
});
if (onClick) {
marker.on("click", () => onClick(item));
}
mapInstance.add(marker);
markers.set(item.id, marker);
}
});
// 2. 移除不在新列表中的标记
const oldIds = Array.from(markers.keys());
const removeIds = oldIds.filter(id => !items.some(inner => inner.id === id));
const removeMarkers = removeIds.map(id => {
const marker = markers.get(id)!;
markers.delete(id);
return marker;
});
mapInstance.remove(removeMarkers);
}
return { markers, update };
}
设计亮点:
- 泛型约束:Item 必须包含 id 属性,确保标记可被追踪
- DOM 复用:通过 reuseContent 参数复用已有 DOM 元素,避免频繁重建导致的闪烁
- 差异更新:自动对比新旧列表,移除不再需要的标记,添加新增的标记
6.2 工人标记(含预计到达时间)
为每个工人创建带头像和预计到达时间的自定义标记:
typescript
function useWorkerMarkers() {
const MarkerManager = markerManager();
function update(
workers: WorkerTracing[],
estimatedTimeSeconds: Map<ID, number>,
mapInstance: AMap.Map
) {
MarkerManager.update<WorkerTracing>(
workers,
mapInstance,
item => [Number(item.lng), Number(item.lat)],
(item, reuseContent) => {
const time = Math.round((estimatedTimeSeconds.get(item.id)! / 60) * 10) / 10;
const timeText = `${item.name}<br>预计${time}分钟`;
// 复用已有 DOM 时只更新文字内容
if (reuseContent !== undefined && typeof reuseContent === "object") {
reuseContent.querySelector(".time-text")!.innerHTML = timeText;
return { content: reuseContent, title: "" };
}
// 创建新的标记 DOM
const startMarkerContent = document.createElement("div");
startMarkerContent.className = "worker-marker-with-time";
const avatarImg = document.createElement("img");
avatarImg.src = item.avatar || workerImage;
avatarImg.className = "worker-avatar";
avatarImg.style.width = "32px";
avatarImg.style.height = "32px";
avatarImg.style.borderRadius = "50%";
avatarImg.style.objectFit = "cover";
avatarImg.style.border = "2px solid white";
avatarImg.style.boxShadow = "0 0 4px rgba(0,0,0,0.3)";
const timeDiv = document.createElement("div");
timeDiv.className = "time-text";
timeDiv.innerHTML = timeText;
timeDiv.style.padding = "4px 8px";
timeDiv.style.fontSize = "12px";
timeDiv.style.color = "white";
timeDiv.style.backgroundColor = "rgba(0, 0, 0, 0.7)";
timeDiv.style.borderRadius = "10px";
startMarkerContent.appendChild(avatarImg);
startMarkerContent.appendChild(timeDiv);
return { title: "", content: startMarkerContent };
}
);
}
return { markers: MarkerManager.markers, update };
}
6.3 电站标记(含状态波纹动画)
电站标记根据状态显示不同的视觉效果,报警状态有红色波纹动画:
typescript
function useStationMarkers() {
const MarkerManager = markerManager();
function update(
stations: PowerStation[],
mapInstance: AMap.Map,
onChange: (id: string | number) => void
) {
MarkerManager.update<PowerStation>(
stations,
mapInstance,
(item) => [Number(item.lng), Number(item.lat)],
(item, reuseContent) => {
const statusText = `${item.stationStatus}`;
// 复用 DOM 时只更新状态属性
if (reuseContent) {
const rContent = reuseContent as HTMLElement;
const attr = rContent.getAttribute("station-status");
if (attr !== statusText) {
rContent.setAttribute("station-status", statusText);
}
return { content: rContent, title: `电站${item.id}` };
}
// 创建新标记
const markerContent = document.createElement("div");
markerContent.classList.add("event-marker");
markerContent.setAttribute("station-status", statusText);
markerContent.innerHTML = `
<div class="ripple-container status-2">
<div class="ripple ripple-1"></div>
<div class="ripple ripple-2"></div>
<div class="ripple ripple-3"></div>
<div class="center-dot"></div>
</div>
<div class="ripple-container status-1">
<div class="center-dot-green"></div>
</div>
`;
return { title: `电站${item.id}`, content: markerContent };
},
(item) => onChange(item.id)
);
}
return { markers: MarkerManager.markers, update };
}
CSS 波纹动画实现:
css
@keyframes ripple-animation {
0% {
width: 38px;
height: 38px;
opacity: 1;
}
100% {
width: 95px;
height: 95px;
opacity: 0;
}
}
.event-marker .ripple {
position: absolute;
top: 50%;
left: 50%;
border: 3.8px solid #f44;
border-radius: 50%;
opacity: 0;
transform: translate(-50%, -50%);
}
.event-marker .ripple-1 {
width: 38px;
height: 38px;
animation: ripple-animation 2s ease-out infinite;
}
.event-marker .ripple-2 {
width: 38px;
height: 38px;
animation: ripple-animation 2s ease-out infinite 0.6s;
}
.event-marker .ripple-3 {
width: 38px;
height: 38px;
animation: ripple-animation 2s ease-out infinite 1.2s;
}
通过 CSS 属性选择器控制不同状态下的显示:
css
/* 默认隐藏所有波纹 */
.event-marker .ripple-container {
display: none;
}
/* 正常状态显示绿色标记 */
.event-marker[station-status="1"] .ripple-container.status-1 {
display: block;
}
/* 报警状态显示红色波纹 */
.event-marker[station-status="2"] .ripple-container.status-2 {
display: block;
}
七、区域遮罩与高亮(Polygon)
使用 AMap.Polygon 实现地图区域遮罩和高亮效果,突出显示特定区域:
typescript
export function createMask(mapInstance: AMap.Map) {
// 全国范围的遮罩路径
const maskPath: AMap.LocationValue[] = [
[73.4992, 18.1489],
[135.083, 18.1489],
[135.083, 53.5609],
[73.4992, 53.5609],
[73.4992, 18.1489]
];
// 大面积遮罩,挖空指定区域(多个区域同时高亮)
const maskPolygon = new window.AMap.Polygon({
path: [maskPath, areaPath2, yantaAreaPath], // 第一个是外轮廓,后续是挖空区域
strokeColor: "transparent",
strokeWeight: 0,
fillColor: "#000000",
fillOpacity: 0.7
});
mapInstance.add(maskPolygon);
// 高亮区域 1
const polygonArea = new window.AMap.Polygon({
path: areaPath2,
strokeColor: "#00ffff",
strokeWeight: 4,
strokeOpacity: 1,
fillColor: "rgba(0, 255, 255, 0.2)",
fillOpacity: 0.2
});
mapInstance.add(polygonArea);
// 高亮区域 2
const yantaPolygon = new window.AMap.Polygon({
path: yantaAreaPath,
strokeColor: "#00ff9d",
strokeWeight: 4,
strokeOpacity: 1,
fillColor: "rgba(0, 255, 157, 0.2)",
fillOpacity: 0.2
});
mapInstance.add(yantaPolygon);
}
Polygon 关键属性:
| 属性 | 类型 | 说明 |
|---|---|---|
| path | LngLat\[\]\[\] | 多边形路径,支持嵌套数组实现挖空(镂空)效果 |
| strokeColor | string | 边框颜色 |
| strokeWeight | number | 边框宽度(像素) |
| strokeOpacity | number | 边框透明度(0-1) |
| fillColor | string | 填充颜色 |
| fillOpacity | number | 填充透明度(0-1) |
遮罩实现原理:第一个 path 数组定义整个遮罩的区域,后续的 path 数组定义需要"挖空"显示的区域,实现类似聚光灯的效果。
八、交互与通信
8.1 点击电站切换
使用 BroadcastChannel API 实现地图组件与其他组件之间的通信:
typescript
// 创建广播通道
const stationChannel = new BroadcastChannel("station-change");
// 点击电站标记时,广播切换消息
const changeStation = (stationId: string | number) => {
stationChannel.postMessage({
type: "CHANGE_STATION",
stationId
});
};
BroadcastChannel 的优势:
- 同源页面/组件间通信,无需手动管理事件监听
- 自动隔离不同频道,不会互相干扰
- 比 postMessage 更简洁,不需要指定 targetOrigin
8.2 报警音效
当电站出现报警状态时,触发音效提醒:
typescript
import sound from "@/assets/sound.mp3";
export function useAudioPlayer() {
let audioPlayer: HTMLAudioElement | null = null;
function play() {
try {
if (!audioPlayer) {
audioPlayer = new Audio(sound);
}
audioPlayer.currentTime = 0; // 从头播放
audioPlayer.play().catch(error => {
console.error("播放音效失败:", error);
});
} catch (error) {
console.error("创建音频播放器失败:", error);
}
}
function pause() {
audioPlayer?.pause();
}
return { play, pause };
}
注意:现代浏览器要求用户交互后才能播放音频,因此 play() 需要在用户首次点击页面后调用才有效。如果是在自动轮询中触发,可能被浏览器拦截,需要做好错误处理。
九、生命周期管理
在 Vue 组件的 onBeforeUnmount 中妥善清理所有地图相关资源,避免内存泄漏:
typescript
onBeforeUnmount(() => {
timer = false; // 停止轮询定时器
state.workers.forEach((worker) => worker.riding.clear()); // 清除所有导航路径
AudioPlayer.pause(); // 停止音效播放
stationChannel.close(); // 关闭广播通道
mapInstance!.destroy(); // 销毁地图实例,释放内存
});
清理顺序建议:
- 先停止数据轮询(timer = false),防止在销毁过程中产生新的异步操作
- 清除所有叠加物(路径、标记等)
- 关闭外部资源(音频、通信通道等)
- 最后销毁地图实例
十、常见问题与注意事项
10.1 安全密钥配置
2021年底之后申请的高德 Key 必须配置安全密钥,否则地图无法正常加载。在引入 SDK 的 script 标签之前声明:
html
<script type="text/javascript">
window._AMapSecurityConfig = {
securityJsCode: "你的安全密钥"
};
</script>
10.2 骑行导航的坐标格式
search 方法的起点和终点参数必须是 lng, lat 格式的数组,注意经纬度顺序,不要颠倒。
10.3 并发搜索限制
高德 API 对并发请求有频率限制(QPS)。如果同时需要计算大量路径,建议分批处理或加入请求间隔,避免触发限流。
10.4 TypeScript 类型处理
部分高德 API 的类型定义可能不完整(如 RidingPolicy),可以使用 (window.AMap as any) 进行类型断言,或自行补充类型声明。
10.5 隐藏版权信息
如果业务需要,可以通过 CSS 深度选择器隐藏高德地图的 logo 和版权信息:
css
:deep(.amap-copyright),
:deep(.amap-logo) {
display: none !important;
}
注意:请确认你的高德地图使用协议是否允许隐藏版权信息。
10.6 音频自动播放限制
浏览器通常禁止在没有用户交互的情况下自动播放音频,因此报警音效可能需要在用户首次点击页面后才能正常触发。建议在报警逻辑中做好 try-catch 错误处理。
十一、总结
本文完整介绍了高德地图骑行导航 API 在 Vue 3 项目中的实践,涵盖以下核心功能模块:
| 功能模块 | 使用的 API | 核心要点 |
|---|---|---|
| 地图初始化 | AMap.Map | 配置中心点、缩放级别、自定义样式 |
| 骑行路径规划 | AMap.Riding + search() | 电动车模式、批量并发搜索 |
| 自定义标记 | AMap.Marker + DOM | 增量更新、DOM 复用、状态波纹动画 |
| 区域遮罩/高亮 | AMap.Polygon | 镂空遮罩实现聚光灯效果 |
| 实时轮询 | setTimeout + Promise.all | 每 3 秒刷新,并发路径搜索 |
| 跨组件通信 | BroadcastChannel | 点击电站切换联动 |
| 报警音效 | HTMLAudioElement | 浏览器自动播放限制处理 |
关键设计原则:
- 隐藏默认标记(hideMarkers: true),使用自定义 DOM 标记实现更丰富的视觉效果
- 批量并发搜索(Promise.all),大幅提升多路径计算性能
- 增量更新复用(DOM 复用 + 实例复用),避免重复创建和闪烁
- 完善的资源清理(onBeforeUnmount),防止内存泄漏
希望本文能帮助你在项目中更好地使用高德地图骑行导航 API!