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

7.5 KiB
Raw Blame History

前端面试题:地图路线规划 + 轨迹动画系统

题目描述

在 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动画抖动 均匀间隔,视觉丝滑

下采样算法:

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

期望回答:

  • rAF 60fps → 250 点只需 4 秒(太快)
  • rAF 帧率不稳定(后台标签页降到 1fps→ 动画时间不可预测
  • setInterval(40) 稳定 25fps后台自动暂停浏览器节流回来时从当前位置继续
  • 需要配合进度条时需要可知的总时长,setInterval 更可控

追问 3startAnimation 中为什么要 await addCaregiver()

期望回答: addCaregiver() 内部调用 loadAMap() 是异步的。如果护理员不存在,需要等标记创建完成再开始动画。await 确保 caregiverMarker.value 已被赋值,后续 setInterval 中才能正常调用 setPosition

追问 4如何让动画支持「可调速」

期望回答: 将 40ms 提取为变量 speed,暴露给外部。速度加倍 = interval 减半:

const speedMap = { '1x': 40, '2x': 20, '4x': 10 }
// 或动态计算,进度条相应调整