news 2026/9/29 2:08:26

Vue 3 高德地图接入:Key、jscode、白屏与内存泄漏排查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue 3 高德地图接入:Key、jscode、白屏与内存泄漏排查

地图白屏这件事,几乎每个在 Vue 项目里接过高德地图的人都遇到过。第一次遇到的时候我以为是 Key 配错了,把控制台翻了个底朝天,报错信息是INVALID_USER_SCODE,看着像密钥问题,实际上那一次是因为我在组件里给地图容器写了个height: 100%,而它的父元素根本没有高度。后来接的项目多了,回头看,从申请开发者 Key 到在 Vue 里稳定跑起来,中间散落着一堆这种"知道就一分钟、不知道能查一晚上"的细节。

这篇内容对准的就是这条链路:怎么在高德开放平台申请个人开发者的 Key,2021 年底之后新增的安全密钥到底该怎么处理,以及拿到 Key 之后在 Vue 项目里(重点讲 Vue 3,Vue 2 的差异会顺带说)怎么加载、怎么封装、怎么避免实例被响应式系统拖垮。适合两类人看:一类是刚接触地图开发、连 Key 都没申请过的新手;另一类是把地图接进了项目、但一遇到白屏或者内存泄漏就抓瞎的中级开发者。所有代码示例都是可以直接抄进项目改改就用的结构,参数和坑点我会说清楚来龙去脉,而不是只丢一段代码给你。

1. 申请 Key 这一步,真正容易翻车的是安全密钥和配额

大部分人把"申请 Key"想得特别简单——注册账号、创建应用、复制一串字符,结束。但实际项目里 Key 相关的报错占了地图问题的大头,原因就在于申请流程里有两个东西经常被忽略:安全密钥(jscode)和配额边界。这两样没搞清楚,后面写再多代码都是白搭。

1.1 个人开发者认证和 Key 的创建路径

打开高德开放平台的控制台,路径是「应用管理」→「我的应用」→「创建新应用」。应用名称随便填,用途选一个贴近的即可,比如"出行""生活服务"。创建完应用之后,点「添加 Key」,这里有几个字段必须认真选:

  • Key 名称:写清楚用途,比如web-map-dev、web-map-prod,后面排查问题时能一眼看出这串 Key 属于哪个环境。
  • 服务平台:这个字段是新手最容易选错的地方。做浏览器端地图(也就是 Vue 项目里跑的),必须选Web 端(JS API)。如果你选成了 Web 服务,那拿到的是 RESTful 接口用的 Key,前端 JS API 加载时会直接报域名或类型错误。
  • 域名白名单:可以留空,也可以填你的开发域名。留空表示不做限制,本地开发最省事,但上线前强烈建议补上。

创建完成后你会看到两串东西,一串是Key(32 位字符),另一串在"安全密钥"区域,也就是jscode。很多人在这一步只复制了 Key,把 jscode 忽略了,然后代码跑起来报INVALID_USER_SCODE,回头一顿翻文档才发现漏了东西。

关于个人开发者的认证:平台对未认证的个人账号和完成实名认证的账号,配额是不一样的。我实测下来,做个人项目、学习、demo 演示这类场景,未认证的配额完全够用;但如果是一个有一定日活的小产品,还是尽早完成个人认证,配额会明显宽松一些。具体的调用量数字平台会不定期调整,建议以控制台里"配额管理"页面显示的实际数值为准,不要照搬网上几年前的博客。

提示:每个人的 Key 都是和账号绑定的,不要在公开仓库里提交自己的真实 Key 和 jscode。有些同学图省事直接写死在main.js里推到 GitHub,过几天发现调用量异常,多半是被扫走了。

1.2 为什么多了一串 jscode,它到底防的是什么

2021 年底之后,高德 JS API 2.0 引入了安全密钥机制。原因其实不难理解:Key 是一串明文,写在浏览器里等于公开,任何人都能从你的页面源码里把它抠出来,然后拿着你的 Key 去刷配额。加了 jscode 之后,请求签名多了一层,别人光有 Key 没法直接用。

在代码里的体现是这样,你需要在使用地图之前,往window上挂一个配置对象:

