7.5 KiB
7.5 KiB
前端面试题:地图路线规划 + 轨迹动画系统
题目描述
在 Vue 3 + 高德地图的养老/医疗微前端应用中,实现一个护理员轨迹系统:
操作流程:
[📍标记起点] → 点击地图放 📍
[🏁标记终点] → 点击地图放 🏁
[➕添加护理员] → 在起点创建 👩⚕️
[🚗规划路线] → AMap.Driving 计算驾车路线 → 蓝色折线
[▶出发] → 护理员沿路线动画移动
不管路径 500km 还是 2000km,动画始终 ~10 秒丝滑播完
核心要求:
- 起点和终点通过点击地图手动标记(独立
AMap.Marker) - 驾车路线使用
AMap.Driving规划(真实道路路径,非直线) - 护理员标记沿路线动画移动,无论路径多长都流畅无卡顿
- 支持暂停、回到起点、全部清除
- 护理员不存在时点击"出发"要自动创建(容错 UX)
架构设计
useRouteTrack(mapInstance)
├─ enterSetMode('origin'|'dest') ← 点击地图放标记
├─ addCaregiver() ← 在起点创建护理员
├─ planRoute() ← AMap.Driving 路线规划
│ └─ downsamplePath(full, 250) ← ★ 路径下采样
├─ startAnimation() ← setInterval 帧动画
└─ clearAll() ← 完整清理
核心技术点
1. 路径下采样 — 丝滑动画的秘密
AMap.Driving 返回原始路径:5000+ 个点(每几米一个)
↓ downsamplePath(fullPath, 250)
动画路径:250 个点(均匀间隔)
↓ setInterval(40ms) → 25fps
总时长:250 × 40ms = 10 秒(恒定)
为什么需要下采样?
| 不用下采样 | 用下采样 |
|---|---|
| 5000 点 × 40ms = 200 秒(太慢) | 250 点 × 40ms = 10 秒(刚好) |
| 短距离只有 200 点 = 8 秒(不一致) | 始终 250 点 = 恒定 10 秒 |
| 每个点间隔 ~1m,动画抖动 | 均匀间隔,视觉丝滑 |
下采样算法:
function downsamplePath(path: [number, number][], target: number): [number, number][] {
if (path.length <= target) return [...path]
const result: [number, number][] = []
const step = (path.length - 1) / (target - 1) // 5000/249 ≈ 20.08
for (let i = 0; i < target; i++) {
result.push(path[Math.min(Math.round(i * step), path.length - 1)])
}
return result
}
为什么选 250?
- 250 × 40ms = 10 秒,不短不长,刚好展示完整轨迹
- 25fps 是人眼感知流畅的最低帧率,再低会卡顿
- 250 个
setPosition调用对 DOM 压力极小
2. 帧动画引擎
async function startAnimation(): Promise<void> {
// 🔧 容错:护理员不存在 → 自动在起点创建
if (!caregiverMarker.value) {
if (!originPoint.value) { alert('请先标记起点'); return }
await addCaregiver()
}
const total = animPath.value.length - 1 // 249 步
animTimer = setInterval(() => {
if (animStep.value >= total) {
stopAnimation() // 到达终点自动停
animProgress.value = 1
return
}
animStep.value++
animProgress.value = animStep.value / total // 0→1 驱动进度条
caregiverMarker.value?.setPosition(animPath.value[animStep.value])
}, 40) // 25fps
}
| 参数 | 值 | 说明 |
|---|---|---|
| 帧间隔 | 40ms | 25fps,人眼流畅感知阈值 |
| 总步数 | 249 | 250 个路径点,首点即起始位置 |
| 总时长 | ~10s | 恒定,不受实际距离影响 |
| 进度计算 | step/total |
0→1,驱动 CSS 进度条宽度 |
3. 三种 status 的完整错误处理
driving.search(originLngLat, destLngLat, (status, result) => {
if (status === 'complete' && result.routes?.length > 0) {
// ✅ 成功:提取路径 → 下采样 → 绘制折线
} else if (status === 'error') {
// ❌ 参数/权限问题:INVALID_USER_SCODE(缺安全密钥)等
reject(new Error(`路线规划失败: ${result.info || result.message}`))
} else if (status === 'no_data') {
// ⚠️ 起终点之间确实无路(如海岛 → 内陆无桥梁)
reject(new Error('起终点之间无可用路线'))
} else {
// ⁉️ 未知状态,不应发生
reject(new Error(`路线规划失败 (${status})`))
}
})
4. 点击地图放标记 — 模式切换设计
enterSetMode('origin' | 'dest')
→ exitSetMode() // 先退出当前模式(互斥)
→ settingMode = mode // origin 或 dest
→ map.setDefaultCursor('crosshair')
→ map.on('click', handler)
→ 点击地图:
→ 获取经纬度
→ mode === 'origin' ? applyOrigin() : applyDestination()
→ exitSetMode() // 单次点击后自动退出
三个关键设计决策:
| 决策 | 理由 |
|---|---|
| 单次点击后自动退出 | 避免用户误放多个标记 |
| origin 和 dest 互斥 | 同一时间只能设一种点,防止状态混乱 |
| 退出时恢复光标 + 解绑事件 | exitSetMode() 统一清理,防止事件泄漏 |
考察点汇总
| 层级 | 考察点 | 难度 |
|---|---|---|
| 算法 | 路径下采样(均匀间隔重采样) | ⭐⭐⭐ |
| 动画 | setInterval 帧动画 + 进度归一化 | ⭐⭐ |
| API 集成 | AMap.Driving 三种 status 的完整处理 | ⭐⭐⭐ |
| 交互设计 | 模式切换 + 点击地图放标记 | ⭐⭐ |
| 容错设计 | 护理员缺失时自动创建 | ⭐ |
| 资源管理 | setInterval 清理 + 事件解绑 | ⭐⭐ |
| 调试 | INVALID_USER_SCODE 问题排查 | ⭐⭐⭐⭐ |
加分项
- 能解释为什么 250 个点 — 250×40ms=10s 恒定,与距离无关
- 能提出变速方案 — 起步加速/到站减速(easeInOutCubic),用动态 interval 替代固定 40ms
- 能指出
new AMap.LngLat(lng, lat)的必要性 — 数组[lng, lat]在某些版本不被 Driving 接受 - 能说明
map: undefined会导致 Driving 失败 — Driving 依赖 map 做投影计算 - 能分析不同距离下的优化策略 — 短距离提高 target 点数(更细腻),长距离降低(防卡顿)
追问方向
追问 1:如果路线有 10 万个点,250 的下采样会丢失多少细节?
期望回答: 10 万 → 250,每 400 个点取 1 个,城市街道级别的拐弯会被抹平。但视觉上因为标记移动只显示一个点,250 个关键帧足以模拟沿途运动。如果需要更高精度,可以动态调整 target:短距离(<50km)用 500 点,长距离(>500km)用 150 点。
追问 2:为什么用 setInterval 而不用 requestAnimationFrame?
期望回答:
rAF60fps → 250 点只需 4 秒(太快)rAF帧率不稳定(后台标签页降到 1fps)→ 动画时间不可预测setInterval(40)稳定 25fps,后台自动暂停(浏览器节流),回来时从当前位置继续- 需要配合进度条时需要可知的总时长,
setInterval更可控
追问 3:startAnimation 中为什么要 await addCaregiver()?
期望回答: addCaregiver() 内部调用 loadAMap() 是异步的。如果护理员不存在,需要等标记创建完成再开始动画。await 确保 caregiverMarker.value 已被赋值,后续 setInterval 中才能正常调用 setPosition。
追问 4:如何让动画支持「可调速」?
期望回答: 将 40ms 提取为变量 speed,暴露给外部。速度加倍 = interval 减半:
const speedMap = { '1x': 40, '2x': 20, '4x': 10 }
// 或动态计算,进度条相应调整