# 前端面试题:高德地图 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 | null = null /** 全局对象缓存 — 成功后直接返回,避免再次 await */ let AMapGlobal: typeof AMap | null = null export async function loadAMap(): Promise { // ① 已加载过 → 直接返回 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 \| 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' } ↓ ↓ AMap.Driving 等服务可用 ❌ 错误顺序(写在 AMapLoader.load 之后): ← 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` 中加 `