高德地图添加轨迹规划和重放功能

This commit is contained in:
2026-06-25 10:30:45 +08:00
parent 0089e5c107
commit 91afc9d81c
7 changed files with 2740 additions and 0 deletions

View File

@@ -0,0 +1,231 @@
# 前端面试题:高德地图 JSAPI 加载机制与安全密钥
## 题目描述
在 Vue 3 + Vite 项目中,使用 `@amap/amap-jsapi-loader` 动态加载高德地图 JSAPI 2.0。要求:
1. **全局只加载一次** — 多个组件同时调用不会重复下载 ~600KB 脚本
2. **加载失败可重试** — 不缓存失败的 Promise
3. **安全密钥按需配置** — 2021/12/02 后申请的 Key 必须配置 `securityJsCode`
4. **安全密钥必须在脚本加载之前设置** — 否则 Driving/Geocoder 等服务报 `INVALID_USER_SCODE`
---
## 完整源码与解析
### 1. 配置文件
```ts
// config/amap.ts
export const AMAP_JSAPI_KEY = import.meta.env.VITE_AMAP_JSAPI_KEY as string
export const AMAP_WEB_KEY = import.meta.env.VITE_AMAP_WEB_KEY as string
export const AMAP_SECURITY_CODE = import.meta.env.VITE_AMAP_SECURITY_CODE as string
export const AMAP_VERSION = '2.0'
export const AMAP_PLUGINS = [
'AMap.Geocoder', // 地理编码
'AMap.AutoComplete', // 输入提示
'AMap.PlaceSearch', // 搜索
'AMap.Geolocation', // 定位
'AMap.MarkerClusterer', // 点聚合
'AMap.Driving', // 驾车路线规划
] as const
```
### 2. 加载器 — 单例模式
```ts
// composables/useAmap.ts
import AMapLoader from '@amap/amap-jsapi-loader'
import { AMAP_JSAPI_KEY, AMAP_SECURITY_CODE, AMAP_VERSION, AMAP_PLUGINS } from '@/config/amap'
/** 全局 Promise 缓存 — 多次调用共享同一个下载 */
let amapPromise: Promise<typeof AMap> | null = null
/** 全局对象缓存 — 成功后直接返回,避免再次 await */
let AMapGlobal: typeof AMap | null = null
export async function loadAMap(): Promise<typeof AMap> {
// ① 已加载过 → 直接返回
if (AMapGlobal) return AMapGlobal
if (!amapPromise) {
// ② ⚠️ 必须在 JSAPI 脚本加载之前设置安全密钥
if (AMAP_SECURITY_CODE && !(window as any)._AMapSecurityConfig) {
;(window as any)._AMapSecurityConfig = {
securityJsCode: AMAP_SECURITY_CODE,
}
}
// ③ 发起加载
amapPromise = AMapLoader.load({
key: AMAP_JSAPI_KEY,
version: AMAP_VERSION,
plugins: [...AMAP_PLUGINS],
})
.then((amap) => {
AMapGlobal = amap // ④ 成功后缓存,后续调用瞬间返回
return amap
})
.catch((err) => {
amapPromise = null // ⑤ 失败后清空 → 允许重试
throw new Error(`高德地图 JSAPI 加载失败: ${err.message}`)
})
}
return amapPromise
}
```
---
## 核心知识点
### 知识点 ①:单例模式 — 避免重复加载
```
组件 A 组件 B 组件 C
│ │ │
├─ loadAMap() ├─ loadAMap() ├─ loadAMap()
│ amapPromise │ amapPromise │ amapPromise
│ = load() │ !== null │ !== null
│ (开始下载) │ (等待同一Promise)│ (等待同一Promise)
│ ↓ │ ↓ │ ↓
└─── await ──────┴─── await ──────┴─── await
AMapGlobal = amap (缓存)
后续任何调用if (AMapGlobal) return AMapGlobal → 瞬间返回
```
**两层缓存的作用:**
| 缓存层 | 类型 | 作用 |
|--------|------|------|
| `amapPromise` | `Promise<AMap> \| null` | 并发调用去重 — A、B、C 同时调用时,共享同一个下载 Promise |
| `AMapGlobal` | `typeof AMap \| null` | 跨时间缓存 — 下载完成后,任意时刻再调用直接返回对象 |
### 知识点 ②:失败重试
```ts
.catch((err) => {
amapPromise = null // ← 清空 Promise下次调用重新下载
throw new Error(...)
})
```
如果不置 `null`,失败的 Promise 会被缓存,后续调用拿到的是同一个 rejected Promise永远无法恢复。
### 知识点 ③:安全密钥的时序约束
```
✅ 正确顺序:
window._AMapSecurityConfig = { securityJsCode: 'xxx' }
<script src="https://webapi.amap.com/maps?v=2.0&key=..."></script>
AMap.Driving 等服务可用
❌ 错误顺序(写在 AMapLoader.load 之后):
<script src="..."></script> ← JSAPI 已加载,安全配置窗口关闭
window._AMapSecurityConfig = { ... } ← 无效Driving 等服务报 INVALID_USER_SCODE
```
**为什么必须在此之前?** 高德 JSAPI 脚本加载时读取 `_AMapSecurityConfig` 并完成内部初始化。脚本加载后该配置窗口关闭,之后再设置不会生效。
### 知识点 ④:`as const` 插件列表
```ts
export const AMAP_PLUGINS = [
'AMap.Geocoder',
// ...
] as const
```
`as const` 将数组类型收窄为 `readonly` 字面量元组,提供:
- 类型安全:`[...AMAP_PLUGINS]` 展开时保留精确字面量类型
- 防止意外修改:插件列表不应在运行时被 push/pop
- Tree-shaking 友好Vite 可以更好地优化
### 知识点 ⑤:环境变量前缀 `VITE_`
```
.env 文件中:
VITE_AMAP_JSAPI_KEY=xxx ← ✅ Vite 会暴露给客户端
AMAP_JSAPI_KEY=xxx ← ❌ 客户端 import.meta.env 读不到
```
Vite 只暴露 `VITE_` 前缀的环境变量,防止敏感信息泄漏。这是 Vite 的安全设计。
---
## INVALID_USER_SCODE 排查流程
```
[护理员] ❌ Driving 返回错误: 未知错误 INVALID_USER_SCODE
```
**排查清单:**
| 检查项 | 怎么做 |
|--------|--------|
| ① Key 申请时间 | 2021/12/02 之后申请的 Key 必须配置安全密钥 |
| ② `_AMapSecurityConfig` 时机 | 必须在 `AMapLoader.load()` **之前**设置 |
| ③ JSAPI Key vs 安全密钥 | 是两个不同的值,都在 AMap 控制台查看 |
| ④ 密钥拼写 | `securityJsCode`(注意大小写)不能有空格 |
| ⑤ 环境变量 | `.env``VITE_AMAP_SECURITY_CODE=xxx`,重启 dev server |
| ⑥ 条件加载 | 如果没有安全密钥,不设置 config老 Key 不需要) |
---
## 三种 AMap 密钥的区别
| | JSAPI Key | Web Service Key | 安全密钥 |
|---|---|---|---|
| 环境变量 | `VITE_AMAP_JSAPI_KEY` | `VITE_AMAP_WEB_KEY` | `VITE_AMAP_SECURITY_CODE` |
| 用途 | 地图展示、标记、折线 | 服务端 API地理编码等 | 前端安全验证 |
| 使用方式 | `AMapLoader.load({ key })` | 后端 fetch 请求 | `window._AMapSecurityConfig` |
| 暴露风险 | 前端可见(无法避免) | **应仅在后端使用** | 前端可见(建议代理) |
| 影响范围 | 无此 Key 地图无法加载 | 无此 Key 后端 API 不可用 | 2021/12 后新 Key 不配则 Driving/Geocoder 报错 |
---
## 考察点汇总
| 层级 | 考察点 | 难度 |
|------|--------|------|
| **设计模式** | 单例模式 + 两层缓存Promise + 对象) | ⭐⭐⭐ |
| **错误处理** | 失败清空 Promise 允许重试 | ⭐⭐ |
| **时序约束** | `_AMapSecurityConfig` 必须在脚本加载之前 | ⭐⭐⭐⭐ |
| **Vite** | `VITE_` 前缀的环境变量暴露机制 | ⭐⭐ |
| **TypeScript** | `as const` 字面量类型收窄 | ⭐⭐ |
| **调试** | INVALID_USER_SCODE 的系统化排查 | ⭐⭐⭐⭐ |
## 追问方向
### 追问 1为什么不直接在 `index.html` 中加 `<script>` 标签加载 AMap
**期望回答:**
- 按需加载:仅在访问地图页面时才下载 600KB 脚本,首页不需要
- 插件管理:通过 AMapLoader 统一管理插件版本
- 微前端兼容:子应用独立控制加载时机,避免主应用 `<script>` 影响
- 安全密钥时序:`AMapLoader.load()` 之前可以执行任意 JS 设置 `_AMapSecurityConfig`
### 追问 2如果多个子应用都需要 AMap`AMapGlobal` 会冲突吗?
**期望回答:** 如果多个子应用共用同一个域名,`AMapGlobal` 是模块级变量,在同一 JS 上下文中共享。如果子应用用 iframe 沙箱隔离(本项目的 `vue3-app` 配置了 `iframe: true`),则每个 iframe 有独立的 JS 上下文,各自加载自己的 AMap不会冲突。
### 追问 3`window._AMapSecurityConfig` 中还有一个 `serviceHost` 字段是干什么的?
**期望回答:** 用于代理模式。生产环境中不应把安全密钥写在前端代码里,可以通过 Nginx 反向代理转发:
```nginx
location /_AMapService/ {
set $args "$args&jscode=真实密钥";
proxy_pass https://restapi.amap.com/;
}
```
前端只需配置代理地址而不暴露密钥:
```ts
window._AMapSecurityConfig = { serviceHost: '/_AMapService' }
```