
上周帮朋友收拾一个后台项目需求听着特别简单页面上放一张地图点一下拿坐标顺手把地址反查出来塞进表单里。就这么点事他从晚上八点折腾到十一点卡在第一步——高德地图的个人开发者 key 怎么建、建好之后往哪儿填、Vue 项目里怎么加载三个问题一个没跑掉。我接手之后二十分钟跑通所以这篇就把整条链路从零讲一遍怎么申请高德地图个人开发者 key怎么在 Vue 项目里把它用起来以及那些官方文档不写、但一定会撞上的坑。这篇文章适合三类人第一次给 Vue 项目接地图的前端、接过一次但配置总是记不住的开发者、以及想搞清楚为什么非要安全密钥这件事的人。代码以 Vue 3 Vite 为主Vue 2 的差异我会单独点出来。全程不需要什么高级前置知识会装依赖、会写组件就够了。1. 动手之前先把路线想清楚1.1 高德地图在 Vue 项目里到底承担什么角色很多人一上来就问怎么装地图组件其实先该问的是我要它干什么。地图在前端项目里通常是三类活一类是展示型比如门店分布、设备点位、配送范围只读不写一类是交互型让用户在图上点选、拖拽、圈选最后把坐标或地址回填到表单里还有一类是分析型画热力图、画轨迹、画围栏背后往往还挂着后端服务。这三类活对地图能力的要求完全不同。展示型只需要渲染 标记点 信息窗体交互型要多一步逆地理编码把经纬度换成某某路几号这种人类能读的地址分析型则会用上路径规划、海量点、自定义图层。先把你的场景归到哪一类想明白后面的依赖就只装你需要的插件地图初始化的体积能小一大截。还有一点容易被忽略地图容器本质上就是一个普通的 div高德在它上面盖了一层 canvas。所以 Vue 里的响应式系统跟地图实例是两套东西把AMap.Map的实例扔进ref()里深度监听只会让页面越来越卡。这个问题在后面第 3 章会具体讲怎么处理。1.2 三种集成方式别盲选Vue 项目里接高德目前主流就三条路各有各的适用面。第一条手动在 index.html 里挂 script 标签。最原始也最可控script srchttps://webapi.amap.com/maps?v2.0key你的key/script然后全局就有window.AMap了。优点是简单、无依赖缺点是同步阻塞、key 硬编码在 HTML 里、插件还得单独用AMap.plugin()再加载一次。小 demo 可以正经项目不建议。第二条用官方的amap/amap-jsapi-loader。这是目前最推荐的方案。它是一个 Promise 风格的加载器可以异步按需加载指定插件key 和插件列表都能通过参数传进去天然适配 Vite 和 Webpack 的按需加载逻辑。缺点是它只负责把 AMap 对象给你封装组件、管理生命周期这些活还得自己干。第三条用第三方组件库比如 vue-amap。优点是开箱即用el-amap这种标签写起来确实快缺点是版本迭代经常跟不上官方 JS API 的节奏插件覆盖不全遇到新能力还得绕回原生 API等于学两套东西。我的经验是如果项目超过三个月生命周期老老实实用官方 loader 自己封装前期多花两小时后期少踩半个月的坑。集成方式上手成本可控性长期维护推荐场景script 手动引入低低差单页 demo、静态页官方 jsapi-loader中高好绝大多数 Vue 项目第三方组件库低中一般快速原型、能力需求简单另外提一句容易混淆的小程序接入高德地图跟 Web 端 JS API 完全是两码事。小程序里一般是用wx.getLocation拿坐标再去调高德的 Web 服务 API 做逆地理编码中间还夹着域名白名单和 HTTPS 的要求。你在这篇文章里学的东西不能直接搬到小程序反过来也一样。2. 高德开放平台个人开发者 Key 的申请全流程2.1 注册、实名认证与账号类型的选择第一步是去高德开放平台注册账号。手机号 验证码就能注册但注册完只是游客状态必须完成实名认证才能创建 Key。个人认证走身份证 人脸识别通常几分钟就过企业认证要营业执照和对公信息慢一些但配额和可用的服务更多。这里有个选择要做用个人账号还是企业账号如果你只是自己练手、做课程作业、或者给一个还没上线的小项目试水个人账号完全够用免费配额对日常开发来说绰绰有余具体数值以控制台实时显示为准平台会调整。但如果项目要长期运营、要走商务合同、要开发票那从一开始就用企业账号省得后期迁移时 Key 全部作废、线上重新配置一遍。实名认证通过后进入控制台的应用管理这是所有配置的入口。我建议你在这个阶段就把「账号信息」页面的开发者 ID 记下来后面排查问题时客服经常会问。2.2 创建应用和新增 Key每个字段都别乱填控制台里的层级是应用 → Key。一个应用下可以挂多个 Key这个设计是为了方便你按端区分。点创建新应用名字随便起比如官网前端但含义要清晰因为半年后你回头看列表全是测试1测试2就傻了。应用建好之后点添加 Key这几个字段是重点Key 名称我习惯按「环境 端」命名例如web-prod、web-dev。因为开发和生产建议用两个 Key方便出问题时单独吊销某一个不至于一改就全站崩。服务平台这是最容易填错的一栏。Web 页面、Vue 项目选Web 端JS API。别选成Web 服务那是给后端调 REST 接口用的两者 Key 不通用选错了前端加载地图会直接报无权限。域名白名单填你的站点域名生产环境填正式域名开发环境可以填localhost。这个白名单是按域名做的调用来源校验不填也能跑但一旦有人把你的 Key 抄走挂到别的站上配额就替别人烧了。所以能填就填。附加服务如果不需要一律不勾。提交之后Key 就生成出来了一串 32 位的字母数字。把它当成密码对待别提交进 Git 仓库。2.3 安全密钥到底是什么为什么必须配这是新接触的人最困惑的一点。2021 年 12 月之后申请的 Key加载 JS API 时必须额外配一个安全密钥securityJsCode否则控制台会甩你一个INVALID_USER_SCODE错误地图白屏。原因其实不难理解前端 Key 是明文的F12 一看就有单靠它拦不住盗用。于是高德加了一层——Key 管你是谁安全密钥管这次请求确实来自你授权的来源。两者配对使用缺一不可。安全密钥跟 Key 在同一个页面生成就在 Key 那一行的下方。它有两种使用方式方式一前端直接配置。在加载地图之前往 window 上挂一个全局对象window._AMapSecurityConfig { securityJsCode: 你申请到的安全密钥 }这种方式简单但本质上密钥还是暴露在浏览器里只能起到提高盗用门槛的作用防不住有心人。方式二配代理服务器转发。前端请求自己的域名由后端反向代理到高德的接口把密钥留在服务端。这是官方推荐的生产方案安全性最高代价是要多写一段服务端配置。我的建议是个人项目、内部系统用方式一够了对外的商业项目老老实实上方式二。别嫌麻烦密钥泄露导致配额被刷爆、账单找上门那才是真麻烦。注意安全密钥和 Key 是绑定的重新生成 Key 之后安全密钥一定要同步更新否则前端表现就是昨天还好好的今天突然白屏。3. Vue 项目里接入高德地图的实操全流程3.1 工程初始化与依赖安装假设你手上已经有一个 Vue 3 Vite 的项目。如果是空目录先起一个npm create vitelatest amap-demo -- --template vue cd amap-demo npm install如果你还在用 Node 比较老的版本先确认一下环境node -v看版本npm -v看包管理器。Vite 对 Node 版本有要求版本太低会直接报错启动不了这类装了半天跑不起来的问题八成出在这里。然后装地图加载器npm install amap/amap-jsapi-loader --save这个包很小它做的事情就是帮你动态插入 script 标签、维护加载状态、把插件一起拉下来不会把整份高德 SDK 打进你的 bundle。接着在项目根目录建两个环境变量文件.env.development和.env.production# .env.development VITE_AMAP_KEY你的开发环境 Key VITE_AMAP_SECURITY_CODE你的开发环境安全密钥 # .env.production VITE_AMAP_KEY你的生产环境 Key VITE_AMAP_SECURITY_CODE你的生产环境安全密钥Vite 只会把VITE_开头的变量注入到客户端代码里这一点要在.gitignore里把.env.local之类的文件排掉同时给团队留一份.env.example说明要填哪些字段。提示环境变量里拿到的值永远是字符串别指望VITE_XXXfalse会自动变成布尔值这个坑在别的配置里也很常见。3.2 封装一个可复用的地图加载器直接在组件里写加载逻辑会导致每个用到地图的页面都加载一次、插件重复注册。正确做法是封装一个模块级的单例加载器// src/utils/amapLoader.js import AMapLoader from amap/amap-jsapi-loader let amapPromise null export function loadAMap() { // 已经加载过或正在加载直接复用同一个 Promise if (amapPromise) return amapPromise // 安全密钥必须在 load 之前挂上 window._AMapSecurityConfig { securityJsCode: import.meta.env.VITE_AMAP_SECURITY_CODE } amapPromise AMapLoader.load({ key: import.meta.env.VITE_AMAP_KEY, version: 2.0, plugins: [ AMap.Geocoder, AMap.Marker, AMap.InfoWindow, AMap.PlaceSearch ] }) return amapPromise }这个写法有三个要点值得说第一单例缓存的不是一个布尔值而是 Promise 本身。因为地图加载是异步的如果两个组件几乎同时挂载用布尔值判断会出现第一个还在加载中、第二个以为已经加载好的竞态。缓存 Promise两个组件 await 的是同一个对象天然安全。第二插件列表只写当前项目真正用到的。Geocoder逆地理编码、Marker标记点、InfoWindow信息窗体、PlaceSearchPOI 搜索是按需加载的不写进去就不会下载对应的脚本首屏体积能省不少。第三安全密钥的赋值必须在 load 之前写在后面就晚了会直接触发前面说的INVALID_USER_SCODE。3.3 把地图封装成组件生命周期一定要管住接下来写组件。Vue 3 用script setuptemplate div refmapRef classmap-box/div /template script setup import { ref, shallowRef, onMounted, onBeforeUnmount } from vue import { loadAMap } from /utils/amapLoader const emit defineEmits([picked]) const mapRef ref(null) const map shallowRef(null) // 关键用 shallowRef不要用 ref const geocoder shallowRef(null) const markers [] onMounted(async () { const AMap await loadAMap() map.value new AMap.Map(mapRef.value, { zoom: 15, center: [116.397428, 39.90923], viewMode: 2D }) geocoder.value new AMap.Geocoder({ city: 全国 }) map.value.on(click, (e) { const lng e.lnglat.getLng() const lat e.lnglat.getLat() addMarker(AMap, [lng, lat]) reverseGeocode([lng, lat]) }) }) function addMarker(AMap, position) { markers.forEach((m) m.setMap(null)) markers.length 0 const marker new AMap.Marker({ position, map: map.value, anchor: bottom-center }) markers.push(marker) } function reverseGeocode(lnglat) { geocoder.value.getAddress(lnglat, (status, result) { if (status complete result.regeocode) { const addr result.regeocode.formattedAddress emit(picked, { lnglat, address: addr }) } else { console.warn(逆地理编码失败, status, result) } }) } onBeforeUnmount(() { // 地图实例必须手动销毁否则会内存泄漏 if (map.value) { map.value.destroy() map.value null } geocoder.value null }) /script style scoped .map-box { width: 100%; height: 480px; } /style这段代码里有几个点我第一次写的时候全踩过shallowRef而不是ref。地图实例是一个庞大的对象内部有一堆循环引用和 DOM 节点。用ref的话 Vue 会尝试对它做深度响应式代理结果是页面每动一下地图就卡一下严重点直接栈溢出。shallowRef只代理最外层实例本身原样保留这是 Vue 里接第三方重型实例的标准姿势。map.destroy()必须调用。地图实例挂着 DOM 监听、定时器和 WebGL 上下文不销毁的话在单页应用里切换路由几十次浏览器内存能涨到几百兆。onBeforeUnmount里除了 destroy把持有实例的变量也置空帮 GC 一把。容器必须有明确高度。上面.map-box给了height: 480px这是硬性要求。高德地图初始化的时候会去读容器的高度如果父元素高度是auto或者0地图会渲染成一片空白控制台还没有任何报错。这个问题在打包后布局异常的场景里出现频率极高后面会再讲。逆地理编码的回调是 error-first 风格的先判断status complete再取result.regeocode.formattedAddress。有的地址精度只到街道formattedAddress可能不全需要的话可以再拼addressComponent里的省市区字段。3.4 环境变量、路由守卫与状态管理的配合地图选点通常不是孤立功能往往是表单流程的一环。假设用户点选择地址跳到地图页选完再跳回来坐标要带到上一页——这时候就需要状态管理或路由传参。简单场景用路由 query 就够router.push({ name: mapPicker, query: { from: order } })选完再router.back()配合 Pinia 存一下结果。如果项目已经用了 Vuex原理一样存进 mutation 即可Pinia和Vuex在存这种临时数据上没什么差别选团队顺手的那套就行别为了这个专门换。有一个路由层面的坑必须提醒如果你的页面被keep-alive缓存了onBeforeUnmount不会触发地图实例会一直留着。这时候要用onActivated/onDeactivated这对钩子来管地图的创建与销毁或者干脆在onDeactivated里手动map.destroy()onActivated里重新创建。我见过最离谱的一个案例是后台管理系统里开了十几个标签页每个都缓存地图最后浏览器直接卡死。另外如果地图页需要按路由参数定位到不同城市记得在参数变化时调map.setCenter()而不是重建实例watch一下路由参数就行。重建实例的代价远大于挪动中心点。4. 踩坑记录常见报错与排查思路速查4.1 错误码对照表先对着查高德的报错信息有时候非常沉默——白屏、不报错、或者只给个错误码。我把撞见过的攒成一张表现象大概率原因处理方式控制台INVALID_USER_SCODE安全密钥没配、配晚了、或与 Key 不匹配确认window._AMapSecurityConfig在 load 之前赋值INVALID_USER_KEYKey 填错、服务平台选错检查是不是把 Web 服务 Key 用到了 JS API地图区域全白无报错容器高度为 0给容器显式设置 height域名白名单报错当前域名不在白名单内开发时加 localhost上线前加正式域名打包后布局错乱容器尺寸被父级 flex 挤压或路由切换未重算加resize监听或改用固定高度移动端手势与页面滚动打架事件冒泡冲突容器上加touch-action: none局部处理Android 端逆地理编码报 10021通常是原生 SDK 的签名与 Key 不匹配核对包名与 SHA1 签名跟 Web 端不是一套配置最后一行特别说明一下10021 这类错误码在 Android 原生 SDK 里很常见根因是签名指纹和 Key 绑定的信息对不上。如果你是 Web 项目却搜到了这个错误码说明你搜错方向了Web 端不存在签名这一说问题一定在 Key、安全密钥或域名白名单上。4.2 打包部署和路由切换引发的诡异现象现象一本地开发好好的npm run build 之后地图不见了。八成是构建时环境变量没注入。检查一下.env.production是否存在于构建时的工作目录以及 CI 里有没有把变量传进去。另一个可能是 base 路径配置不对静态资源 404但这种通常控制台会报错容易区分。现象二从 A 页面进 B 页面地图只显示一半或者压缩成一条线。这是典型的容器尺寸计算时机问题。地图初始化时读到的容器宽度是初始值路由切换之后容器变宽了但地图不知道。解决办法是监听容器尺寸变化手动调用map.resize()const observer new ResizeObserver(() { map.value map.value.resize() }) observer.observe(mapRef.value) onBeforeUnmount(() { observer.disconnect() })比window.resize监听更靠谱因为它能捕捉到父容器变化引起的尺寸改变而不是只有窗口大小变化时才触发。现象三弹窗里放地图第一次打开正常关掉再打开就空白。弹窗关闭时 DOM 被销毁但地图实例没销毁第二次创建时容器 id 或引用冲突。严格按第 3 章的做法在关闭钩子里 destroy就能解决。4.3 几个容易被忽略的性能与体验细节标记点数量超过几百个别一个个 new Marker。每个 Marker 都是一个独立的 DOM 对象数量一上去帧率断崖式下跌。这种场景应该用AMap.MassMarks或者点聚合插件前者适合静态海量点后者适合缩放层级跨度大的场景。如果你只是想画一片区域的热度用AMap.HeatMap比堆标记点优雅得多。瓦片图层是个容易被低估的能力。高德默认的道路底图之外还支持卫星图、路网图层也可以通过AMap.TileLayer自定义瓦片。做园区内部地图、楼层图这类需求时把自制的瓦片图叠在底图上再用AMap.Bounds限制可视范围是个很实用的组合。要注意的是自定义瓦片的坐标系必须和高德的底图一致不然会出现偏移这个偏移在放大到 18 级以上时会非常明显。首屏别急着加载地图。地图 SDK 的体积不小如果地图在首屏之下用IntersectionObserver做懒加载滚到可视区域再初始化首屏时间能改善不少。这个优化对于内容型页面尤其值得做。内存泄漏要主动验。一个简单的自测方法在应用里反复进出地图页二十次打开 Chrome 的任务管理器看标签页的内存曲线。如果每次回来都没降下去说明 destroy 没生效或者事件监听没解绑。实操心得我现在的习惯是凡是地图组件一律先写好onBeforeUnmount里的清理逻辑再写初始化。顺序反过来写十有八九会忘。最后再说一个我自己的经验。地图项目的排查成本八成花在配置上而不是代码上——Key、安全密钥、域名白名单、服务平台类型这四个东西只要有一个对不上表现都是白屏而且报错信息往往指向不明。所以我现在接新项目的第一件事不是写组件而是新建一个只有一张地图的空白页把配置链路先跑通。链路通了再往上叠业务逻辑出问题时就能确定是业务代码的锅排查范围立刻缩小一半。这个习惯帮我省下的时间比任何调试技巧都多。