8.3 KiB
8.3 KiB
前端面试题:高德地图 JSAPI 加载机制与安全密钥
题目描述
在 Vue 3 + Vite 项目中,使用 @amap/amap-jsapi-loader 动态加载高德地图 JSAPI 2.0。要求:
- 全局只加载一次 — 多个组件同时调用不会重复下载 ~600KB 脚本
- 加载失败可重试 — 不缓存失败的 Promise
- 安全密钥按需配置 — 2021/12/02 后申请的 Key 必须配置
securityJsCode - 安全密钥必须在脚本加载之前设置 — 否则 Driving/Geocoder 等服务报
INVALID_USER_SCODE
完整源码与解析
1. 配置文件
// 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. 加载器 — 单例模式
// 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 |
跨时间缓存 — 下载完成后,任意时刻再调用直接返回对象 |
知识点 ②:失败重试
.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 插件列表
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 反向代理转发:
location /_AMapService/ {
set $args "$args&jscode=真实密钥";
proxy_pass https://restapi.amap.com/;
}
前端只需配置代理地址而不暴露密钥:
window._AMapSecurityConfig = { serviceHost: '/_AMapService' }