# 高德地图面试题 --- ## 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 → 模板绑定到
mapInstance // shallowRef → 地图实例(为什么用 shallowRef 见 Q2) loading/error // ref → 加载/错误状态 initMap() // 异步初始化:loadAMap() → new AMap.Map(container, options) destroyMap() // 销毁实例 + onUnmounted 自动调用 ``` ### 4. 页面中使用 ```vue ``` ### 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 的 ` ``` 通过 **业务前缀命名**(如 `geofence-`)来隔离,而非依赖 Vue 的 scoped 机制。 --- ## 总结一句话 > **Marker 通过 `content` + `offset` 实现自定义 UI;静态标记复用 `setPosition`,临时标记批量清理,样式用全局 `