ARTICLE DETAIL

资讯详情

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

高德地图搜索与点击定位全流程开发实践

高德地图搜索与点击定位全流程开发实践 搜索加定位高德地图开发里最基础但也最容易被忽视的一个场景。我见过太多人把“搜索”和“点击定位”当成两个割裂的功能来做结果搜索能出结果点击列表项却定位不准要么标记不消失要么地图中心点偏了十万八千里。这篇文章我就把这一整套链路从头到尾拆一遍从Web端JS API到移动端SDK再到小程序把我踩过的坑和验证过的方案全部写出来。先聊一下这个需求的核心用户输入关键词地图端返回POI兴趣点列表用户点击其中一条地图将视角移动到该点的坐标并在地图上打点展示详情。这个流程听起来简单但真正做好需要处理好搜索参数、坐标体系、事件绑定、多端适配几个环节任何一个地方出问题体验都会很糟糕。1. 搜索加点击定位这一套流程到底在做什么1.1 一条完整交互链路的产品拆解在动手写代码之前先想清楚整套交互在前端页面里是如何流转的。一个标准的“搜索并点击定位”功能实际包含四条子链路。第一条是搜索输入链路。用户在搜索框里输入关键词前端拿到这个关键词后去调用高德地图的搜索能力。这里多数开发者的直觉是“直接调用POI搜索接口”但高德还提供了输入提示联想建议功能可以做到边输入边提示这个后面我会展开讲。第二条是结果展示链路。搜索接口返回的POI列表并不只是“名称”和“坐标”两个字段还包含地址、电话、类型、评分、营业时间等信息。结果列表如何渲染、展示哪些字段、需不需要分页这些直接决定用户在第一屏能不能快速找到目标地点。第三条是地图联动链路。用户点击列表中的某一条结果后地图需要完成三件事把该点的经纬度设置为地图中心、在对应位置添加标记点、弹出信息窗体展示详情。这一步听起来简单但实际开发中80%的问题都出在这中心点设置了但标记不显示、标记显示了但气泡不弹、地图缩放层级不合适导致用户看不清周边环境。第四条是状态同步链路。搜索结果和地图标记要保持一致用户点了第二条、再点第三条前一个标记要能正确清除地图的视野范围要平滑过渡。很多半成品功能就是在这条链路上偷工减料导致点几次之后地图上全是旧标记。1.2 别把“搜索”做成“地理编码”我在代码评审时见过最多的误区是把POI搜索和地理编码混为一谈。高德地图的Web服务API里有两类接口一类是“地理编码/逆地理编码”一类是“关键字搜索POI”。地理编码解决的是“根据结构化地址获取坐标”比如输入“北京市朝阳区望京街10号”返回一个坐标点。POI搜索解决的是“根据关键词找兴趣点”比如输入“望京 咖啡”返回望京周边所有咖啡馆的坐标列表。两者的核心区别在于地理编码输入的是“精确或接近精确的地址”POI搜索输入的是“模糊的语义关键词”。如果你做的功能是让用户输入一个地址然后定位那用地理编码没问题如果是让用户搜“火锅”“加油站”“某某大厦”必须用POI搜索。搞混这两个接口会出现同一个关键词在不同接口下返回完全不同的结果用户会直接判定功能不可用。高德的这两个接口内部都使用GCJ-02坐标系火星坐标系这一点后面单独讲因为它也是定位偏移的常见来源。1.3 为什么这一套功能是地图应用的地基搜索加定位不是某个垂直场景的专属需求而是几乎所有地图应用的公共底座。外卖应用让用户搜索地址并定位送餐点打车应用让乘客搜索目的地并确认上车位置城市服务小程序让用户搜索办事机构然后导航过去。这些场景的交互模型都一样搜索定位标记然后进入下一步业务。把这一套公共链路做好最大的收益是后续加功能很省事。比如你完成了搜索定位在这个基础上加路线规划只需要把定位到的坐标作为起终点参数传进去加周边推荐只需要把定位坐标作为中心点去调周边搜索。很多团队上来就做“大而全”的功能集成结果地基不稳导致后面每个功能都要回来补锅。我个人的建议是先把搜索、点击、定位、打点、气泡这五个点做得足够扎实再往上堆业务。2. Web端搜索定位完整实现从零到一的核心代码2.1 环境准备与初始化地图我用高德地图JS API 2.0版本做示例2.0和1.4.x在API风格上有差异但核心逻辑一致。第一步先到高德开放平台创建应用获取Key。这个地方要特别注意2021年之后高德做了安全升级JS API 2.0除了Key之外还要求设置安全密钥securityJsCode或者使用代理服务器。不配置安全密钥地图会在页面加载时报错或白屏。初始化地图的基础代码// 先在HTML头部引入JS API // script srchttps://webapi.amap.com/maps?v2.0key你的Key/script // 注意2.0版本还需要配置安全密钥二选一即可 const map new AMap.Map(container, { zoom: 11, center: [116.397428, 39.90923], viewMode: 2D, resizeEnable: true });这里有一个低级别问题但很容易被忽略容器div必须显式设置高度。很多新手把地图容器的高度忘记了或者写成100%但父级没有高度结果地图加载后是一块空白。我习惯在样式里直接给地图容器设置固定高度或者用flex布局撑开总之要确保初始化时容器有明确的宽高。2.2 关键字搜索PlaceSearch的正确打开方式高德JS API的POI搜索核心是PlaceSearch插件需要先通过AMap.plugin加载然后实例化。参数配置直接决定搜索结果的质量我先给出一份我用下来比较合适的配置AMap.plugin(AMap.PlaceSearch, function () { const placeSearch new AMap.PlaceSearch({ pageSize: 10, pageIndex: 1, city: 全国, citylimit: false, extensions: all, type: , map: map }); window.handleSearch function (keyword) { placeSearch.search(keyword, function (status, result) { if (status complete result.poiList) { renderPoiList(result.poiList.pois); } else { console.warn(搜索失败或没有结果, status); } }); }; });这里每个参数都有讲究。pageSize是每页返回的数据量我一般设为10加载更多时分页拉取用户体验比一次拉50条更流畅。pageIndex是页码做分页时配合使用。city参数的范围控制很关键。city传“全国”时搜索结果不受城市限制适合用户不确定目标城市的情况。citylimit配合city使用比如你明确只在北京市域内提供服务可以把city设为“北京”并把citylimit设为true这样结果就不会跑出北京。extensions参数控制返回字段的详细程度。“base”只返回基础字段包括名称、坐标、地址“all”会额外返回电话、图片、评分、营业时间、品牌等信息。如果只是定位打点用“base”就够了数据量更小响应更快如果结果列表要展示电话和评分用“all”。在实例化PlaceSearch时传入map参数可以让搜索过程中高德自动在图上展示POI标记。这个功能在需要“即搜即显示”的场景很好用但也会带来一个麻烦当你需要自定义标记样式或做点击列表项高亮时默认标记反而碍事。我的处理是不在实例化时传map而是拿到搜索结果后自己渲染自定义标记控制力更强。2.3 点击搜索结果地图精准定位这是整套流程里最容易出问题的一环。很多人的第一版代码是拿到列表项文本再去搜一次然后定位到搜索结果的第一个这是大错特错的。正确的做法是搜索返回的每一个POI对象自带经纬度点击列表项时直接使用该经纬度。一个POI对象的结构大致长这样{ id: B0FFH6U7G8, name: 望京SOHO, location: { lng: 116.480861, lat: 39.996548 }, address: 阜通东大街与望京街交叉口, type: 商务住宅;楼宇;商住两用楼宇, tel: 010-84712345, pname: 北京市 }点击列表项定位的核心代码// 假设已有一个Marker实例和InfoWindow实例 const marker new AMap.Marker({ map: map }); const infoWindow new AMap.InfoWindow({ offset: new AMap.Pixel(0, -30), autoMove: true }); function handlePoiClick(poi) { const lnglat [poi.location.lng, poi.location.lat]; // 顺序有讲究先设置中心点再设置缩放层级 map.setCenter(lnglat); map.setZoom(16); // 移动标记并设置内容 marker.setPosition(lnglat); marker.setTitle(poi.name); infoWindow.setContent( div classpoi-info h4 poi.name /h4 p poi.address /p p (poi.tel || ) /p /div ); infoWindow.open(map, lnglat); }这里有几个细节值得展开说。第一个是setCenter和setZoom的顺序。如果你先setZoom再setCenter地图的视角变换会出现一种“瞬移感”因为地图先改变了缩放比例再跳到目标点。反过来先setCenter再setZoom动画过渡会舒服很多。如果你追求更大的视野或更小的视野可以直接用setZoomRange限制一下最大最小缩放级别防止用户缩到太细看不出周边路网。第二个是marker复用。不要在每次点击时都new一个Marker而应该全局只维护一个Marker实例点击时用setPosition更新位置。频繁创建销毁DOM节点和地图叠加层在低端设备上会出现卡顿甚至导致标记闪烁。第三个是InfoWindow的autoMove参数。当标记点在地图边缘时信息窗体可能超出可视区域。autoMove设为true后高德会自动平移地图确保窗体完整显示。这个参数默认是false很多人不知道导致点击边缘区域的POI时气泡被截断观感很差。第四个是空白坐标兜底。在实际开发中极少数POI的location字段可能为null或undefined特别是在用户搜索一些自定义地点或冷门POI时。点击这类结果直接取location.lng会报错。我建议在handlePoiClick开头加一层判断function handlePoiClick(poi) { if (!poi.location || !poi.location.lng || !poi.location.lat) { console.warn(该POI缺少坐标信息, poi); return; } // 正常定位逻辑 }2.4 搜索联想与防抖处理前面提到的输入提示联想建议功能在高德JS API里有独立的插件AMap.AutoComplete。这个体验对搜索功能提升非常明显用户刚输入两个字符下拉列表就开始给建议选中建议再触发搜索比让用户完整输入关键词再点搜索按钮高效得多。AMap.plugin([AMap.AutoComplete, AMap.PlaceSearch], function () { const autoComplete new AMap.AutoComplete({ input: searchInput, city: 全国, outPutDirAuto: true }); // 监听选中事件 autoComplete.on(select, function (e) { const poi e.poi; if (poi poi.location) { handlePoiClick({ name: poi.name, location: poi.location, address: poi.address || }); } }); });注意AutoComplete和PlaceSearch在结果格式上有差异AutoComplete返回的poi.location直接在对象上而PlaceSearch返回的location在poi.location下但坐标结构两者是一致的。这里我不做复杂适配只强调一点输入提示的实现相对独立容易加对体验提升大建议所有搜索定位场景都配上。防抖处理是另一个容易忽略的细节。如果用户输入每个字符都触发一次搜索请求高德接口的并发压力会很大而且前端响应结果错乱的风险也高——用户输入“望京”上一次“望”的搜索结果可能比“望京”的结果晚返回导致列表闪现后又被旧的覆盖。我的做法是下拉提示用AutoComplete自带的监听搜索动作统一加300毫秒防抖let searchTimer null; function debouncedSearch(keyword) { if (searchTimer) clearTimeout(searchTimer); searchTimer setTimeout(() { placeSearch.search(keyword, callback); }, 300); }3. 移动端与小程序场景的差异化适配3.1 Android原生搜索定位的实现要点Web端逻辑清楚了Android原生SDK的思路基本一致但API风格差异较大。高德Android SDK的POI搜索核心类是PoiSearch需要先构造PoiSearch.Query对象再设置搜索监听器。// 构造搜索查询对象 PoiSearch.Query query new PoiSearch.Query(keyword, , city); query.setPageSize(10); query.setPageNum(0); PoiSearch poiSearch new PoiSearch(context, query); poiSearch.setOnPoiSearchListener(new PoiSearch.OnPoiSearchListener() { Override public void onPoiSearched(PoiResult poiResult, int resultCode) { if (resultCode 1000 poiResult ! null) { ListPoiItem pois poiResult.getPois(); // 渲染到列表 } } Override public void onPoiItemDetailSearched(PoiItem poiItem, int resultCode) { // POI详情回调 } }); poiSearch.searchPOIAsyn();Android端点击列表项定位的代码// 点击列表项后移动地图相机 LatLng latLng new LatLng(poiItem.getLatLonPoint().getLatitude(), poiItem.getLatLonPoint().getLongitude()); aMap.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, 16f)); // 使用MarkerOptions创建标记 aMap.addMarker(new MarkerOptions() .position(latLng) .title(poiItem.getTitle()) .snippet(poiItem.getSnippet()));Android端有两个容易踩的坑。第一个是权限问题高德SDK定位和搜索功能在部分Android 6.0及以上机型上需要动态申请定位权限否则搜索出的结果虽然能用但Map组件会显示“无法定位”。第二个是生命周期管理PoiSearch实例在页面onDestroy时要及时销毁否则内存泄漏这在单Activity多Fragment架构下尤其明显。3.2 小程序场景搜索到定位的无缝处理微信小程序接入高德有纯前端方案和服务端中转方案两条路。纯前端方案是直接在小程序中请求高德Web服务API的“搜索POI”接口用wx.request调用。这种方式简单直接但要注意高德的Web服务API有域名白名单和配额限制而且请求返回是JSON格式需要自己渲染列表和Map组件做联动。个人开发者在微信小程序后台配置合法域名时需把高德的API域名加进白名单。另一种方案是使用高德的小程序SDK微信小程序里通过ref引用MapContext调用includePoints或moveToLocation方法实现视角移动。// 在wxml中定义map组件 // map idmyMap show-location stylewidth:100%;height:400px;/map const mapCtx wx.createMapContext(myMap, this.instance); function locateTo(poi) { const lat poi.location.lat; const lng poi.location.lng; mapCtx.moveToLocation({ latitude: lat, longitude: lng }); // 或者使用includePoints将多个点适配到视野内 mapCtx.includePoints({ points: [{ latitude: lat, longitude: lng }], padding: [60, 60, 60, 60] }); }小程序端的差异点在于原生map组件的markers属性是数据驱动的你更新markers数组即可完成打点逻辑内存管理由框架接管。但注意markers的id要用字符串类型如果用了整型部分低版本微信在大量动态更新时会报错。另一个小程序场景常见的需求是“从微信小程序跳转到高德App”。这个需求需要用高德提供的URI API拼接scheme参数然后通过wx.openLocation或直接把URL传给用户长按唤起。如果你要在WebView里跳转可以用高德H5端的URI跳转协议格式大致是https://uri.amap.com/marker?position116.480861,39.996548name望京SOHO这种方式可以把定位点直接以标记形式呈现在高德App中适合分享和跨应用联动场景。3.3 多端统一方案都绕不开的坐标校验问题Web端、Android端、小程序端三端的搜索接口返回的POI坐标都是GCJ-02。但有一个隐蔽问题如果你把搜索结果拿给其他坐标系的地图比如国际版的Google地图去展示坐标就会偏移100到700米不等。多端开发时我的建议是建立一个坐标转换工具层统一处理三端的坐标逻辑。GPS设备直接采集的是WGS-84坐标如果需要在高德地图上展示必须先转成GCJ-02反之如果你从高德拿到坐标需要传给后端存入数据库要考虑业务是否需要的是GCJ-02还是WGS-84。高德的官方文档有一个坐标系说明页明确指出“高德地图API的所有坐标均采用GCJ-02”。所以凡是涉及坐标存储和分发一定要在接口文档里写清楚坐标系属性。4. 坐标系、偏移排查与高德API避坑实录4.1 火星坐标系的来龙去脉和我们的日常处理开发中每隔一段时间就会遇到一次“坐标偏移”问题很多人第一反应是代码写错了实际上大概率是坐标系没统一。GCJ-02俗称火星坐标系是中国国家测绘局制定的加密坐标系在地图显示层面国内主流地图都采用这套标准。WGS-84是GPS设备直接输出的原始坐标两者之间存在一个非线性的偏移且偏移量随位置变化不可能是简单的加常数。我做一个常见的处理方案示例把一个WGS-84坐标转成GCJ-02// 简化的坐标偏移修正示例完整算法需要偏心率和投影参数 function wgs84ToGcj02(lng, lat) { const a 6378245.0; const ee 0.006693421622965943; let dLng transformLng(lng - 105.0, lat - 35.0); let dLat transformLat(lng - 105.0, lat - 35.0); const radLat (lat / 180.0) * Math.PI; let magic Math.sin(radLat); magic 1 - ee * magic * magic; const sqrtMagic Math.sqrt(magic); dLat (dLat * 180.0) / (((a * (1 - ee)) / (magic * sqrtMagic)) * Math.PI); dLng (dLng * 180.0) / ((a / sqrtMagic) * Math.cos(radLat) * Math.PI); return { lng: lng dLng, lat: lat dLat }; }这个算法不是官方提供的但社区里广泛验证过精度可以满足非测绘级应用。我实际项目里的做法是坐标转换工具收敛到一个独立模块所有外部坐标进入地图应用之前统一调用这个模块避免散落在各个业务代码里。4.2 常见定位与搜索问题速查表我在几个项目里反复遇到过同样的问题整理成一张速查表方便大家对照排查。现象可能原因解决方案搜索无结果或结果为空关键词过于生僻或citylimit限制导致结果被过滤更换模糊短词把city设为“全国”citylimit设为false点击列表项后地图中心偏移POI坐标和地图容器坐标系不一致多为GCJ-02与WGS-84混用统一坐标系进入地图前做坐标转换标记点不显示Marker实例未添加到地图上或缩放级别过小被视野忽略new Marker时传map参数或调用map.add(marker)标记显示了但气泡不弹InfoWindow的open方法被覆盖或offset设置导致气泡跑到屏幕外检查open调用时机调整offset并允许autoMove点击第2条结果时第1条的标记还在Marker是新建未复用地图上叠加了多个旧Marker全局维护唯一Marker实例setPosition更新坐标搜索响应很慢无防抖处理每次输入都发请求增加300ms防抖并考虑使用AutoComplete代替手动搜索微信小程序markers更新后消失id类型不对或数量超过限制确保id为字符串单次更新控制在合理数量内这套表看起来零散但每一行都是实际场景里被反复问过的问题。点开任何一个地图相关的技术群隔三差五就能看到有人post这些问题。4.3 高德API免费额度和配额这事比想象中重要搜索引擎里能看到“高德地图api收费坑人”这类搜索词背后反映的是很多开发者对高德的配额和计费规则不够了解。高德开放平台的API分为个人开发者免费版和企业付费版免费版有配额限制比如Web服务API的搜索POI接口个人开发者默认配额是30万次/天还是60万次/天具体以控制台显示为准。配额用完了怎么办控制台会给每个Key单独的Quota配置可以在“配额调整”里申请提升个人开发者一般能申请到更高的配额。但要注意如果你的应用用户量上来搜索请求量大免费配额很快就会触顶。我见过一个地图H5项目上线一周就超过了免费配额搜索接口直接返回错误页面列表全空用户反馈一大堆。规避方案有几个一是给搜索接口加缓存同关键词的结果在短时间内直接复用不重复请求二是做多Key轮询但要注意高德对同一应用多Key有风控三是把搜索请求放到服务端服务端用自己的Key统一请求并做结果缓存。从成本和稳定性角度我推荐第三种服务端缓存一套POI结果后即使某个时间段触发限流也可以用缓存返回给客户端不至于让用户看到白屏。4.4 几个值得长期坚持的复盘要点搜索定位这个功能做完第一版之后不要觉得就完事了。产品上线后一定要复盘以下几个点搜索结果的点击率是多少用户搜了但没点的情况多不多点击定位后多久开始下一步操作定位后用户是否频繁调整地图视野。这些数据能反过来验证你的搜索参数设置。比如如果用户搜索后经常不点击就手动拖地图说明搜索结果排序和用户预期差异大可能需要调整关键词匹配策略或者城市范围。如果用户点击定位后又手动缩放地图说明默认的zoom层级不合适太大看不清周边路网太小看不到目的地细节。我在做一个景区导览小程序时发现用户搜索景点名称后默认zoom为16时部分用户会再缩小两级看全景。后来我们把默认zoom提高到14并改用includePoints把景点和周边停车场一起放进视野整体交互数据好了不少。这种细节优化靠的是持续观察而不是一次开发就定型。最后再分享一个小技巧搜索结果列表项强烈建议绑定POI的id作为唯一标识不要用数组索引。搜索刷新或分页加载时索引会变map里的marker和列表的对应关系很容易错乱用id能保证一一对应。我在第二次做类似功能时把列表项的data属性直接挂上POI对象一劳永逸地解决了对应关系问题。这个项目做完到现在我把这套搜索定位链路沉淀成了一个工具方法库Web端一个方法、Android端一个类、小程序端一个公共函数。后面再做任何地图相关的项目直接扒过来改个key就能用省了不少重复造轮子的时间。地图开发的核心从来不是API记得多熟而是把交互链路里的坑提前踩平让用户无感地完成搜索到定位的过程。
返回列表