// 一定要在加载 JS API 之前执行 window._AMapSecurityConfig = { securityJsCode: '你申请到的 jscode' }

注意这句代码的执行时机。它必须在AMapLoader.load()或者引入的 JS API 脚本执行之前就跑完,否则地图初始化时会报INVALID_USER_SCODE。我见过最典型的一种写法是把这段配置写在某个组件的onMounted里,而地图加载又写在另一个更早执行的模块顶层,结果配置永远慢半拍。

如果你觉得把 jscode 直接放前端不舒服(这种直觉是对的),高德提供了另一种方式:设置代理服务器,把securityJsCode换成serviceHost,让请求经过你自己的后端转发。个人小项目用前者完全没问题,涉及商业数据或者对安全要求高的场景,走代理是更稳妥的选择,后面第 5 章会展开讲。

1.3 配额、并发和那些容易被误判的"超限"

配额这块要分清楚两个概念:地图加载次数和服务调用量。前者是你打开一个带地图的页面就算一次;后者是调用地理编码、路径规划这类服务接口的次数。它们各自独立计数。

做个人项目时最容易踩的一个坑是:本地开发时反复刷新页面,一天下来地图加载次数蹭蹭往上涨。因为每次热更新或者手动刷新都会重新加载一次地图。我一般的做法是开发阶段用一个专门的开发 Key,上线再用生产 Key,两个 Key 的配额分开统计,免得开发把生产的额度刷没了。

另外一个容易被误判的情况是并发限制。高德的 JS API 对同时发起的请求有并发上限,如果你在页面初始化时一口气调了十几个地理编码请求,后面的可能会被限流,返回的结果是空的但不是报错。这种问题不会给你明显的错误提示,只表现为"数据莫名其妙少了几条"。解决方案很简单,把批量请求做队列控制,每次并发不超过 5 个。

项目常见误区实际处理方式
服务平台选择选成 Web 服务浏览器端必须选 Web 端(JS API)
jscode 使用只用了 Key加载地图前挂到 window._AMapSecurityConfig
开发环境生产和开发共用一个 Key分开申请,配额独立统计
批量请求一次性全发出去控制并发数,建议不超过 5
域名白名单一直留空上线前补上生产域名

2. 在 Vue 里加载 JS API:三种方式,我为什么只推荐一种

Key 拿到手之后,下一步是把它加载进 Vue 项目。这里有个前置认知:高德 JS API 不是 npm 上那种可以直接 import 的模块,它是一个浏览器端脚本,需要通过 script 标签或动态注入的方式加载。围绕这个限制,社区里演化出了三种常见做法,实际体验差别很大,我一个个说。

2.1 直接在 index.html 里挂 script 标签

最朴素的方式,在public/index.html或者 Vite 项目的index.html里塞一行:

<script src="https://webapi.amap.com/maps?v=2.0&key=你的Key"></script>

然后在组件里直接用全局的AMap对象。这方式的好处是简单直接,坏处也很明显:

第一,Key 硬编码在 HTML 里,切换环境很麻烦。开发用的 Key 和生产用的 Key 不一样,你总不能在打包时手动改 HTML。第二,脚本是同步加载的,如果网络慢,会阻塞页面渲染。第三,也是我最不能忍的一点,你无法在代码里精确控制加载的时机。比如某些页面根本用不到地图,但脚本照样加载了,白白浪费一次加载次数——而地图加载次数是要计配额的。

当然它也不是一无是处。如果整个应用就一个页面用地图,而且对首屏性能不敏感,这么写最省事。但只要是正经的多页面项目,我都不建议。

2.2 @amap/amap-jsapi-loader:目前最省心的方案

这是高德官方提供的加载器,通过 npm 安装:

npm install @amap/amap-jsapi-loader --save # 或者 pnpm add @amap/amap-jsapi-loader

用起来是 Promise 风格的:

import AMapLoader from '@amap/amap-jsapi-loader' AMapLoader.load({ key: '你的Key', version: '2.0', plugins: ['AMap.Scale', 'AMap.ToolBar'] }).then((AMap) => { const map = new AMap.Map('container', { zoom: 11, center: [116.397428, 39.90923] }) }).catch((e) => { console.error('地图加载失败', e) })

