504 lines
21 KiB
Markdown
504 lines
21 KiB
Markdown
# 高德地图 Composable 面试题
|
||
|
||
---
|
||
|
||
## Q1:如何在 Vue 3 中集成高德地图(AMap)?
|
||
|
||
---
|
||
|
||
## 题目
|
||
|
||
请你设计一个 Vue 3 组合式函数(Composable)来集成高德地图 JSAPI,要求:
|
||
1. 支持在多个页面中独立使用,但 SDK 脚本只下载一次
|
||
2. 安全密钥在脚本加载之前正确设置
|
||
3. 地图容器在 flex 布局中能正确渲染
|
||
|
||
请描述关键实现步骤和设计决策。
|
||
|
||
---
|
||
|
||
## 参考答案
|
||
|
||
### 1. 配置文件(`src/config/amap.ts`)
|
||
|
||
集中管理 Key、安全密钥、版本号和插件列表:
|
||
|
||
| 配置项 | 说明 |
|
||
|--------|------|
|
||
| `AMAP_JSAPI_KEY` | 前端地图 Key(`VITE_AMAP_JSAPI_KEY` 环境变量) |
|
||
| `AMAP_SECURITY_CODE` | 安全密钥,2021/12/02 后申请的 Key 必须配合使用 |
|
||
| `AMAP_VERSION` | JSAPI 版本,如 `'2.0'` |
|
||
| `AMAP_PLUGINS` | 需要加载的插件数组,如 `Geocoder`、`Driving`、`ToolBar` 等 |
|
||
|
||
### 2. `loadAMap()` — SDK 全局单例加载
|
||
|
||
用两个模块级变量实现双重锁:
|
||
|
||
```
|
||
let amapPromise = null // 正在进行的加载 Promise(防并发重复加载)
|
||
let AMapGlobal = null // 已完成的加载结果(后续调用 O(1) 秒返)
|
||
|
||
loadAMap():
|
||
① if (AMapGlobal) → 直接返回缓存 // 最快路径
|
||
② if (!amapPromise) → 创建加载 Promise // 首次 / 上次失败后
|
||
├─ 先设置 window._AMapSecurityConfig // ⚠️ 必须在 load 之前
|
||
├─ 调用 AMapLoader.load({ key, version, plugins })
|
||
├─ 成功 → 存入 AMapGlobal
|
||
└─ 失败 → amapPromise = null(允许重试)
|
||
③ return amapPromise // 并发调用复用同一 Promise
|
||
```
|
||
|
||
关键设计:
|
||
- **Promise 去重**:多个组件同时调用,只发一次网络请求
|
||
- **失败可重试**:`catch` 中重置 `amapPromise`,不污染 `AMapGlobal`
|
||
|
||
### 3. `useAmap()` — 地图实例管理
|
||
|
||
```ts
|
||
// 返回值
|
||
containerRef // ref → 模板绑定到 <div ref="containerRef" />
|
||
mapInstance // shallowRef → 地图实例(为什么用 shallowRef 见 Q2)
|
||
loading/error // ref → 加载/错误状态
|
||
initMap() // 异步初始化:loadAMap() → new AMap.Map(container, options)
|
||
destroyMap() // 销毁实例 + onUnmounted 自动调用
|
||
```
|
||
|
||
### 4. 页面中使用
|
||
|
||
```vue
|
||
<template>
|
||
<div ref="containerRef" class="map-container" />
|
||
</template>
|
||
|
||
<script setup>
|
||
import { onMounted, nextTick } from 'vue'
|
||
import { useAmap } from '@/composables/useAmap'
|
||
|
||
const { containerRef, mapInstance, initMap } = useAmap({ zoom: 15 })
|
||
|
||
onMounted(async () => {
|
||
const map = await initMap()
|
||
if (map) nextTick(() => map.resize()) // 修正 flex 布局下的初始尺寸
|
||
})
|
||
</script>
|
||
```
|
||
|
||
### 5. 关键细节
|
||
|
||
- **安全密钥时序**:`_AMapSecurityConfig` 必须在 `AMapLoader.load()` 之前设置,否则 Driving / Geocoder 等服务报 `INVALID_USER_SCODE`
|
||
- **flex 布局修正**:地图创建后调用 `map.resize()`,因为 flex 分配的尺寸可能尚未生效
|
||
- **`%` 而非 `vh`**:容器用 `height: 100%` 逐级继承,适配微前端宿主不一定是全视口的场景
|
||
|
||
---
|
||
|
||
## 总结一句话
|
||
|
||
> **配置文件集中管理 Key/插件 → `loadAMap()` 全局单例确保 SDK 只下载一次 → `useAmap()` 封装实例生命周期,页面只需绑定 `containerRef` 并调用 `initMap()`。**
|
||
|
||
|
||
---
|
||
|
||
## Q2:多个地图页面复用时有哪些性能陷阱?如何解决?
|
||
|
||
---
|
||
|
||
## 题目
|
||
|
||
你的应用中有 3 个页面各自包含高德地图,用户在不同页面间切换。请分析:
|
||
1. **JSAPI 脚本会重复下载吗?如何保证只下载一次?**
|
||
2. **`mapInstance` 为什么用 `shallowRef` 而不是 `ref`?从性能和副作用两个角度说明。**
|
||
3. **页面切换时旧地图实例如何清理?画出关键生命周期。**
|
||
|
||
---
|
||
|
||
## 参考答案
|
||
|
||
### 1. JSAPI 脚本只下载一次
|
||
|
||
`loadAMap()` 通过 `AMapGlobal` 缓存已加载结果。页面 A 首次调用时发起 `AMapLoader.load()`,页面 B/C 调用时直接命中缓存返回,不产生额外网络请求。
|
||
|
||
**进阶:`AMap` 与 `mapInstance` 的本质区别**
|
||
|
||
| 对象 | 本质 | 作用 | 数量 |
|
||
|------|------|------|------|
|
||
| **`AMap`** | 全局命名空间 (SDK) | 提供构造函数(如 `new AMap.Marker`)和工具方法 | 全局唯一 |
|
||
| **`mapInstance`** | 地图实例 (Instance) | 具体的地图对象,负责渲染、缩放、事件监听 | 每个容器一个 |
|
||
|
||
> **面试官提问:既然已经有了 `mapInstance`,为什么在点击事件处理函数中还要重新调用 `await loadAMap()`?**
|
||
>
|
||
> **回答:** 因为 `mapInstance` 只是一个渲染好的地图“窗口”,它不包含创建新零件(如 Marker 或 Polyline)的“工厂工具”。为了在点击位置创建新的 Marker,必须通过 `loadAMap()` 获取 `AMap` 这个构造函数库。由于 `loadAMap` 做了单例缓存,这种重复调用是 O(1) 级别的,既保证了代码的健壮性(确保 SDK 已加载),又不会产生额外的网络开销。
|
||
|
||
页面切换时的关键流程:
|
||
|
||
```
|
||
页面 A 活跃 → 页面 B 挂载
|
||
│
|
||
├─ A.onUnmounted → destroyMap()
|
||
│ └─ mapInstance.value.destroy() // 释放 WebGL 上下文、事件监听
|
||
│
|
||
└─ B.onMounted → initMap()
|
||
├─ loadAMap() → 命中 AMapGlobal 缓存(不下载)
|
||
└─ new AMap.Map(containerB, options) // 全新实例挂到新 DOM
|
||
```
|
||
|
||
**核心结论**:SDK 全局常驻,地图实例按页面创建/销毁,互不干扰。
|
||
|
||
### 2. 为什么 `mapInstance` 必须用 `shallowRef`?
|
||
|
||
| 维度 | `shallowRef` ✅ | `ref` ❌ |
|
||
|------|-----------------|----------|
|
||
| **性能** | 仅追踪 `.value` 整体替换,初始化为 O(1) | 递归代理整个 AMap.Map 对象树(数百个属性/WebGL 上下文),阻塞主线程 |
|
||
| **副作用** | 不劫持属性访问,AMap 内部 `this` 指向不变 | Proxy 改变 `this` 指向,可能破坏地图渲染循环、事件系统 |
|
||
| **语义** | 代码中只需要 `mapInstance.value = new Map()` / `.destroy()` / `= null`,不关心内部属性变化 | 完全用不到深度追踪能力 |
|
||
|
||
**一句话**:`shallowRef` 用于"只关心对象整体是否被替换,不关心内部怎么变"的第三方复杂实例。
|
||
|
||
### 3. 页面切换时的完整生命周期
|
||
|
||
| 步骤 | 时机 | 动作 |
|
||
|------|------|------|
|
||
| ① 旧地图销毁 | `onUnmounted` → `destroyMap()` | `map.destroy()` 释放画布、事件、DOM |
|
||
| ② 响应式清理 | Vue 组件卸载 | `mapInstance`、`containerRef` 随组件 GC |
|
||
| ③ 新地图创建 | `onMounted` → `initMap()` | `containerRef` 绑定新 DOM → `new AMap.Map(新容器)` |
|
||
| ④ SDK 缓存命中 | `initMap()` 内部 `loadAMap()` | `AMapGlobal` 非空,O(1) 返回 |
|
||
|
||
---
|
||
|
||
## 总结一句话
|
||
|
||
> **SDK 全局单例只下载一次,地图实例按页面生灭,`shallowRef` 避免深度代理第三方对象 —— 三者配合才能让多页面地图既快又稳。**
|
||
|
||
|
||
---
|
||
|
||
## Q3:如何在高德地图上添加和管理 Marker?
|
||
|
||
---
|
||
|
||
## 题目
|
||
|
||
在一个电子围栏应用中,你需要在地图上添加多种标记:老人位置(👴,可拖拽)、护理员位置(👩⚕️,程序控制移动)、起点/终点(📍/🏁,静态)、围栏顶点(序号圆点,绘制完成后清除)。请回答:
|
||
|
||
1. **Marker 的基本创建流程是什么?`content` 和 `offset` 的作用?**
|
||
2. **不同类型的 Marker 在"创建/更新/销毁"策略上有什么不同?为什么?**
|
||
3. **为什么 Marker 的 CSS 样式不能写在 Vue 的 `<style scoped>` 中?**
|
||
|
||
---
|
||
|
||
## 参考答案
|
||
|
||
### 1. Marker 的基本创建
|
||
|
||
```ts
|
||
const marker = new AMap.Marker({
|
||
position: [lng, lat], // 经纬度
|
||
content: '<div class="my-marker">👴</div>', // 自定义 DOM 内容
|
||
offset: new AMap.Pixel(-17, -17), // 锚点偏移(让图标中心对准坐标)
|
||
zIndex: 200, // 层级
|
||
draggable: true, // 是否可拖拽
|
||
})
|
||
map.add(marker) // 添加到地图
|
||
```
|
||
|
||
- **`content`**:任意 HTML 字符串,AMap 将其渲染为 Marker 的 DOM 节点。比默认的图标更灵活(可以用 emoji + CSS)。
|
||
- **`offset`**:Marker 默认以左上角对齐坐标点,`offset` 向左上偏移一半宽高,使图标中心对准坐标。
|
||
|
||
### 2. 不同 Marker 的创建/更新/销毁策略
|
||
|
||
| 类型 | 创建策略 | 更新策略 | 销毁策略 |
|
||
|------|----------|----------|----------|
|
||
| **老人 👴** | `addPerson` 时 `new Marker` + `map.add` | 拖拽 `dragend` → `setPosition` + 边界检测 | `removePerson` 时 `map.remove` |
|
||
| **护理员 👩⚕️** | `addCaregiver` 时 `new Marker` + `map.add` | 动画循环中 `setPosition`(每 40ms) | `removeCaregiver` 时 `map.remove` |
|
||
| **起点 📍 / 终点 🏁** | 首次点击时 `new Marker`,**保存引用** | **复用**:再次点击只调 `setPosition`,不重建 | `removeOrigin/Dest` 时 `map.remove` |
|
||
| **围栏顶点** | 每次点击都 `new Marker` + `map.add` | 无需更新(绘制过程中只增不减) | `finishDraw`/`cancelDraw` 时**批量 `map.remove`** |
|
||
|
||
**为什么起点/终点用"复用 setPosition"而非"删了重建"?** 避免闪烁,性能更好,且语义上"同一个标记在移动"比"消失再出现"更自然。
|
||
|
||
**为什么围栏顶点要批量清理?** 顶点是临时绘制辅助标记,围栏完成后由 Polygon 接管显示,顶点 Marker 必须全部 `map.remove()`,否则会残留在画布上。
|
||
|
||
### 3. 为什么样式不能 scoped?
|
||
|
||
Vue 的 `<style scoped>` 会给选择器加 `data-v-xxx` 属性选择器。但 Marker 的 DOM 是由 AMap **直接注入地图容器**的,不在 Vue 模板编译范围内,DOM 节点上没有 `data-v-xxx` 属性 → scoped 样式完全匹配不到。
|
||
|
||
**正确做法**:在 `.vue` 文件末尾加一个非 scoped 的 `<style>` 块:
|
||
|
||
```vue
|
||
<style>
|
||
/* 全局生效,按 className 精确匹配,避免污染 */
|
||
.geofence-person-marker { width: 34px; height: 34px; ... }
|
||
.geofence-caregiver-marker { width: 34px; height: 34px; ... }
|
||
</style>
|
||
```
|
||
|
||
通过 **业务前缀命名**(如 `geofence-`)来隔离,而非依赖 Vue 的 scoped 机制。
|
||
|
||
---
|
||
|
||
## 总结一句话
|
||
|
||
> **Marker 通过 `content` + `offset` 实现自定义 UI;静态标记复用 `setPosition`,临时标记批量清理,样式用全局 `<style>` + 业务前缀隔离。**
|
||
|
||
|
||
---
|
||
## Q4:如何实现"标记起点 → 终点 → 规划路线 → 动画移动"的完整交互?
|
||
|
||
---
|
||
|
||
## 题目
|
||
|
||
护理员巡护场景:点击按钮 → 点击地图放置 📍 起点 → 同样放置 🏁 终点 → 添加 👩⚕️ 护理员 → 规划驾车路线(蓝色折线)→ 护理员沿路线动画到达终点。请回答:
|
||
|
||
1. **"点击地图放标记"的交互如何设计?为什么不用全局 `map.on('click')`?**
|
||
2. **路线规划的核心流程?`AMap.Driving` 有哪些注意点?**
|
||
3. **动画如何实现?为什么需要对路径点降采样?**
|
||
|
||
---
|
||
|
||
## 参考答案
|
||
|
||
### 1. "标记模式"交互:用完即解绑
|
||
|
||
采用 **进入模式 → 点击地图 → 退出模式** 三段式,核心是一个 `settingMode` 状态变量(`'origin'` | `'dest'` | `null`):
|
||
|
||
| 阶段 | 动作 |
|
||
|------|------|
|
||
| **进入** | `settingMode = mode`,光标设为 `crosshair`,`map.on('click', handler)` |
|
||
| **点击** | `eventLngLat(e)` 提取坐标 → `applyOrigin(pt)` 或 `applyDestination(pt)` |
|
||
| **退出** | `settingMode = null`,光标还原,`map.off('click', handler)` |
|
||
|
||
**为什么不用全局 `map.on('click')`?** 一直开着无法区分"现在该放起点还是终点",需要额外的 `if/else` 分支;模式开关职责单一,每个模式用完即解绑,防止事件堆积。
|
||
|
||
**Marker 复用策略**:起点/终点如果已存在,只调 `setPosition()` 而非删了重建 —— 避免闪烁。
|
||
|
||
### 2. 路线规划:`AMap.Driving` 五步走
|
||
|
||
前提:`config/amap.ts` 的 `AMAP_PLUGINS` 已包含 `'AMap.Driving'`。
|
||
|
||
```
|
||
① new AMap.Driving({ map, policy: 0 }) → 必须传 map 实例
|
||
② driving.search(起点LngLat, 终点LngLat, callback) → 回调风格,需手动包装 Promise
|
||
③ 从 result.routes[0].steps[i].path 逐段拼出完整坐标数组
|
||
④ new AMap.Polyline({ path, strokeColor: '#4A90D9' }) + map.add()
|
||
⑤ map.setFitView([polyline], false, [60,60,60,60]) → 自动缩放让整条路线可见
|
||
```
|
||
|
||
| 注意点 | 说明 |
|
||
|------|------|
|
||
| **插件预加载** | 未在 `AMAP_PLUGINS` 中声明则 `AMap.Driving` 为 `undefined` |
|
||
| **`policy` 策略** | `0`=速度优先, `1`=费用优先, `2`=距离优先, `3`=不走快速路 |
|
||
| **回调 ≠ Promise** | `search` 是回调风格,用 `new Promise` 包裹以配合 `async/await` |
|
||
| **`steps[i].path`** | 每段道路包含该段所有拐点,需遍历拼接 |
|
||
|
||
### 3. 动画 & 降采样
|
||
|
||
**为什么降采样?** Driving 返回的原始路径有 **数千个点**(每几米一个),直接逐点动画:
|
||
> 5000 点 × 40ms = **200 秒**,太慢且大部分点位间距不到 1 像素。
|
||
|
||
降采样到 **250 点**:等距抽取,`250 × 40ms ≈ 10 秒`,流畅且速度合理。
|
||
|
||
```ts
|
||
// 降采样:从 N 个点中等距取 target 个
|
||
function downsamplePath(path, target = 250) {
|
||
const step = (path.length - 1) / (target - 1)
|
||
return Array.from({ length: target }, (_, i) => path[Math.round(i * step)])
|
||
}
|
||
```
|
||
|
||
**动画循环**:`setInterval` 每 40ms 调一次 `marker.setPosition(animPath[i])`,配合 `animProgress`(0→1)驱动进度条 UI。用 `setInterval` 而非 `rAF` 是因为地图不是逐帧画布,固定间隔配合降采样点数能精确控制总耗时。
|
||
|
||
### 完整流程(状态机视角)
|
||
|
||
```
|
||
settingMode='origin' → click → applyOrigin(Marker 复用)
|
||
↓
|
||
settingMode='dest' → click → applyDestination(Marker 复用)
|
||
↓
|
||
addCaregiver() → new Marker(👩⚕️) at 起点
|
||
↓
|
||
planRoute() → Driving.search → 拼 path → Polyline(蓝) → downsample→250
|
||
↓
|
||
startAnimation() → setInterval 40ms → setPosition → 到达
|
||
```
|
||
|
||
> 全程通过 `isPlanning` / `isAnimating` / `canPlanRoute`(computed) 控制按钮互斥与禁用状态。
|
||
|
||
---
|
||
|
||
## 总结一句话
|
||
|
||
> **模式开关管理点选(用完即解绑)→ `AMap.Driving` + 回调转 Promise 规划路线 → 降采样到 250 点后 `setInterval` + `setPosition` 驱动动画,状态变量全程控制 UI 互斥。**
|
||
|
||
|
||
---
|
||
## Q5:大卡车每10s发一个GPS点,连续3天(~25920点),前端如何绘制轨迹?会卡顿吗?如何处理?
|
||
|
||
---
|
||
|
||
## 题目
|
||
|
||
一辆大卡车每 10 秒上报一个 GPS 位置,连续跑了 3 天,累计约 **25920 个轨迹点**。前端需要在地图上展示完整轨迹。请回答:
|
||
|
||
1. **直接渲染 25920 个点会不会卡顿?为什么?**
|
||
2. **有哪些优化方案?请从数据层、渲染层、交互层三个维度分别说明。**
|
||
3. **落地的推荐组合方案是什么?**
|
||
|
||
---
|
||
|
||
## 参考答案
|
||
|
||
### 1. 会不会卡顿?为什么?
|
||
|
||
**一定会严重卡顿,甚至可能浏览器崩溃(OOM)**。原因:
|
||
|
||
| 环节 | 问题 |
|
||
|------|------|
|
||
| **数据规模** | 25920 个点 = 25919 条线段,远超 AMap.Polyline 流畅阈值(通常 < 2000 段) |
|
||
| **渲染机制** | `AMap.Polyline` 基于 DOM / 普通 Canvas 逐段绘制,缩放平移时全量重绘,直接榨干 CPU |
|
||
| **内存风险** | 大规模坐标数组 + AMap 内部渲染缓冲持续驻留,极端情况 OOM 崩溃 |
|
||
| **视觉浪费** | 屏幕宽度 ~1920px,缩小看整条轨迹只需几百像素,25920 个点中 95% 挤在同一像素毫无贡献 |
|
||
|
||
**一句话**:DOM/Canvas 全量渲染 2.6 万段折线 → 主线程阻塞 + 内存激增 + 每帧重绘 → 必定卡顿。
|
||
|
||
---
|
||
|
||
### 2. 优化方案(数据层 → 渲染层 → 交互层)
|
||
|
||
#### ▎数据层优化:减少点数(脱水)
|
||
|
||
##### 方案一:Douglas-Peucker 抽稀(核心方案)
|
||
|
||
道格拉斯-普克算法——保留轨迹形状特征,丢弃共线/冗余点:
|
||
|
||
> **原理**:连首尾 → 找离连线最远的中间点 → 若距离 > epsilon 则保留并递归 → 否则丢弃中间所有点。
|
||
|
||
- epsilon = 10 米时,25920 点 → **约 500~800 点**,地图上肉眼完全无法分辨差异
|
||
- O(n log n),浏览器计算 < 50ms,前端即可完成
|
||
- 现成库:`simplify-js`、`@turf/simplify`,一行调用
|
||
|
||
**效果**:数据量精简 70%~90%,弯道保留密集点,直道只留两端点。
|
||
|
||
##### 方案二:按缩放级别动态抽稀
|
||
|
||
同一份数据,不同 zoom 用不同 epsilon——远看粗、近看细:
|
||
|
||
| Zoom | 含义 | Epsilon | 预期点数 |
|
||
|------|------|---------|---------|
|
||
| 3~8 | 省/国级别 | 500m | < 100 |
|
||
| 9~12 | 城市级别 | 100m | 200~500 |
|
||
| 13~15 | 区/街道 | 20m | 500~1500 |
|
||
| 16+ | 建筑级 | 5m 或原始数据 | 1000~5000 |
|
||
|
||
**实现**:预计算 3~4 个精度版本的 path,监听 `map.on('zoomend')` 切换 `polyline.setPath()`,避免实时计算。
|
||
|
||
##### 方案三:后端预聚合(前端最省心)
|
||
|
||
后端存储时提前产出多精度版本:
|
||
|
||
```
|
||
GET /api/track/123?precision=low → ~300 点(zoom < 10)
|
||
GET /api/track/123?precision=medium → ~2000 点(zoom 10~14)
|
||
GET /api/track/123?precision=high → 原始数据(zoom > 14)
|
||
```
|
||
|
||
**优点**:前端无需计算,首次请求即最优。
|
||
**缺点**:需后端配合,存储多份。
|
||
|
||
##### 方案四:时间维度聚合
|
||
|
||
最近数据精细、历史粗化:
|
||
|
||
| 时间段 | 展示策略 | 点数 |
|
||
|--------|---------|------|
|
||
| 最近 2 小时 | 原始精度(10s/点) | 720 |
|
||
| 2~12 小时前 | 1 分钟/点 | 600 |
|
||
| 12~72 小时前 | 10 分钟/点 | 360 |
|
||
|
||
**适用场景**:实时监控页,用户更关注"现在在哪",而非历史每一秒。
|
||
|
||
---
|
||
|
||
#### ▎渲染层优化:换引擎(重构)
|
||
|
||
##### 方案五:AMap.Loca — 高德官方 WebGL 大数据引擎(推荐)
|
||
|
||
高德专门针对大屏和海量数据开发的 WebGL 可视化引擎。Loca 中的 **`LineLayer`(折线层)** 或 **`LinkLayer`(飞线层)** 专为万级、十万级轨迹线设计:
|
||
|
||
- WebGL 渲染,GPU 并行处理,不受 DOM/Canvas 瓶颈限制
|
||
- 平移缩放只更新投影矩阵,不重建几何
|
||
- 2.6 万条线段也能维持 60fps
|
||
|
||
**这是高德官方推荐的海量线条方案,也是 Gemini 回答的核心推荐。**
|
||
|
||
##### 方案六:AMap.LabelsLayer + LabelMarker(海量标记场景)
|
||
|
||
如果需要在地图上显示 2.6 万个停靠点或关键标记,传统 `Marker` 超过几百个就卡。`LabelsLayer` 基于 Canvas/WebGL,渲染几万个点极其流畅。配合 `AMap.MassMarks` 可做到百万级散点。
|
||
|
||
##### 方案七:AMap.CustomLayer / 自定义 WebGL 图层(终极可控)
|
||
|
||
使用 `AMap.CustomLayer` 叠加 Three.js 或原生 WebGL:
|
||
|
||
- 坐标一次性上传 GPU 缓冲
|
||
- 完全控制渲染管线(着色器、线宽、颜色渐变)
|
||
- 百万点也能 60fps
|
||
|
||
**缺点**:开发成本高,25920 点场景不需要。
|
||
|
||
---
|
||
|
||
#### ▎交互层优化:按需加载(减负)
|
||
|
||
##### 方案八:视口裁剪(Bounds Filter)
|
||
|
||
监听 `mapmove` / `zoomchange`,获取 `map.getBounds()`,只渲染当前可视区域内的轨迹段:
|
||
|
||
- 视口内的段 → `polyline.setMap(map)` 显示
|
||
- 视口外的段 → `polyline.setMap(null)` 隐藏
|
||
- 用户放大后视口变小,实际渲染点数自然大幅下降
|
||
|
||
**配合 zoom 自适应使用效果最佳**:缩小看粗略趋势(点数少),放大看局部细节(视口小 → 点数也少)。
|
||
|
||
##### 方案九:防抖(Debounce)
|
||
|
||
地图拖拽和缩放时频繁触发重绘,必须加防抖:
|
||
|
||
- 对 `mapmove` / `zoomchange` 加 **200ms 防抖**
|
||
- 等用户操作停止后再重新计算和渲染轨迹
|
||
- 避免拖拽过程中每秒触发数十次无意义的重绘
|
||
|
||
##### 方案十:分片计算(rAF 分批)
|
||
|
||
如果 2.6 万个点必须全量处理(如客户端抽稀计算),不要用 `forEach` 一次性跑完导致浏览器假死:
|
||
|
||
- 用 `requestAnimationFrame` 将计算分批,每帧处理 500~1000 个点
|
||
- 计算期间保持 UI 响应,避免"页面卡死"的用户体验
|
||
|
||
---
|
||
|
||
### 3. 推荐组合方案
|
||
|
||
按量级分档推荐:
|
||
|
||
| 点量 | 组合 |
|
||
|------|------|
|
||
| **< 1 万** | DP 抽稀 + zoom 自适应 + 防抖 |
|
||
| **1~5 万**(本场景) | DP 抽稀 + zoom 自适应 + 视口裁剪 + 防抖 |
|
||
| **5~20 万** | DP 抽稀 + zoom 自适应 + **Loca LineLayer** + 防抖 |
|
||
| **20 万+** | 后端预聚合 + Loca LineLayer / CustomLayer + 视口裁剪 |
|
||
|
||
**对于本场景(25920 点)的具体落地步骤**:
|
||
|
||
1. 收到原始 25920 点 → 前端 `simplify-js` DP 抽稀,epsilon 10m → ~800 点
|
||
2. 预计算 3 个精度版本(epsilon = 5m / 50m / 200m),对应 zoom >= 14 / 10~13 / < 10
|
||
3. 创建 `AMap.Polyline`,根据当前 zoom 选择对应精度
|
||
4. 监听 `zoomend` → `setPath(对应精度)` 无缝切换
|
||
5. 对 `mapmove` 加 200ms 防抖,避免拖拽时频繁重绘
|
||
6. 如果后端支持分段查询,叠加视口裁剪只加载可见段
|
||
|
||
> **够用原则**:2.6 万点不需要上 Loca 或 CustomLayer。DP 抽稀 + zoom 自适应 + 防抖三板斧足够丝滑。点数上 5 万再考虑 Loca。
|
||
|
||
---
|
||
|
||
## 总结一句话
|
||
|
||
> **数据层 DP 抽稀去冗余(脱水)→ 渲染层 Loca/CustomLayer 换引擎(重构,量大时)→ 交互层 zoom 自适应 + 视口裁剪 + 防抖(减负),三层组合按量级分档选配,2.6 万点 DP + zoom + 防抖足矣。**
|