Files
microapp-vue3-interview/docs/interview-question-route-track.md

201 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 前端面试题:地图路线规划 + 轨迹动画系统
## 题目描述
在 Vue 3 + 高德地图的养老/医疗微前端应用中,实现一个**护理员轨迹系统**
```
操作流程:
[📍标记起点] → 点击地图放 📍
[🏁标记终点] → 点击地图放 🏁
[➕添加护理员] → 在起点创建 👩‍⚕️
[🚗规划路线] → AMap.Driving 计算驾车路线 → 蓝色折线
[▶出发] → 护理员沿路线动画移动
不管路径 500km 还是 2000km动画始终 ~10 秒丝滑播完
```
**核心要求:**
1. 起点和终点通过**点击地图手动标记**(独立 `AMap.Marker`
2. 驾车路线使用 `AMap.Driving` 规划(真实道路路径,非直线)
3. 护理员标记沿路线**动画移动**,无论路径多长都流畅无卡顿
4. 支持暂停、回到起点、全部清除
5. 护理员不存在时点击"出发"要**自动创建**(容错 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动画抖动 | 均匀间隔,**视觉丝滑** |
**下采样算法:**
```ts
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. 帧动画引擎
```ts
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 的完整错误处理
```ts
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`
**期望回答:**
- `rAF` 60fps → 250 点只需 4 秒(太快)
- `rAF` 帧率不稳定(后台标签页降到 1fps→ 动画时间不可预测
- `setInterval(40)` 稳定 25fps后台自动暂停浏览器节流回来时从当前位置继续
- 需要配合进度条时需要可知的总时长,`setInterval` 更可控
### 追问 3`startAnimation` 中为什么要 `await addCaregiver()`
**期望回答:** `addCaregiver()` 内部调用 `loadAMap()` 是异步的。如果护理员不存在,需要等标记创建完成再开始动画。`await` 确保 `caregiverMarker.value` 已被赋值,后续 `setInterval` 中才能正常调用 `setPosition`
### 追问 4如何让动画支持「可调速」
**期望回答:** 将 40ms 提取为变量 `speed`,暴露给外部。速度加倍 = interval 减半:
```ts
const speedMap = { '1x': 40, '2x': 20, '4x': 10 }
// 或动态计算,进度条相应调整
```