它内部做了几件有价值的事情:按需加载(只有调用 load 才会去拉脚本)、避免重复加载(同一个页面多次调用 load 只会真实加载一次)、插件预加载(plugins 参数里的插件会和主脚本一起加载,省去后续的异步请求)。

关于版本号,2.0是当前的推荐版本,它支持了更现代的渲染方式和 3D 视角。老项目里常见的1.4.15属于上一代,语法有差异,比如 1.x 时代没有 jscode 的概念。新项目直接上 2.0,别犹豫。

有一点需要说明:AMapLoader.load()是单例的。也就是说,你在 A 组件里传了plugins: ['AMap.Scale']加载完,B 组件再调用 load 传别的插件,主脚本不会重新加载,但插件会按需补上。这个机制大部分时候是好事,但如果你在 A 组件里传的 Key 和 B 组件不一样,第二次调用不会生效——它会复用第一次的配置。所以 Key 要统一,最好抽成一个配置常量。

2.3 在 Vite 项目里的几个细节

用 Vite 的同学会遇到一个具体问题:@amap/amap-jsapi-loader在开发模式下的预构建(pre-bundling)偶尔会报错,提示找不到某些依赖。我遇到过的解决方式是在vite.config.js里把它排除掉:

// vite.config.js export default defineConfig({ optimizeDeps: { exclude: ['@amap/amap-jsapi-loader'] } })

还有 SSR 场景要注意。如果你的项目用了 Nuxt 或者其它服务端渲染方案,AMapLoader.load()在服务端执行会因为找不到document直接报错。处理方式是把加载逻辑包在onMounted里,或者用process.client/import.meta.env.SSR做判断。这个坑不算高德独有,所有依赖浏览器 API 的库都一样。

最后说下 TypeScript。官方类型包是@types/amap-js-api,但这个包的更新频率一般,2.0 的很多新 API 都没有类型定义。我的做法是不强求类型完整,给AMap变量声明成any或者自己写一份精简的.d.ts,把常用的类(Map、Marker、Geocoder)声明上就够了。为了类型完整性去跟类型包较劲,性价比不高。

加载方式按需加载环境切换类型支持推荐度
index.html 直接引入不支持麻烦弱不推荐
@amap/amap-jsapi-loader支持方便中强烈推荐
自己封装动态 script 注入支持方便弱有特殊需求时用

3. 把地图封成组件:生命周期和响应式是两个大坑

能加载出来只是开始。真正在项目里用,你大概率会把地图封装成一个可复用组件,比如<AmapView />。封装的时候,Vue 的响应式系统和地图实例之间有一层微妙的冲突,处理不好会导致地图卡顿、标记点不显示、甚至内存泄漏。这一章讲的就是这些。

3.1 容器高度为 0:白屏问题的头号原因

先把最经典的坑说了。地图组件写完,页面上一片空白,控制台没有任何报错。这时候你几乎可以立刻怀疑一件事:地图容器的高度是不是 0。

高德地图的初始化依赖容器元素的实际尺寸。如果容器的offsetHeight是 0,地图虽然"创建成功"了,但你什么都看不见。而这个 0 高度通常不是容器自己造成的,是它的父级链条上某一层没有高度。

常见的错误写法是这样:

<div class="map-wrapper"> <div id="map-container"></div> </div>
.map-wrapper { /* 没有设置高度 */ } #map-container { width: 100%; height: 100%; /* 100% 的 0 还是 0 */ }

height: 100%是相对于父元素的,父元素没高度,子元素自然也是 0。解决方案是给容器链上至少有一层是明确的高度,比如height: 500px或者height: 100vh,或者用 flex 布局让父级被撑开:

.map-wrapper { width: 100%; height: 60vh; /* 给一个明确的视口高度 */ } #map-container { width: 100%; height: 100%; }

还有一种更隐蔽的情况:容器初始时是隐藏的(比如在 Tab 切换的第二个标签页里),display: none的元素尺寸是 0,地图在隐藏状态下初始化就会出问题。正确的做法是等容器显示之后再初始化地图,或者初始化之后调用map.resize()让它重新计算尺寸。我一般会在 Tab 切换的回调里手动调一次resize(),这个动作几乎零成本,但能省掉很多莫名其妙的显示问题。

