Files
microapp-vue3-interview/docs/interview-question-amap-loader.md

232 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 前端面试题:高德地图 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' }
```