天地图 T.Marker 自定义图标与文字备注对齐及批量打点实践
发布时间:2026/10/1 18:51:28来源:尧图网络
默认那个蓝色水滴我用着一直没觉得有什么问题直到上个月做本地生活的一个门店地图甲方在评审会上直接说图钉太普通了每家门店要带自己的 logo图钉底下还得挂一行门店名加营业时间。当时我心想这有什么难的不就是换个 icon 加个 label 嘛。结果从下午两点一直调到晚上十点中间被返回code: 301001 / 非法key卡了两个小时被文字备注的偏移量卡了一个小时最后发现罪魁祸首是图标锚点设成了左上角。这篇就把这一整套流程拆开讲天地图密钥到底怎么申请、301001这类报错怎么一步步定位、T.Marker怎么换成自定义图标、文字备注有哪几种实现路线各自适合什么场景、图标和文字怎么才能对得准、批量打点的时候性能和事件怎么处理。无论你是第一次接触地图 JS API还是从别的平台迁移过来看完应该能少走我走过的那几个弯。1. 从一次 301001 报错说起天地图密钥的申请与校验逻辑1.1 密钥不是申请完就能用先分清服务类型和 referer 白名单很多人第一次拿到tk就直接往 HTML 里一贴然后在控制台看到一个 301001第一反应是我 key 是不是复制错了。实际上天地图的密钥在创建的时候就绑定了两个关键属性服务类型和referer 白名单。这两项只要有一个对不上表现都是加载不出地图或者瓦片全部空白。创建应用的时候服务类型里能选的通常有浏览器端和服务端两类。浏览器端说白了就是给你在网页里用 JS API 或者直接拼瓦片地址用的它必须绑定 referer服务端是给你在后端做地理编码、路径规划这类接口调用用的不校验 referer 但一般有自己的配额。拿浏览器端的 key 去请求接口或者拿服务端的 key 塞进前端页面都是 301001 的常见来源这一点在官方文档里其实写得比较轻描淡写很多人是踩了才知道。referer 白名单的填法我也吃过亏。当时本地开发用http://localhost:8080/线上是https://map.example.com/我把两个域名分别写进去中间用逗号隔开结果线上一直报错。后来发现列表里的分隔符得看控制台提示有的是英文逗号有的是换行有的还支持*通配。我的建议是先在控制台里只填一个域名验证通了再加第二个一次加一堆最后出问题根本不知道是哪一项坏了。1.2 从报错码到根因301001 的四条排查路径301001我在项目里遇到过四种完全不同的成因光看报错信息是分不出来的只能按顺序排除。下面这张表是我自己整理的排查顺序从最省时间的开始。排查顺序检查项典型表现处理方式1key 是否被 URL 截断或带空格偶发失败刷新一下又好了检查拼接处有没有多余空格、有没有做 encodeURIComponent2referer 是否匹配本地能跑、部署后全白把线上域名补进白名单注意 http/https 和端口3服务类型是否匹配瓦片能出、接口全挂或者反过来浏览器端和服务端的 key 分开建4key 是否被禁用或超额之前好使某天突然不行去控制台看应用状态和调用量曲线第一条我要单独强调。有一次我把 key 拼在瓦片 URL 后面URL 里还带了其他查询参数结果拼接顺序不对tk的值被后面的参数截断了一部分表现就是九成瓦片正常、偶尔一两张 404。这种问题最难查因为它看起来像是网络抖动而不是配置错误。后来我养成了一个习惯把拼好的请求地址复制到浏览器地址栏里单独打开能出图说明配置没问题出不来就老老实实查白名单。第二条里有个隐蔽的坑页面被嵌在 iframe 里的时候referer 是父页面的地址。如果你的地图页面是作为子页面被主站引用的那白名单里要填的是主站的域名不是地图页面自己的域名。这个坑我在一个后台管理系统里踩过排查了半天才发现地图页面根本没被直接访问过。1.3 前端放密钥的安全边界以及它在别处也能复用前端代码里的tk是必然可见的这个没法藏。你能做的防护就三件事白名单限制、用量监控、按环境分 key。我当时给项目建了三个应用本地开发一个、测试环境一个、生产一个这样万一哪个 key 被刷了直接禁掉那个就行不影响其他环境。这个做法成本很低但真正出事的时候能救命。顺便提一句同一个 key 在别的地方也能复用。比如在桌面端 GIS 工具里加载天地图作为底图本质上就是配一个 WMTS 服务地址格式大概是https://t{0-7}.tianditu.gov.cn/vec_w/wmts?SERVICEWMTSREQUESTGetTile VERSION1.0.0LAYERvecSTYLEdefaultTILEMATRIXSETw FORMATtilesTILEMATRIX{z}TILEROW{y}TILECOL{x}tk你的tk这里面t{0-7}是八个瓦片子域名用来分散请求。换底图只需要把vec_w矢量底图换成img_w影像或者ter_w地形晕渲图层名LAYER也要跟着改。在 GIS 工具里配置的时候如果报错八成还是白名单的问题因为这类工具发请求时带的 referer 往往是空的这时候你可能需要单独建一个不校验 referer 的 key或者干脆走服务端代理。瓦片拼地址这件事看起来简单但参数顺序、大小写、占位符改名都是坑建议直接复制官方示例再改。2. 默认蓝色水滴为什么必须换掉自定义图标的构造方式2.1 T.Icon 的三个参数决定图钉长在哪里天地图里换图标不是把图片地址塞给 Marker 就完事中间要经过一个T.Icon对象。这个对象只关心三件事图片地址、图片尺寸、锚点位置。// 先构造图标对象 var shopIcon new T.Icon({ iconUrl: /static/pin-shop.png, iconSize: new T.Point(32, 40), // 图片在图上渲染的像素尺寸 iconAnchor: new T.Point(16, 40) // 锚点这里是底边中点 }); // 再用它创建 Marker var marker new T.Marker(new T.LngLat(116.404, 39.915), { icon: shopIcon, title: 门店A }); map.addOverLay(marker);这里有个我卡了很久的点iconAnchor是相对于图片左上角的像素坐标它标记的是哪个点对准经纬度位置。默认情况下如果你不写iconAnchor很多版本会按图片中心对齐于是图钉的针尖会飘到坐标点上方半张图的位置。对于图钉这种针尖要扎在坐标上的图形锚点必须设成(宽度/2, 高度)也就是底边中点。我当时设成了(0, 0)结果图钉整个跑到坐标点的右下角看起来像是坐标偏移其实是锚点错了。还有一个容易被忽略的拼写问题添加覆盖物的方法名是addOverLay中间的L是大写的。这不是我打错字API 就是这么定义的写成addOverlay会静默失败或者直接报 undefined。这个大小写第一次用几乎必踩而且因为没有报错提示很容易怀疑到别的地方去。2.2 图标资源本身的三条规范比 API 更容易出问题API 用对了图标还是可能显示不正常问题出在图片资源本身。我总结了三条尺寸要按 2 倍图准备。现在大部分屏幕是高分屏你准备一张 32×40 的 PNG在很多设备上会糊。我的做法是直接准备 64×80 的图然后把iconSize设成(32, 40)浏览器会缩一半渲染视觉上就是清晰的。这个逻辑和 CSS 里用background-size缩放高清图是一样的。背景必须透明。用了白色底的 JPG 做图钉放到地图上就是一个白方块压着路网非常难看。PNG 或者 SVG 都行我一般用 PNG-24兼容性最稳。路径用绝对路径或者 base64。相对路径出问题特别多尤其是页面路由带多级的时候./images/pin.png很可能被解析到错误的目录。如果图标很小小于 2KB我倾向于直接内联成 base64省一次请求也省掉路径问题。代码里写起来有点丑但比排查 404 省事。2.3 换图标不必重建 Marker多个点还能共用同一个 Icon有个细节值得说一下如果只是想把某个点从普通门店换成旗舰店图标不需要removeOverLay再重新addOverLay。Marker 上有setIcon方法直接换就行marker.setIcon(new T.Icon({ iconUrl: /static/pin-flagship.png, iconSize: new T.Point(32, 40), iconAnchor: new T.Point(16, 40) }));更进一步如果你图上有几百个点用的都是同一种图标不要每个 Marker 都 new 一个 T.Icon。构造一次然后把这个实例传给所有 Marker能省掉不少内存。我做过一个 800 个点的项目一开始每个点都新建 Icon页面跑了十几分钟内存就上去了改成共用之后明显稳定。这个优化在几十个点的时候看不出来点多的时候效果很直接。3. 文字备注的四条实现路线别一上来就选最复杂的给 Marker 加文字备注这件事天地图没有提供一行代码搞定的写法得自己组合。我试过四种路线各有各的适用场景下面按从简到繁说。3.1 T.Label最省事但样式天花板很低天地图自带T.Label这个覆盖物类型专门用来放文字。var label new T.Label({ text: 门店A · 09:00-21:00, position: new T.LngLat(116.404, 39.915), offset: new T.Point(0, -8) }); map.addOverLay(label);offset是相对 position 的像素偏移配合图钉锚点用一般把文字往上推一点点让它贴在图标下方或者上方。样式方面这个对象提供了一批set开头的方法常见的像改文字、改位置、改背景色、改字体颜色、改边框颜色、改字号字重这些都有具体方法名和你用的 API 版本有关建议对着官方文档确认一遍再写死。它的短板也很明显圆角、内边距、阴影这些 CSS 层面的东西你控制不了。天生成的那种带边框的小方块做后台管理类的标注够用做面向 C 端的页面就有点糙。另外文字长了它不会自动换行会摊成很长一条压住地图上其他要素。3.2 自绘 DOM 覆盖物样式自由度最高我最后选的就是这条想要完全自由的样式就别用地图自带的文字对象自己在页面容器上叠一层绝对定位的 div然后跟着地图同步位置。核心就两个 API监听地图的移动和缩放事件以及把经纬度换算成容器像素坐标。// 1. 建一个容器盖在地图上 var overlayLayer document.createElement(div); overlayLayer.style.cssText position:absolute;left:0;top:0;pointer-events:none;z-index:10; map.getContainer().appendChild(overlayLayer); // 2. 为每个点生成一个标签元素 function createTag(lnglat, html) { var el document.createElement(div); el.className map-tag; el.innerHTML html; el.style.cssText position:absolute;transform:translate(-50%,-100%); background:rgba(255,255,255,.92);border-radius:6px; padding:3px 8px;font-size:12px;white-space:nowrap; box-shadow:0 2px 6px rgba(0,0,0,.15);pointer-events:auto; overlayLayer.appendChild(el); return el; } // 3. 地图一动就重新同步位置 var tags []; function syncTags() { tags.forEach(function (item) { var px map.lngLatToContainer(item.lnglat); if (!px) { item.el.style.display none; return; } item.el.style.display ; item.el.style.left px.getX() px; item.el.style.top px.getY() px; }); } map.addEventListener(moveend, syncTags); map.addEventListener(zoomend, syncTags);这里面有两个关键点。一是translate(-50%, -100%)用 CSS 变换做居中比在 JS 里算宽度除以二更省事而且元素内容变长变短都不用重新计算。二是pointer-events外层容器设成none让鼠标事件穿透到地图上单个标签再设成auto让它自己能点这样既不挡住地图的拖拽标签又能绑定点击。注意moveend和zoomend是地图停止移动/缩放后才触发的。如果你想让标签跟手得用move和zoom这类持续触发的事件但那样每秒会调用几十次坐标换算几百个标签就会卡。我的折中做法是拖动过程中隐藏标签停下来再显示。3.3 InfoWindow 常驻适合信息量大但点位少的场景如果每个点的备注不止一行还有地址、电话、营业时间那用 InfoWindow 更合适。它本来是为点击弹窗设计的但你可以不绑定点击直接在创建后调openInfoWindow让它常驻。var infoWin new T.InfoWindow(div classshop-card门店A/div, { offset: new T.Point(0, -42) }); marker.openInfoWindow(infoWin);这个方案的优点是内容和排版完全由你自己写 HTML二是窗口自带小尖角视觉上跟图钉是一体的。缺点也很清楚窗口会挡住地图点位一多就互相压而且常驻窗口的关闭逻辑要自己管。我的经验是超过五个点就不要用常驻 InfoWindow 了。3.4 四条路线的对比与选择建议方案样式自由度实现成本适合点位数量我实际用在哪T.Label低很低几十个后台管理系统自绘 DOM 层高中等几百个面向 C 端的门店地图InfoWindow 常驻高低5 个以内单个重点位置详情Canvas 图层自绘极高高上千个数据大屏如果你只是做内部工具别折腾T.Label就够了。但只要是给外部用户看的页面我建议直接上自绘 DOM 层一次写好封装成函数后面所有项目都能复用。至于 Canvas 图层它适合几千个点同时展示的场景需要自己算像素、自己画文字投入产出比只在特定项目里划算普通门店地图完全没必要。4. 图标和文字对不齐的根因基本都在这三件事上4.1 iconAnchor 和 offset 是两套坐标系别混着算我一开始调偏移的时候犯过一个错以为给图钉设了iconAnchor: (16, 40)文字 label 就会自动跟着这个锚点走。实际上不是。iconAnchor只管图标自己贴在哪里文字是独立定位的两者各算各的。所以想让文字正好贴在图标下方你得自己算图标底部在坐标点上文字要再往下推一个图标的高度。如果用的是T.Labeloffset的 y 值大概是正数向下如果用的是自绘 DOM 加translate(-50%, -100%)标签的底边就在坐标点上这时候要往下移就改成translate(-50%, 0)再配一个top的额外偏移。这两套偏移的方向是反的来回切换的时候最容易搞混我建议一个项目里只用一套别混着写。4.2 坐标系差一点整体就偏一条街地图上打点最常见的整体偏移问题根因通常是坐标系。天地图用的坐标基准和常见的 GPS 原始坐标、以及某些平台使用的加密坐标之间是有差别的如果你拿 A 平台的坐标直接丢到 B 平台上打点偏移几十米到几百米都有可能在市区看就是差了一条街。处理方式有两种。一是统一数据源让所有坐标都从同一个平台导出这是最省事的。二是在入库时做一次转换把坐标转成目标平台用的基准再存。我当时做门店地图数据是从业务系统导出的正好就是天地图能直接用的基准所以没做转换但我在文档里专门写了一行注释标明坐标系免得后来接手的人从别的地方导数据进来踩坑。顺手推荐一个自检方法拿一个你熟悉的地标比如公司楼下去平台的坐标拾取工具里点一下记下坐标在你自己页面里打这个点看看针尖对不对得上那个位置。对不上就是坐标系或者锚点的问题对得上就说明配置没问题后面再出偏差就是数据本身的问题。4.3 缩放层级变化时哪些东西会跟着变地图缩放的时候覆盖物的行为不完全一样。Marker 的位置和图标大小是跟着缩放的虽然图标渲染尺寸一般固定但它贴的坐标会重新投影自绘 DOM 标签需要你自己在zoomend里重算位置忘了监听就会看到文字飘在原处不动。T.Label一般会自己跟着走但如果你自己改过它的 position就要手动更新。还有一个细节低缩放级别下比如整省可见几百个标签会挤成一团这时候应该做聚合或者直接隐藏标签只留图标。我的做法是设一个阈值缩放级别小于某个值就display:none掉所有文字只显示图钉。这个判断写在zoomend里成本很低但体验差别很大——不然用户拉远一看满屏都是糊在一起的文字块。5. 批量打点时的交互、动态更新与资源回收5.1 事件绑定里的 this 指向我在这上面浪费过一小时给 Marker 绑点击事件是个高频需求但写法有讲究。// 反例所有点都提示最后一个点的名字 for (var i 0; i list.length; i) { var m new T.Marker(list[i].lnglat); m.addEventListener(click, function () { alert(list[i].name); // i 已经变成 list.length 了 }); map.addOverLay(m); } // 正例用闭包把每个点的数据锁住 list.forEach(function (item) { var marker new T.Marker(item.lnglat, { icon: sharedIcon }); marker.addEventListener(click, function () { showCard(item); }); map.addOverLay(marker); });这是 JS 里的经典闭包问题但在地图场景里特别容易犯因为你会本能地想我先建好点等点击的时候再查数据。把数据在创建 Marker 的时候就绑进去别等到点击时回头查既避免闭包问题也省一次查找。我在事件回调里还习惯加一句e.stopPropagation()或者对应的阻止冒泡写法防止点图标的同时把点击也传给地图导致触发别的图层逻辑。5.2 动态改文字、改图标、改位置的三种写法实际项目里点位状态是会变的比如门店营业中/休息中要换颜色或者某个点的文字要随筛选条件变化。三种常见更新方式改图标用setIcon这个前面说过。改文字如果用T.Label就调它对应的setText如果用自绘 DOM 就直接改元素的innerHTML或文本内容后者显然更灵活。改位置要注意Marker 一般有setLngLat之类的方法但自绘标签必须手动重新跑一次同步函数否则位置对不上。我给项目封装过一个updateMarker(id, data)函数内部根据传入字段决定要调哪几个 setter最后统一跑一次syncTags()。这样业务侧只调一个方法不用关心底下用的是哪种实现。封装成本大概半小时但后面每次需求变更都能省事。5.3 清理和回收不清理会慢慢变卡单页应用里最常见的泄漏就是切换页面时没清掉地图上的覆盖物。用户在同一条路由上反复进出Marker 越堆越多图表越来越卡但代码里看不出任何问题。标准做法是在组件卸载时做三件事把 Marker 数组清空、把自绘的 DOM 容器整个移除、把事件监听解绑。天地图有清空覆盖物的方法名字里带 clear但自绘的那层 div 不在它的管理范围内必须你自己parentNode.removeChild()掉。我有一次只清了 Marker 没清 DOM 层结果返回页面时上层还叠着上一次的标签位置全错看起来像是地图缓存了旧数据其实是我自己没清干净。提示如果你用的是定时刷新点位比如每 30 秒轮询一次不要在回调里反复new T.Marker而不清旧的。正确顺序是先清空全部再重新添加而不是增量往 map 上加。增量加在长跑页面里迟早出问题。6. 排查实录图标不显示、文字被裁、高分屏发虚6.1 图标整片不显示先按这个顺序查图标不显示是最高频的问题我列一下我的排查顺序基本三分钟内能定位。打开浏览器开发者工具的网络面板筛一下图片请求。如果图片请求 404那是路径问题改用绝对路径或者把图片内联成 base64。如果请求根本没发出去那是iconUrl没被正确读取检查是不是给了空字符串或者错误的变量。如果请求 200 但图上还是没有那大概率是锚点或者尺寸问题把iconAnchor设成图片中心先看看能不能出来能出来就说明是锚点把图推到了可视区外面。还有一种情况是图片请求被跨域策略拦了尤其是图标放在别的域名的静态资源服务器上控制台会有明确的提示这时候要么把图片挪到同域要么配置好跨域头。6.2 文字被地图容器裁掉原因有三种自绘标签被裁我遇到三种情况。第一种是容器的overflow是hidden标签往地图边界外走了就被切掉了这时候需要在容器外层再套一层宽一点的可视区域或者干脆限制标签不往边界外飘。第二种是层级被别的元素盖住地图容器的 z-index 往往很高你的标签层如果没设够就会被压在地图下面看不见加个z-index解决。第三种比较隐蔽是地图本身的交互层拦截了鼠标事件表现不是看不见而是点不动这个要靠pointer-events解决前面说过。6.3 高分屏上图标和文字发虚图标发虚一般是因为用了一倍图解法前面讲过准备两倍图然后缩小渲染。文字发虚则是另一回事通常是定位坐标带了小数浏览器在做亚像素渲染时把文字边缘抹糊了。可以在设置left/top的时候取整item.el.style.left Math.round(px.getX()) px; item.el.style.top Math.round(px.getY()) px;就加两个Math.round在很多设备上视觉差别非常明显。这个技巧在别的地图库里同样适用不算天地图特有的问题。6.4 一张对照表收个尾现象最可能的根因第一件事查什么瓦片全白 301001key 服务类型或白名单不匹配控制台应用状态与 referer 配置偶发单张瓦片失败拼 URL 时参数被截断复制完整地址到浏览器直接访问图钉针尖不在坐标上iconAnchor 设置错误改成底边中点再试添加覆盖物无效果方法名大小写写错确认是 addOverLay标签位置不跟随地图没监听 moveend / zoomend检查事件绑定标签被裁一半容器 overflow 或 z-index审查父级样式内存越用越高切换页面没清覆盖物和 DOM检查卸载逻辑点位整体偏移坐标系不一致或锚点错误用已知地标做对照测试我现在做任何地图项目第一天就会把上面这些点写成一份自检清单放在项目文档里谁接手都能快速定位。图标和文字备注这件事本身不难难的是链路长——从密钥到瓦片、从锚点到坐标系、从事件到内存任何一环出问题表现都差不多。把这条链路完整走通一遍之后你会发现后面所有的地图需求都是在这套东西上加加减减没什么新鲜的。真要说心得就是别怕把 demo 写小换成别人做这个门店地图我可能还是从一张图钉、一个标签的最小 demo 开始跑通了再往上堆业务逻辑比一上来就在大项目里调快得多。
网站建设高端定制企业官网