3.2 shallowRef 和 markRaw:别让 Proxy 代理地图实例

Vue 3 用reactive和ref包裹对象时,会创建一个 Proxy 代理。这个特性对普通数据对象是好事,但用在 AMap 的实例上就是灾难。

问题出在两点。第一,AMap 的实例内部有大量非公开的私有属性和方法,Proxy 拦截会影响它们的行为,表现为地图上的交互变得迟钝、拖拽卡顿。第二,地图实例上挂着非常深的内部对象树,Proxy 会递归地做响应式转换,初始化一次可能要多花几百毫秒。

正确的做法是用shallowRef或者markRaw:

import { shallowRef, markRaw, onMounted, onBeforeUnmount } from 'vue' import AMapLoader from '@amap/amap-jsapi-loader' const mapRef = shallowRef(null) onMounted(async () => { const AMap = await AMapLoader.load({ key: '你的Key', version: '2.0' }) // markRaw 再包一层,双保险 mapRef.value = markRaw(new AMap.Map('map-container', { zoom: 11, center: [116.397428, 39.90923] })) })

shallowRef只对.value的引用变化做追踪,不会深入代理内部对象;markRaw则是明确告诉 Vue"这个对象永远不要变成响应式"。两个一起用,基本可以杜绝这类问题。同样的道理适用于 Marker、Polyline 这类覆盖物实例,如果你需要把它们存进数组管理,数组本身可以是响应式的,但数组里的实例建议用markRaw处理。

注意:如果你是在 Vue 2 里开发,虽然 Options API 的响应式机制不同,但把地图实例直接挂在data()里返回同样有性能问题,因为 Vue 2 会递归遍历对象的所有属性做defineProperty。推荐的写法是在created或者mounted里挂到this.$options或者一个非响应式的局部变量上。

3.3 组件卸载时必须做的清理动作

地图实例占用的资源不小,包括 DOM 节点、事件监听、还有内部维持的 WebGL/Canvas 上下文。如果组件销毁时不做清理,地图实例会一直挂在内存里。单页面应用里来回切换几次带地图的路由,内存占用就很可观了。

标准清理动作有两步:

onBeforeUnmount(() => { if (mapRef.value) { mapRef.value.destroy() // 销毁地图实例,释放内部资源 mapRef.value = null } })

map.destroy()会连带销毁地图上的所有覆盖物和事件监听,所以不用再单独去清理每个 Marker。但有一类东西它管不了:你自己用addEventListener绑在 window 或 document 上的监听。比如监听窗口 resize 让地图自适应,这种要自己解绑。

再补充一点,如果你用的是 Vue 的<KeepAlive>缓存了地图组件,onBeforeUnmount不会触发(因为组件被缓存了,是 deactivated 状态)。这时候需要用onActivated/onDeactivated,在 deactivated 里做销毁或者暂停动画,activated 里重新初始化。KeepAlive 加地图这个组合,出问题的概率比普通场景高不少,能不用就尽量别用。

4. 插件、覆盖物和地理编码:常用能力的落地细节

地图能显示、组件生命周期没问题之后,接下来就是往上面加功能了。这块我挑几个最常用的能力讲,重点不在 API 怎么调,而在那些文档里不写、但实际会绊住你的地方。

4.1 插件动态加载的正确姿势

高德的很多功能是以插件形式提供的,比如比例尺(Scale)、工具栏(ToolBar)、测距(RangingTool)、热力图(HeatMap)。插件有两种加载方式:

一是在AMapLoader.load()的plugins参数里预先声明,好处是一次性加载完;二是用到的时候再加载:

AMap.plugin(['AMap.ToolBar', 'AMap.Scale'], () => { // 回调里再用这两个类 const toolbar = new AMap.ToolBar() const scale = new AMap.Scale() mapRef.value.addControl(toolbar) mapRef.value.addControl(scale) })

第二种方式适合插件多、但每个页面只用其中几个的场景。这里有个顺序问题要注意:AMap.plugin是异步的,回调执行完之前那些类还不存在。我见过有人这么写:

// 错误写法 AMap.plugin(['AMap.HeatMap']) const heatMap = new AMap.HeatMap(mapRef.value) // 报错,AMap.HeatMap 是 undefined

纠正的方式就是把依赖插件的代码放进回调,或者用 Promise 包一层配合 async/await。热力图、轨迹回放、行政区划查询这几个插件,是动态加载出问题的高发区,因为它们的类名和插件名容易对不上,比如行政区划查询的插件名是AMap.DistrictSearch,构造函数也是AMap.DistrictSearch,但热力图插件名是AMap.HeatMap,个别版本的构造函数名有出入,建议加载后先console.log一下确认。

4.2 覆盖物管理与信息窗体

往地图上加标记点是最高频的操作。简单的场景用AMap.Marker就够,但标记点一多(比如几百上千个),性能问题就来了。每一个 Marker 都是一个真实的 DOM 节点,上千个 DOM 节点叠加在地图上,拖动地图时会明显掉帧。

这种量级下要换方案,用点聚合(AMap.MarkerCluster)或者海量点(AMap.MassMarks)。聚合插件适合展示密度分布,它会自动把靠近的标记合并成一个带数字的圆点;海量点适合纯展示、不需要单个点击交互的场景,它用 Canvas 渲染,性能比 DOM 方案高一个数量级。选哪个取决于你的交互需求,需要点单个标记弹窗的就用聚合,纯看分布的就用海量点。

信息窗体(AMap.InfoWindow)有个细节值得说:它默认是单例模式的思路,也就是一个 InfoWindow 实例在同一时间只能显示在一个位置。如果你给每个标记都new一个 InfoWindow,点击时会发现同时弹出好几个,或者干脆乱套。正确做法是全局只维护一个 InfoWindow 实例,点击标记时调用setContent()换内容,再open(map, position):

const infoWindow = new AMap.InfoWindow({ offset: new AMap.Pixel(0, -30) }) marker.on('click', (e) => { infoWindow.setContent(`<div class="poi-card">${data.name}</div>`) infoWindow.open(mapRef.value, e.target.getPosition()) })

信息窗体的内容如果是 HTML 字符串,样式记得加scoped之外的处理。因为你用 innerHTML 塞进去的 DOM 拿不到 Vue 组件的 scoped 样式,得单独写一份全局样式,或者用:deep()穿透。这个小问题卡过我不止一次。

4.3 地理编码:地址和坐标的互转

地理编码(Geocoder)负责把文字地址转成经纬度,逆地理编码反过来。做搜索、定位回显这类功能基本都要用。

AMap.plugin(['AMap.Geocoder'], () => { const geocoder = new AMap.Geocoder({ city: '全国' }) // 地址转坐标 geocoder.getLocation('北京市朝阳区某个具体地址', (status, result) => { if (status === 'complete' && result.geocodes.length) { const { location } = result.geocodes[0] mapRef.value.setCenter([location.lng, location.lat]) } }) // 坐标转地址 geocoder.getAddress([116.397428, 39.90923], (status, result) => { if (status === 'complete') { console.log(result.regeocode.formattedAddress) } }) })

实际使用中,地理编码的返回结果不一定有值。地址写得越模糊(比如只写"朝阳区"),返回的候选越多,geocodes数组可能有好几条;写得越精确,可能一条都没有。所以判断条件不能只写status === 'complete',还要检查数组长度。另外逆地理编码返回的formattedAddress是一个拼接好的完整地址,如果你需要分离省市区,得从addressComponent里逐个字段取,那个结构比较深,建议先打日志看清楚。

批量地理编码要注意前面提到的并发问题。如果你有 100 个地址要转,不要在一个循环里同时发起 100 个请求,用队列分批处理。我一般写个简单的并发控制函数,每批 5 个,跑完一批再跑下一批。

4.4 一些视觉效果插件的取舍

热词里出现了"雷达扩散效果""矩阵树图"这类词,我顺带说下视觉效果的实现思路。雷达扩散、波纹、轨迹动画这类效果,高德官方插件并没有直接提供,常见做法是用AMap.CircleMarker或者自定义AMap.Marker配合 CSS 动画来做。核心是把一个带 CSS keyframes 的元素作为 Marker 的 content 塞进去,让 CSS 负责动画,地图只负责定位。这样性能最好,也最好调试。

把动画交给 CSS 而不是用 JS 定时器逐帧计算位置,是我踩过几次坑之后固定下来的习惯。用setInterval改 Marker 的 position 会导致地图频繁重绘,帧率掉得厉害,而 CSS 动画由浏览器合成层处理,基本不占主线程。

功能推荐方案不推荐的做法原因
少量标记AMap.Marker-简单直接
上千标记MarkerCluster / MassMarks逐个 new MarkerDOM 节点过多掉帧
弹窗单例 InfoWindow + setContent每标记一个 InfoWindow多实例互相干扰
动画效果CSS keyframes + Marker contentsetInterval 改 position频繁重绘,性能差
批量地理编码队列控制并发循环内全部发起触发并发限流,结果丢失

5. 上线前的检查清单,以及我踩过的几个真实坑

代码在本地跑通和在生产环境稳定运行,中间还有一段距离。这一章把上线前该检查的东西列出来,再分享几个我自己踩过、印象比较深的坑,都是那种文档里不会写、只有实际撞上才知道的。

5.1 报错码的含义与定位思路

高德返回的错误码不算多,记住几个高频的能省很多时间:

  • INVALID_USER_SCODE:安全密钥相关。要么是 jscode 没配,要么是配的时机太晚,要么是 Key 和 jscode 不是同一个应用下的。前两个是绝大多数情况。
  • INVALID_USER_KEY:Key 本身有问题,可能是复制错了,也可能是服务平台类型选错。
  • USER_KEY_PLATFROM_ILLEGAL:Key 的服务平台和实际使用场景不匹配,比如拿 Web 服务的 Key 来加载 JS API。
  • DAILY_QUERY_OVER_LIMIT:配额用完了。开发阶段遇到这个,十有八九是本地反复刷新刷爆的,换个开发 Key 或者等次日重置。
  • INVALID_PARAMS:参数格式错误,常见于经纬度传反了,高德要求的是[经度, 纬度],也就是[lng, lat]。

排查顺序我固定按这个走:先看控制台的报错码,对照上表定位到大类;然后检查 Key 和 jscode 的配置时机;再看容器尺寸;最后看插件加载顺序。按这个顺序走,八成的问题十分钟内能定位到。

5.2 生产环境的 jscode 到底该放哪

回到第 1 章留的那个问题:jscode 一定要暴露在前端吗?严格来说,直接放在前端的写法对个人项目、内部系统是可接受的,因为它的作用只是"防止别人拿着你的 Key 随便用",而不是"保护高价值数据"。但这个安全等级确实不高。

更稳妥的方案是走代理。高德支持你配置一个自己的服务端地址,地图相关的服务请求会先发到你的服务器,由服务器带上 jscode 转发给高德。前端只配置:

window._AMapSecurityConfig = { serviceHost: 'https://你的域名/_AMapService' }

服务端需要做的转发逻辑,各地的实现方式差不多,核心是把请求原样转发并且在 URL 里补上 jscode。如果你用的是 Node 中间件,可以在网关层统一处理。这样做的好处是 jscode 永远不出现在浏览器里,即使有人扒了你的 Key,也用不了你的服务配额。

代价是多了一层网络转发,请求延迟会略微增加,而且你得维护这个转发服务。个人项目权衡下来,如果只是学习或者小规模应用,直接放前端问题不大;一旦涉及商业用途或者数据敏感,就走代理。

5.3 几个我踩过的坑,写出来让你少走弯路

第一个坑,热更新导致地图重复初始化。开发阶段用 Vite 的 HMR,改一次代码组件重新挂载一次,但地图容器里残留的旧地图实例没被销毁,结果是多个地图叠在一起,交互混乱。根本原因是 HMR 时onBeforeUnmount不一定按预期执行。我的处理方式是在初始化地图前先检查容器里有没有残留内容,有就清空:

const container = document.getElementById('map-container') if (container && container.innerHTML) { container.innerHTML = '' }

第二个坑,移动端地图和页面滚动冲突。地图是全屏或者大面积的时候,用户想上下滑动页面,手指落在地图上就变成了拖动地图,页面滑不动。解决方式是给地图容器加 CSStouch-action控制,或者在移动端加一个"解锁"按钮,默认让地图不响应触摸,点了按钮才允许拖动。这个交互设计在移动端几乎是必须的,不然用户体验很割裂。

第三个坑,Key 的域名白名单上线后忘了配。本地开发白名单是空的,一切正常;上线之后发现地图全部加载失败。如果你给 Key 配了白名单,一定记得把生产域名加进去,包括带 www 和不带 www 的两种写法,以及如果有子域名也要一并考虑。这个坑犯一次要排查半天,因为控制台只是笼统地报 Key 相关错误。

第四个坑,地图实例在多个组件间传递时被重复创建。如果页面里有两个组件都要操作同一张地图,不要各自new AMap.Map,那会创建两个实例。正确做法是用 provide/inject 或者 Pinia 把地图实例共享出去,确保全局只有一份。共享的时候记得用 shallowRef 存,理由和 3.2 节说的一样。

5.4 一个可以复用的初始化模板

最后给一个我项目里常用的初始化模板骨架,去掉业务逻辑之后大概长这样:

import { shallowRef, markRaw, onMounted, onBeforeUnmount, ref } from 'vue' import AMapLoader from '@amap/amap-jsapi-loader' export function useAmap(containerId, options = {}) { const map = shallowRef(null) const ready = ref(false) let AMapInstance = null onMounted(async () => { window._AMapSecurityConfig = { securityJsCode: import.meta.env.VITE_AMAP_SECURITY_CODE } AMapInstance = await AMapLoader.load({ key: import.meta.env.VITE_AMAP_KEY, version: '2.0', plugins: options.plugins || [] }) map.value = markRaw(new AMapInstance.Map(containerId, { zoom: options.zoom || 11, center: options.center || [116.397428, 39.90923], ...options.mapOptions })) ready.value = true }) onBeforeUnmount(() => { map.value?.destroy() map.value = null }) return { map, ready, AMap: () => AMapInstance } }

Key 和 jscode 走环境变量,不写死在代码里,这样开发和生产用不同的.env文件就能自动切换,也避免了密钥进仓库。这个模板用 Composition API 的useXxx形式组织,适合多个页面复用;如果只是单个页面用,直接写在组件里也完全没问题,不必为了"看起来优雅"强行抽 hook。

踩过这些坑之后我最大的体会是:地图这类依赖浏览器尺寸、实例生命周期和异步脚本库的技术,出问题时的表现往往非常安静——不报错、不崩溃,就是不显示或者卡顿。所以排查思路要养成固定顺序,从配置(Key、jscode、域名)到容器(尺寸、层级、是否隐藏)再到实例(是否被响应式代理、是否重复创建、卸载是否销毁),一层层过。这套顺序不是理论,是我自己撞了无数次南墙之后总结出来的,基本能覆盖九成以上的地图问题。剩下那一成,多半是版本差异或者插件本身的边界情况,去官方文档的版本说明里翻一翻,通常能找到答案。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 2:07:37

支付宝代扣接口签约避坑指南:从申请到联调全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 2:07:07

OpenStack IaaS云管理平台部署实战:从虚拟化原理到实例创建

简介&#xff1a;基于OpenStack的IaaS云管理平台的设计与实现毕业设计论文&#xff0c;是一份可直接参考的完整毕业论文文档&#xff0c;面向云计算方向的学生、开发者和需要部署私有云的运维人员。论文从云计算与IaaS的发展背景切入&#xff0c;梳理基础设施即服务的低成本、高…

作者头像 李华
网站建设 2026/9/29 2:06:40

Nordic蓝牙SoC选型与NimBLE移植实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 2:05:21

STM32+BIH1750+OLED光照监测系统实战设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 2:05:20

机器学习数据预处理实战:清洗、转换、降维与数据泄漏防范

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华