高德地图添加轨迹规划和重放功能
This commit is contained in:
231
docs/interview-question-amap-loader.md
Normal file
231
docs/interview-question-amap-loader.md
Normal 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' }
|
||||
```
|
||||
Reference in New Issue
Block a user