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

8.3 KiB
Raw Permalink Blame History

前端面试题:高德地图 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. 配置文件

// 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(注意大小写)不能有空格
⑤ 环境变量 .envVITE_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如果多个子应用都需要 AMapAMapGlobal 会冲突吗?

期望回答: 如果多个子应用共用同一个域名,AMapGlobal 是模块级变量,在同一 JS 上下文中共享。如果子应用用 iframe 沙箱隔离(本项目的 vue3-app 配置了 iframe: true),则每个 iframe 有独立的 JS 上下文,各自加载自己的 AMap不会冲突。

追问 3window._AMapSecurityConfig 中还有一个 serviceHost 字段是干什么的?

期望回答: 用于代理模式。生产环境中不应把安全密钥写在前端代码里,可以通过 Nginx 反向代理转发:

location /_AMapService/ {
  set $args "$args&jscode=真实密钥";
  proxy_pass https://restapi.amap.com/;
}

前端只需配置代理地址而不暴露密钥:

window._AMapSecurityConfig = { serviceHost: '/_AMapService' }