ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

百度地图自定义点标记与文本标注实战:锚点、性能与跨端接入

百度地图自定义点标记与文本标注实战:锚点、性能与跨端接入 做门店地图、设备分布图、物流轨迹这类需求最后都会落到同一个动作上在百度地图上放点。默认的红水滴图标能跑通流程但只要设计师丢过来一张带圆角阴影的 png或者产品补一句每个点下面要显示店名和价格事情就开始变复杂了。我前前后后做过七八个带地图的项目从最早用 v3.0 硬怼到后来换 GL 版本中间踩的坑基本都集中在三件事上百度地图自定义点标记怎么让图标针尖精确对准经纬度、文本标注怎么让文字不歪不糊不打架、以及点标记数量一上去之后整个页面怎么不卡成幻灯片。这三个东西看着简单实际每一个都有若干反直觉的细节尤其是坐标系、锚点偏移和覆盖物层级这三块文档里写得都不算清楚。这篇内容会从最基础的概念拆分讲起把 Marker点标记、Label文本标注和自定义 Overlay覆盖物的职责边界先划清楚然后一路讲到图标替换、文字样式、批量渲染、跨端接入微信小程序、Qt 桌面端、WordPress 站点以及上线前的自检清单。代码基于百度地图 JavaScript APIGL 版本和 v3.0 都会给到对应的写法差异。适合正在做地图展示功能的前端、全栈或者需要把地图嵌进既有系统的开发者也适合刚接触地图 API、被锚点和坐标系绕晕的同学。1. 三个概念别混Marker 是图形Label 是文字Overlay 才是那个什么都能塞的容器很多同学一开始的困惑不是怎么写代码而是我到底该用哪个类。百度地图把地图上的可交互元素统称为覆盖物OverlayMarker、Label、Circle、Polygon 全都是覆盖物的子类。理清它们的关系后面选型就不会乱。1.1 各自的职责边界Marker点标记最基础的点本质是一张图片加一个坐标。默认长得像红色水滴可以通过Icon换成任意图片。它支持点击、拖拽、旋转是一个点代表一个实体场景的首选。Label文本标注一个带样式的文本块绑定在某个坐标上。它没有图片只有文字和 CSS 样式。它的优势是轻一个 DOM 节点就能放下几十个字的说明适合做店名、价格、状态提示。自定义 Overlay当你需要在一个点上同时放图片、文字、按钮、进度条甚至让它随着数据变化重新渲染Marker 和 Label 拼起来会非常别扭。这时候直接继承Overlay自己写 DOM自由度最高。下面这张表是我在选型时常用的判断依据基本上看完就能定下来用哪个需求推荐方案原因换个漂亮的图标Marker Icon改一个参数就行成本最低图标 下方一行店名Marker Label 组合两者都能独立控制位置和层级图标 店名 评分 按钮自定义 Overlay单点内多元素布局Marker 撑不住纯文字标注如区域名Label不需要图片资源加载快5000 个以上静态点聚合或海量点图层逐点建 DOM 必然卡死几万个点的热力分布海量点图层canvas 绘制不吃 DOM1.2 一个真实场景的选择过程我做过一个连锁门店的地图页需求是这样的地图上每个门店显示一个自定义图标图标右上角有个小红点表示营业中鼠标悬停弹出卡片显示店名、地址、电话点击卡片里的按钮跳转到详情页。第一版我用的是 Marker Label 的组合Marker 放图标Label 放店名。做出来之后问题立刻出现——小红点没法加除非再叠一个 Label但那个 Label 需要精确摆到图标的右上角一改图标尺寸就得重新调偏移维护成本极高。悬停卡片更是没办法Label 的样式撑不起一个带阴影和按钮的卡片。第二版我改成自定义 Overlay一个 DOM 容器里放图标、角标、卡片全部用 CSS 控制悬停事件直接绑在 DOM 上一行mouseenter就搞定。代码量比第一版多了大概三十行但后续加需求变得非常轻松。这个经历给我的结论是只要一个点上承载的元素超过两个就别硬凑直接写 Overlay。Marker 和 Label 是给简单场景用的它们的价值在于开箱即用一旦你开始围绕它们做各种 hack说明方向已经错了。2. AK 与初始化白名单、容器高度、脚本版本这三处最容易卡住一整天地图跑不起来的原因八成不在业务代码里而是在初始化的这三步上。我见过太多人排查半天业务逻辑最后发现是 AK 白名单没配。2.1 脚本引入先确认你用的是哪一代百度地图 JS API 目前主流是两代老的 v3.0 和新的 GL 版本webgl。两者的命名空间不同API 形态也有差异混用会直接报错。!-- GL 版本命名空间是 BMapGL -- script srchttps://api.map.baidu.com/api?typewebglv1.0ak你的AK/script !-- v3.0命名空间是 BMap -- script srchttps://api.map.baidu.com/api?v3.0ak你的AK/script选哪个我的建议是新项目一律用 GL。原因有三个GL 版本基于 WebGL 渲染大量覆盖物时性能明显更好它内置了三维视角、倾斜、旋转这些能力后面如果产品想加个俯视角度不用重构另外 GL 版本的 Marker 支持旋转做车辆方向、风向这类场景省了很多事。v3.0 现在更多是维护老项目时才会碰到。需要注意的一点是GL 版本和 v3.0 的某些扩展库比如聚合不能混用如果你的项目里同时引了两个版本的脚本会大概率出现BMapGL is not defined或者覆盖物画不出来的情况。我踩过一次原因是某个老组件偷偷引了 v3.0导致后加载的 GL 覆盖了全局对象排查了整整一个下午。2.2 AK 白名单本地调试和线上域名的差别对待申请 AK 的时候会让你填Referer 白名单。这里有几个非常实际的坑本地调试file://协议打开的页面Referer 是空的很多情况下会直接被拒。解决办法是起一个本地静态服务用http://localhost:端口访问。用file://硬开页面再抱怨地图不出来是新手最常见的时间浪费。域名要写全example.com和www.example.com是两个不同的 Referer。用通配符*.example.com也没法覆盖裸域两个都得加。我一般会写成example.com/*和*.example.com/*两条。不要图省事留空白名单留空虽然能跑但等于把 AK 暴露给任何人一旦被人拿去刷配额你的服务会直接不可用。这不是危言耸听我有同事的 AK 被扒走之后一天之内配额跑光。多环境多 AK开发、测试、生产各申请一个 AK方便定位问题也方便出事了单独吊销某一个。2.3 容器高度那个让地图莫名其妙不显示的元凶地图容器必须有明确的高度这一点文档里写了但很容易被忽略。如果你写的是div idmap/div然后 CSS 里只有width: 100%没有高度地图初始化会成功不报错但页面上什么都看不到。因为容器高度是 0地图画在了 0 像素的空间里。正确做法是#map { width: 100%; height: 600px; /* 或者用 flex 撑开、vh 单位 */ }我一般会用flex: 1配合父容器的display: flex; flex-direction: column;这样地图能自适应剩余空间。但如果父容器的高度本身也是 auto那还是要显式给一个高度兜底。初始化代码本身很朴素const map new BMapGL.Map(map, { enableMapClick: false // 关掉底图点击避免和自定义点击事件打架 }); map.centerAndZoom(new BMapGL.Point(116.404, 39.915), 15); map.enableScrollWheelZoom(true);enableMapClick: false这个参数值得单独说一句。默认情况下地图底图的 POI 点是可以点击的点了会弹出一个默认的信息框。如果你的自定义标记压在 POI 上点击事件可能会被底图抢走。关掉它可以省掉不少为什么我的点击没反应的困惑。3. 自定义点标记从一张 png 到精确落在经纬度上的针尖这一节是重点。把默认红点换成自己的图标代码就三行但要让图标精确地落对位置就得理解三个参数之间的关系。3.1 Icon 的 size、imageSize、anchor 到底谁管什么const icon new BMapGL.Icon( https://your-cdn.com/marker-shop.png, new BMapGL.Size(32, 40), // size图标在地图上显示的尺寸 { anchor: new BMapGL.Size(16, 40), // anchor图标的哪个点对齐到经纬度 imageSize: new BMapGL.Size(64, 80) // imageSize图片文件本身的像素尺寸 } );这三个参数经常被搞混我用一个类比解释imageSize是这张图的原始尺寸。如果你做的是 2 倍图高清屏适配原图可能是 64x80但你想让它显示成 32x40这时候 imageSize 写 64x80size 写 32x40浏览器就会把它压缩高清屏上依然清晰。size是它在页面上的显示尺寸。anchor是钉子的位置。默认情况下 anchor 是图标的中心点也就是说坐标落在图标的中间。但地图图钉通常是底部尖端对准位置的所以你要把 anchor 设成(宽度/2, 高度)让底部中心对准坐标。我最早不知道 anchor 这回事用默认中心点做出来的图标视觉上永远偏移半个图标的高度调了很久还以为是坐标转错了。后来把 anchor 一改瞬间就对上了。const point new BMapGL.Point(116.404, 39.915); const marker new BMapGL.Marker(point, { icon: icon }); map.addOverlay(marker);3.2 缩放级别变化时切换图标地图缩放的时候同样大小的图标在视觉上会显得忽大忽小。更好的体验是按缩放级别切换不同尺寸的图或者根据业务逻辑切换形态比如缩小的时候显示小圆点放大了才显示完整图钉。map.addEventListener(zoomend, () { const zoom map.getZoom(); markers.forEach(marker { marker.setIcon(zoom 15 ? bigIcon : smallIcon); }); });注意zoomend事件在高频缩放时会连续触发如果标记数量多把setIcon全部跑一遍会很卡。可以加一个防抖或者只对当前视口内的标记做切换配合map.getBounds()做范围过滤。3.3 拖拽、旋转与点击事件marker.enableDragging(); // 开启拖拽 marker.setRotation(45); // GL 版本支持旋转单位是角度 marker.setTitle(门店A); // 鼠标悬停时的原生 tooltip marker.addEventListener(click, (e) { // e.point 是点击位置的坐标e.target 是 marker 实例 showDetailPanel(marker.getPosition()); });旋转这个能力在车辆监控、无人机航线这类场景里特别好用。方向角从后端拿到之后直接setRotation即可。但要注意旋转是围绕 anchor 点转的如果 anchor 设得不对旋转起来图标会甩出去这也是我开始时踩过的坑。拖拽则要注意数据同步拖完之后必须把新的坐标写回你的数据源否则后面重新渲染会跳回原位。marker.addEventListener(dragend, (e) { const p e.point; updateStore(marker.customId, { lng: p.lng, lat: p.lat }); });我习惯在创建 marker 的时候挂一个customId属性上去这样在事件回调里能直接反查到对应的业务数据。用数组下标反查是不靠谱的一旦数据顺序变了就会张冠李戴。3.4 点一多就得算性能账单个页面放 20 个标记怎么写都行。放到 2000 个浏览器就开始喘了。因为每个 Marker 背后都是一个真实的 DOM 节点加上图标图片的请求内存和渲染压力是线性增长的。到 5000 个以上基本可以准备白屏了。方案取舍我整理成一张表方案适用量级是否可点击使用成本普通 Marker数百个以内是最低点聚合 MarkerClusterer数百到数千是聚合后点聚合点中需引入扩展库海量点图层上万级否整体绘制中样式选项有限自绘 canvas 图层十万级需自己实现拾取最高点聚合的思路很直观距离近的点合并成一个带数字的圆放大之后自动拆开。GL 版本一般可以直接用聚合类v3.0 则需要额外引入聚合扩展文件写法大致是new MarkerClusterer(map, markers, options)。具体类名和参数以当前官方文档为准因为不同版本的扩展库命名有差异我遇到过升级后类名变了导致报错的情况。海量点适合只看分布不需要交互的场景比如设备分布、用户热力。它在 v3.0 里叫PointCollection底层用 canvas 绘制几万个点也不卡const points rawData.map(d new BMap.Point(d.lng, d.lat)); const collection new BMap.PointCollection(points, { size: 4, shape: circle, color: #2b7cf6 }); map.addOverlay(collection);代价是它不能单独点选也没有独立的 hover 效果。如果你的产品既要海量又要点击那就得自己做 canvas 拾取计算或者用聚合把量降下来。我一般的判断标准是能聚合就聚合聚合不了的再考虑海量点最后才考虑自定义绘制。因为聚合保留了交互用户能点进去看详情产品体验最完整。4. 文本标注 Label让店名和状态稳稳贴在坐标旁边Label 是最容易被低估的覆盖物。看着就是放一行字实际上它的偏移、层级和样式都有各自的脾气。4.1 setStyle 的那些细节const label new BMapGL.Label(门店A · 营业中, { position: point, offset: new BMapGL.Size(20, -24) }); label.setStyle({ color: #ffffff, fontSize: 12px, lineHeight: 18px, backgroundColor: #2b7cf6, border: none, borderRadius: 4px, padding: 3px 8px, whiteSpace: nowrap }); map.addOverlay(label);这里有两个必须显式处理的点第一Label 默认带一个白色背景和灰色边框。如果你不改它会以百度自己的默认样式出现和你的 UI 完全不搭。border: none和backgroundColor一定要写上。第二Label 的定位原点是左上角不是中心。也就是说position指向的那个点是 Label 左上角的位置。想让 Label 居中在某个点上方就得靠offset加上 CSS 的transform: translateX(-50%)配合后者需要通过 Label 的 DOM 节点操作或者干脆用足够精确的 offset 手算。whiteSpace: nowrap也很关键。默认情况下长文本会自动折行在窄容器里会出现文字变成两三行、把地图遮住一大片的情况。加上这个属性可以强制单行超长的话用textOverflow: ellipsis加maxWidth截断。4.2 层级为什么我的 Label 被图标盖住了这是问得最多的一个问题。地图内部的 DOM 是分层级的不同的覆盖物渲染在不同的 pane 里。Marker 和 Label 都往上层容器里塞节点谁在前面取决于添加顺序和各自的 zIndex。label.setZIndex(100); // 数值越大越靠上 marker.setZIndex(50);我的习惯是给 Label 一个比较高的 zIndex让文字永远压在图标上面。如果你的场景是图标必须盖住文字比如大图标配背景文字那就反过来调。总之不要依赖添加顺序来决定层级显式设置最稳妥因为数据渲染顺序在不同版本里可能不一样。4.3 用 Label 做价格牌、状态标签和方位说明Label 的实用场景比想象中多。除了店名我还用它做过这些事价格牌房产地图上每个房源标一个总价 320 万用不同底色区分价格区间配合 Marker 的图标形成完整的视觉单元。状态标签设备地图上标在线/离线/告警颜色分别是绿灰红运营同事一眼就能看出哪个片区有问题。方位说明园区地图上标注A 区B 区停车场入口这些不需要图标纯文字反而更清爽。实时数值配合定时刷新Label 显示当前路口的车流量label.setContent(String(count))就能更新不用重建覆盖物。setContent这个方法很实用它只更新文本内容不重新创建 DOM性能比删了再加要好得多。更新数据的定时任务里我一般都会走这个接口。还有个隐藏坑Label 数量一多每个都是一个 DOM 节点和 Marker 一样会吃性能。如果一个页面上有上千个文字标注我建议合并成一个自定义 Overlay 用 canvas 画或者干脆做成静态瓦片图层。纯 DOM 撑不住这个量级。5. 当 Marker 和 Label 都不够用手写一个自定义覆盖物前面说过一个点上要放三四个元素的时候就该上自定义 Overlay 了。写法其实不复杂继承Overlay之后实现三个方法就行。5.1 initialize、draw、remove 的职责function ShopCard(point, data) { this._point point; this._data data; } ShopCard.prototype new BMapGL.Overlay(); // 1. 创建 DOM 并插入地图容器 ShopCard.prototype.initialize function (map) { this._map map; const div document.createElement(div); div.className shop-card; div.innerHTML img classshop-card__icon src${this._data.icon} alt div classshop-card__name${this._data.name}/div div classshop-card__badge${this._data.status}/div ; this._div div; map.getPanes().labelPane.appendChild(div); div.addEventListener(click, () { this._data.onClick this._data.onClick(this._data); }); return div; }; // 2. 每次地图移动/缩放时重新计算像素位置 ShopCard.prototype.draw function () { const pixel this._map.pointToOverlayPixel(this._point); this._div.style.left pixel.x px; this._div.style.top pixel.y px; }; // 3. 移除时清理 DOM 和事件 ShopCard.prototype.remove function () { if (this._div this._div.parentNode) { this._div.parentNode.removeChild(this._div); } };三个方法的职责很清晰initialize负责造节点、挂监听draw在每次地图视图变化时被调用负责把经纬度换算成屏幕像素remove负责清理。理解了这一点后面写各种花样的覆盖物都是同一套骨架。5.2 用它做带按钮的卡片标记我用这套骨架做过一个充电桩地图每个桩在缩略状态下显示一个圆点点击展开成卡片卡片里有功率、空闲状态和一个导航到这里的按钮。全部用 CSS 类切换实现展开和收起只是加一个 class非常顺滑。关键点是把 CSS 交给外部的样式表不要在 JS 里拼内联样式。原因有两个一是内联样式的优先级高后面想改很麻烦二是团队协作时设计师改样式只需要动 CSS 文件不用碰 JS。我在initialize里只负责铺 DOM 结构样式全部写在 class 里。另外卡片里的按钮点击要和地图点击区分开记得在按钮上event.stopPropagation()否则点按钮的时候地图也会响应可能会触发地图的默认行为。5.3 内存与事件解绑这里最容易漏自定义覆盖物最大的风险是内存泄漏。因为你在initialize里挂了不少 DOM 事件监听如果remove的时候不清理节点就被事件闭包引用着GC 回收不掉。几百个点反复增删之后页面内存会持续上涨。我的做法是在initialize里保存所有需要解绑的函数的引用在remove里一一摘掉initialize(map) { this._onClick () { /* ... */ }; this._div.addEventListener(click, this._onClick); // ... } remove() { this._div.removeEventListener(click, this._onClick); this._div.parentNode this._div.parentNode.removeChild(this._div); this._div null; }还有个容易忽略的点draw方法里不要做重计算。它在地图平移缩放时会被高频调用如果里面有复杂的布局计算或者 DOM 读写整个拖拽会变得非常卡。我踩过一次draw里调了getBoundingClientRect做位置修正结果地图拖动像在放幻灯片后来把计算挪到数据更新时预先算好才恢复正常。6. 上千个点、微信小程序、Qt 桌面端和 WordPress换个环境就换一套写法同样的业务需求换一个运行环境接入方式可能完全不同。这一节把几个常见环境的差异讲清楚省得你拿着一套代码到处贴。6.1 微信小程序用原生 map 组件不是 JS API这是新人最容易搞错的地方。小程序里不能直接用百度地图的 JS API除非用 web-view 嵌套要用小程序自己的map组件数据通过markers数组传入。map idmap longitude{{lng}} latitude{{lat}} scale16 markers{{markers}} bindmarkertaponMarkerTap /Page({ data: { lng: 116.404, lat: 39.915, markers: [] }, onLoad() { const markers list.map((item, index) ({ id: index, latitude: item.lat, longitude: item.lng, iconPath: /assets/shop.png, width: 32, height: 40, anchor: { x: 0.5, y: 1 }, // 0.5 表示水平居中1 表示底部 label: { content: item.name, color: #333, fontSize: 12, bgColor: #ffffff, padding: 4, borderRadius: 4, anchorX: 0, anchorY: -44 } })); this.setData({ markers }); }, onMarkerTap(e) { // e.detail.markerId 对应上面设置的 id } });几个和小程序相关的实战提醒anchor 用的是 0 到 1 的比例值不是像素这一点和 Web 端的BMapGL.Size完全不同直接复制 Web 代码一定错。marker 数量有性能边界。几十个还好上百个就要考虑聚合了。小程序端也有聚合方案但要注意基础库版本要求。图标路径限制本地图片最稳网络图片要在后台配置合法域名否则真机上加载不出来开发者工具里可能是好的这个差异坑过很多人。label 的 anchorY 是负值往上偏移因为小程序的 label 默认在点下方想放上面就得用负数。callout 和 label 是两个东西label 是一直显示的callout 默认点击才显示做点击弹出气泡用 callout 更合适。6.2 Qt 桌面端QWebEngineView 加 QWebChannel桌面端用 Qt 嵌地图通常是QWebEngineView加载一个本地 HTML地图逻辑还是跑在网页里通过QWebChannel和 C 侧通信。QWebChannel *channel new QWebChannel(view-page()); channel-registerObject(bridge, bridgeObj); view-page()-setWebChannel(channel); view-load(QUrl::fromLocalFile(:/map.html));// map.html 里 new QWebChannel(qt.webChannelTransport, function (channel) { window.bridge channel.objects.bridge; window.bridge.pushMarkers(JSON.stringify(markerData)); });这里有个非常关键的坑用file://协议加载的页面Referer 是空的AK 校验很可能直接失败。页面上会显示AK 校验失败之类的提示地图一片灰。解决方案是在 Qt 里起一个本地 HTTP 服务用QTcpServer做一个极简的静态文件服务把页面用http://127.0.0.1:端口提供出去这样 Referer 就有了AK 白名单里把这个地址加上即可。另一个坑是资源路径。打包成 exe 之后相对路径会失效所以 HTML 里的图片、脚本最好都通过qrc资源系统引入或者用本地服务的绝对路径。我用相对路径打包过一次开发机上好好的装到客户机器上图标全变成了碎图。6.3 WordPress 站点加载时机比代码本身更重要WordPress 里放地图有三种常见做法直接在页面编辑器里用自定义 HTML区块贴代码、写一个 shortcode 在functions.php里注册、或者用现成的地图插件。我推荐 shortcode因为内容编辑和代码分离后期改结构不影响已发布的页面add_shortcode(baidu_map, function ($atts) { $atts shortcode_atts([id map, lat 39.915, lng 116.404], $atts); ob_start(); ? div id?php echo esc_attr($atts[id]); ? stylewidth:100%;height:480px;/div ?php return ob_get_clean(); });真正的坑在于脚本加载时机。很多 WordPress 站点装了缓存或延迟加载插件会把script标签延迟到用户交互时才执行。这时候地图的初始化代码跑了但 API 脚本还没加载直接报BMapGL is not defined。应对办法是监听脚本的加载完成事件而不是把初始化代码直接写在DOMContentLoaded里window.onload function () { if (typeof BMapGL undefined) { setTimeout(initMap, 300); // 简单的重试 return; } initMap(); };更稳的做法是用wp_enqueue_script把 API 脚本注册进去并声明依赖关系让 WordPress 自己控制顺序。但这个需要改主题文件如果只是想快速加个地图用上面的重试方式也够用。顺便说一句站点域名如果有 www 和不带 www 两个版本AK 白名单两个都要加这个在小网站迁移域名之后特别容易漏。7. 上线前的自检清单坐标系、热区、内存与报错对照代码写完能跑和上线之后不出问题中间还差一轮自检。下面这些是我每个项目上线前都会过一遍的项。7.1 坐标系的转换千万别猜这是地图相关项目里最隐蔽的错误来源。GPS 设备采集的原始坐标是 WGS84而百度地图用的是自己的 BD09 坐标系。两者之间差着几百米在市区里足以把你的点放到隔壁小区去。const convertor new BMapGL.Convertor(); const points rawData.map(d new BMapGL.Point(d.lng, d.lat)); convertor.translate(points, 1, 5, function (result) { if (result.status 0) { result.points.forEach((p, i) { createMarker(p, rawData[i]); }); } });translate是异步的回调里才有转换后的坐标。这个接口有调用频率和批量限制几百个点要分批处理中间加一点间隔否则会返回失败状态。from和to的取值是一组数字枚举我这里用的 1 代表 GPS、5 代表百度坐标用之前对着文档的枚举表确认一下别凭记忆写。如果数据源本身就来自百度地图比如通过搜索接口拿到的那就不需要转换直接使用。多转一次反而会偏。判断依据很简单数据从哪来就用哪套坐标。7.2 移动端的热区比视觉尺寸重要移动端上一个 20x20 的图标用手指点命中率很低。但直接把图标放大到 40x40视觉上又会显得笨重。我的做法是图标透明留白图片实际做成 60x60但可见图形只占中间 32x40 的部分四周透明。这样热区大了视觉上还是原来的大小。这个技巧在密集的地图场景里特别好用用户点中的概率明显提升。同时移动端要记得处理双指缩放和单指拖拽的冲突map.enableScrollWheelZoom()在移动端是无效的没有滚轮要靠手势缩放这个百度地图默认就支持不用额外配置。7.3 常见报错对照表现象大概率原因处理方式地图容器空白无报错容器高度为 0显式设置 height提示 AK 校验失败Referer 白名单不含当前域名补全域名注意 wwwBMapGL is not defined脚本未加载完或版本混用检查引入顺序只保留一个版本图标不显示图片路径错误或跨域换本地资源或配置 CDN 跨域头图标位置偏移anchor 未设置设为 (宽/2, 高)点击标记无响应底图 POI 抢占事件初始化时enableMapClick: false拖动地图卡顿draw 里有重计算把计算移到数据更新阶段内存持续上涨覆盖物移除时未解绑事件在 remove 里清理监听和 DOM7.4 关于数据来源有件事必须提醒地图上的数据从哪来决定了这个项目能不能长久做下去。设备坐标、门店信息这类自有数据没问题但如果涉及从地图服务商的开放能力里批量获取内容比如街景图像、POI 详情一定要先确认自己的使用范围是否在授权之内。我见过有人写了批量抓取的脚本短期能跑但账号很快被限制项目也跟着停了。能用官方接口按需调用的就不要自己写循环去刷这条路既不稳也不划算。一个点的渲染、一行文字的摆放、一个图标的锚点这些细节单独看都很小但它们叠在一起决定了一张地图是能用还是好用。我个人在实际操作中的体会是地图这块最值得先做的事情不是写业务逻辑而是先花二十分钟把环境跑通——AK 配好、容器有高度、一个默认标记能出来、点击能响应。这二十分钟能省掉后面几小时的瞎猜。等基础链路打通了再往上加自定义图标、加文字标注、换覆盖物方案每一步都能立刻看到效果出问题也容易定位到底是哪一层的事。再分享一个小习惯我会给每个覆盖物实例挂一个业务 id然后在事件回调里通过这个 id 反查数据而不是靠数组下标。地图上的元素会随着数据刷新增删下标随时会变只有业务 id 是稳定的。这个习惯帮我避免过好几次点 A 显示 B 的信息这种尴尬的线上问题。
返回列表