☰
微信小程序地图选点与路线规划实战:腾讯位置服务全解析
2026/10/15 11:52:29 网站建设 项目流程

1. 地图选点与路线规划的项目拆解

1.1 这个功能到底解决什么问题

微信小程序里做地图选点和路线规划,说白了就是两件事:让用户在地图上点一个位置,然后把起点和终点之间的走法算出来。听起来简单,但实际落地时会发现坑不少。比如用户点了地图,你怎么知道点的是哪个具体地址?路线规划出来之后,怎么在小程序里画出来?走路、骑车、开车三种方式怎么切换?这些都需要一套完整的方案。

我最近刚在一个O2O项目里落地了这套功能,用户下单时需要选择上门地址,同时要看到服务人员从门店出发的路线和预计时间。整个流程涉及地图选点、地址解析、路线规划、路线绘制四个核心环节。这篇文章就把我踩过的坑和最终跑通的方案完整分享出来,适合正在做小程序地图相关功能的开发者参考,也适合产品经理了解技术边界在哪里。

1.2 为什么选腾讯位置服务而不是其他方案

小程序生态里能用的地图能力其实就那么几家,选型时我主要对比了三个维度:小程序原生能力、第三方地图SDK、以及腾讯位置服务的WebService API。

小程序原生提供了wx.chooseLocation和wx.openLocation两个API,前者能让用户选点,后者能打开地图查看。但原生API的问题在于定制性太差,选点界面是固定的,没法加自定义的搜索框、分类筛选、周边推荐这些功能。而且路线规划原生API根本不提供,必须走第三方。

第三方地图SDK里,腾讯位置服务对小程序的支持是最完整的。它提供了JavaScript SDK,可以直接在小程序里用,不需要额外引入原生插件。更重要的是它的WebService API覆盖了逆地址解析、地点搜索、路线规划、距离矩阵等全套能力,而且免费额度对小项目来说完全够用。

这里有个关键决策点:是用SDK还是直接调WebService API?我的建议是混合使用。地图展示和交互用SDK,因为需要实时渲染;路线规划和地址解析用WebService API,因为这类请求不需要实时性,而且API的返回数据更结构化,方便做业务逻辑。

1.3 整体架构设计思路

整个功能拆成三层:展示层、逻辑层、数据层。

展示层就是小程序页面,包含一个map组件和若干交互控件。map组件负责渲染地图、标记点、路线折线。交互控件包括搜索框、路线方式切换按钮、地址列表等。

逻辑层处理用户操作,比如点击地图选点、搜索关键词、切换出行方式。每次操作都会触发对应的数据请求,拿到结果后更新展示层。

数据层就是腾讯位置服务的各种API调用。这里要注意,小程序里调用WebService API需要在小程序后台配置合法域名,否则请求会被拦截。具体来说,需要在“开发设置-服务器域名”里把apis.map.qq.com加到request合法域名里。

注意:腾讯位置服务的WebService API有配额限制,个人开发者每天有固定的调用次数,企业认证后额度会提升。如果项目量级较大,建议提前评估调用量,必要时购买配额包。

2. 核心细节解析与实操要点

2.1 地图选点的三种实现方式

地图选点看起来简单,但实现方式直接决定了用户体验。我试过三种方案,各有优劣。

第一种是直接用wx.chooseLocation。这是最省事的方案,一行代码就能调起选点界面。但问题是这个界面完全不可定制,搜索框的样式、地图的初始位置、周边POI的展示都无法控制。而且用户选完点之后,你只能拿到一个地址字符串和经纬度,拿不到更多的结构化信息。

第二种是用map组件自己实现选点。在页面上放一个全屏的map组件,监听bindtap事件获取点击位置的经纬度,然后调逆地址解析API拿到具体地址。这个方案灵活度最高,你可以在地图上加任何自定义控件。但缺点是用户只能通过点击地图选点,没法搜索。要加搜索功能就得自己实现搜索框和结果列表。

第三种是混合方案,也是我最终采用的。页面上半部分是搜索框和地址列表,下半部分是地图。用户既可以搜索关键词选点,也可以直接点击地图选点。点击地图后,地图中心点固定一个标记,同时调逆地址解析拿到地址信息填充到搜索框里。

// 监听地图点击事件 onMapTap(e) { const { latitude, longitude } = e.detail; this.setData({ marker: { id: 1, latitude, longitude, width: 30, height: 30 } }); this.reverseGeocoder(latitude, longitude); } // 逆地址解析 reverseGeocoder(lat, lng) { const key = '你的KEY'; wx.request({ url: `https://apis.map.qq.com/ws/geocoder/v1/?location=${lat},${lng}&key=${key}`, success: (res) => { if (res.data.status === 0) { const { address, formatted_addresses, address_component } = res.data.result; this.setData({ address: formatted_addresses.recommend, city: address_component.city, district: address_component.district }); } } }); }

这里有个细节要注意:逆地址解析返回的formatted_addresses.recommend是推荐地址,适合展示给用户看;而address是标准地址,适合存数据库。我一开始只存了推荐地址,后来做数据分析时发现地址格式不统一,很难做聚合统计,又回头补了标准地址字段。

2.2 路线规划的参数与返回结构

路线规划API的核心参数就几个:起点经纬度、终点经纬度、出行方式。出行方式用mode参数控制,driving是驾车,walking是步行,bicycling是骑行,transit是公交。

// 路线规划请求 getRoutePlan(from, to, mode) { const key = '你的KEY'; const url = `https://apis.map.qq.com/ws/direction/v1/${mode}/?from=${from.latitude},${from.longitude}&to=${to.latitude},${to.longitude}&key=${key}`; wx.request({ url, success: (res) => { if (res.data.status === 0) { const route = res.data.result.routes[0]; this.drawRoute(route.polyline); this.setData({ distance: route.distance, duration: route.duration }); } } }); }

返回结构里最关键的是polyline字段,它是一串压缩后的坐标点。腾讯位置服务用的是Google的编码算法,需要解压才能用。解压后的坐标数组可以直接传给map组件的polyline属性来绘制路线。

// polyline解压函数 decodePolyline(encoded) { const points = []; let index = 0, lat = 0, lng = 0; while (index < encoded.length) { let shift = 0, result = 0, byte; do { byte = encoded.charCodeAt(index++) - 63; result |= (byte & 0x1f) << shift; shift += 5; } while (byte >= 0x20); lat += (result & 1) ? ~(result >> 1) : (result >> 1); shift = 0; result = 0; do { byte = encoded.charCodeAt(index++) - 63; result |= (byte & 0x1f) << shift; shift += 5; } while (byte >= 0x20); lng += (result & 1) ? ~(result >> 1) : (result >> 1); points.push({ latitude: lat / 1e5, longitude: lng / 1e5 }); } return points; }

这个解压算法我一开始没搞懂,直接用了网上找的版本,结果发现坐标偏移了几百米。后来仔细看文档才发现,腾讯位置服务返回的polyline精度是1e5,而有些网上的示例用的是1e6,除错了精度。这个坑我踩了整整一个下午,最后用官方示例的数据对比才定位到问题。

2.3 地图标记与路线的渲染细节

map组件的markers和polyline属性都是数组,可以同时渲染多个标记和多条路线。但这里有几个性能相关的注意事项。

标记点的iconPath建议用本地图片,不要用网络图片。网络图片在弱网环境下加载慢,而且每次setData都会重新请求。我一开始用了网络图标,结果地图上标记点闪烁得厉害,换成base64编码的本地图片后就稳定了。

路线的color和width属性可以控制线条样式。但要注意,arrowLine属性在部分安卓机型上不生效,如果一定要画箭头,建议用自定义的icons属性在路线上叠加箭头图标。

// 绘制路线 drawRoute(polylineStr) { const points = this.decodePolyline(polylineStr); this.setData({ polyline: [{ points, color: '#3B82F6', width: 6, arrowLine: true, borderColor: '#1E40AF', borderWidth: 2 }] }); }

还有一个细节:map组件的includePoints属性可以自动缩放地图以包含所有标记点。但这个属性在路线规划场景下不太好用,因为路线可能很长,自动缩放后地图级别太小,看不清细节。我的做法是手动计算路线的中心点和跨度,然后设置合适的scale值。

3. 实操过程与核心环节实现

3.1 项目初始化与域名配置

第一步是在小程序管理后台配置合法域名。进入“开发-开发设置-服务器域名”,在request合法域名里添加https://apis.map.qq.com。这一步不做的话,所有API请求都会失败,而且报错信息很模糊,只提示“请求失败”,不会告诉你是因为域名没配置。

第二步是申请腾讯位置服务的KEY。在腾讯位置服务官网注册账号,创建应用,然后添加KEY。创建KEY时需要选择启用的产品,把WebService API和JavaScript SDK都勾上。KEY创建后会有一个配额限制,个人开发者每天有固定的调用次数,如果不够用可以申请提升。

第三步是在小程序里封装一个统一的请求方法,把KEY和错误处理都封装进去。

// utils/map.js const KEY = '你的KEY'; const BASE_URL = 'https://apis.map.qq.com/ws'; function request(path, params = {}) { return new Promise((resolve, reject) => { wx.request({ url: `${BASE_URL}${path}`, data: { ...params, key: KEY }, success: (res) => { if (res.data.status === 0) { resolve(res.data.result); } else { reject(new Error(res.data.message || '请求失败')); } }, fail: reject }); }); } module.exports = { reverseGeocoder: (lat, lng) => request('/geocoder/v1/', { location: `${lat},${lng}` }), searchPlace: (keyword, city) => request('/place/v1/search', { keyword, boundary: `region(${city},0)`, page_size: 20 }), direction: (mode, from, to) => request(`/direction/v1/${mode}/`, { from: `${from.latitude},${from.longitude}`, to: `${to.latitude},${to.longitude}` }) };

注意:KEY不要硬编码在代码里,建议放在小程序的配置文件中,或者通过后端接口下发。硬编码的KEY一旦泄露,别人可以盗用你的配额。

3.2 搜索与选点的完整交互流程

用户打开选点页面时,默认展示当前定位附近的地图。这里需要先调wx.getLocation获取用户当前位置,然后以这个位置为中心初始化地图。

onLoad() { wx.getLocation({ type: 'gcj02', success: (res) => { this.setData({ latitude: res.latitude, longitude: res.longitude, scale: 16 }); this.loadNearbyPlaces(res.latitude, res.longitude); }, fail: () => { // 定位失败时默认到城市中心 this.setData({ latitude: 39.908, longitude: 116.397, scale: 12 }); } }); }

搜索框的交互我做了防抖处理,用户输入停止300毫秒后才发起请求。这个细节很重要,不然用户每输入一个字符就发一次请求,既浪费配额又影响性能。

搜索结果列表展示时,每条结果要显示名称、地址、距离三个信息。距离是用wx.getLocation拿到的当前位置和搜索结果坐标算出来的,用Haversine公式计算。

// 计算两点距离 function getDistance(lat1, lng1, lat2, lng2) { const R = 6371; const dLat = (lat2 - lat1) * Math.PI / 180; const dLng = (lng2 - lng1) * Math.PI / 180; const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLng / 2) ** 2; return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); }

用户点击搜索结果后,地图移动到该位置并添加标记,同时把地址信息回填到搜索框。如果用户直接点击地图,则走逆地址解析流程。两条路径最终都汇聚到同一个onLocationSelected方法,保证数据格式一致。

3.3 路线规划与展示的完整实现

路线规划入口在选点完成后出现。用户选好终点后,页面底部弹出一个面板,显示三种出行方式的预计时间和距离。用户切换出行方式时,重新发起路线规划请求。

switchMode(e) { const mode = e.currentTarget.dataset.mode; this.setData({ currentMode: mode }); this.getRoutePlan(this.data.origin, this.data.destination, mode); }

路线绘制时,除了polyline,我还加了起点和终点的标记。起点用绿色图标,终点用红色图标,这样用户一眼就能看出方向。

updateMarkers(origin, destination) { this.setData({ markers: [ { id: 1, latitude: origin.latitude, longitude: origin.longitude, iconPath: '/images/start.png', width: 32, height: 32, callout: { content: '起点', display: 'ALWAYS', padding: 8, borderRadius: 4 } }, { id: 2, latitude: destination.latitude, longitude: destination.longitude, iconPath: '/images/end.png', width: 32, height: 32, callout: { content: '终点', display: 'ALWAYS', padding: 8, borderRadius: 4 } } ] }); }

路线规划返回的duration单位是秒,展示时需要转成“X小时Y分钟”的格式。距离单位是米,超过1000米要转成公里。

formatDuration(seconds) { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); if (h > 0) return `${h}小时${m}分钟`; return `${m}分钟`; } formatDistance(meters) { if (meters >= 1000) return `${(meters / 1000).toFixed(1)}公里`; return `${meters}米`; }

4. 常见问题与排查技巧实录

4.1 定位不准与坐标偏移问题

小程序里拿到的定位坐标是gcj02格式的,腾讯位置服务的API也接受gcj02坐标,所以正常情况下不需要转换。但如果你从其他来源拿到了wgs84坐标(比如GPS设备),直接传给腾讯位置服务就会偏移几百米。

我遇到过一个诡异的问题:在iOS上定位很准,在安卓上偏移了大概500米。排查后发现是部分安卓机型返回的坐标精度不够,wx.getLocation的type参数设成gcj02后问题解决。如果设成wgs84,在小程序里展示会有偏移。

提示:小程序map组件只接受gcj02坐标,所有传给map组件的坐标都必须是gcj02格式。如果你从后端拿到的是其他格式,需要先转换。

4.2 路线规划返回空结果的排查

路线规划API返回空结果的情况我遇到过几次,原因各不相同。

第一次是起点和终点太近,步行路线规划返回了空数组。腾讯位置服务的步行规划有个最小距离限制,大概50米以内不返回路线。解决办法是在调API之前先判断距离,如果小于50米就直接显示“距离太近,无需规划”。

第二次是跨城路线规划失败。驾车规划支持跨城,但骑行和步行不支持。如果用户选了骑行但起点和终点在不同城市,API会返回错误。我的处理方式是捕获错误后提示用户“该出行方式不支持跨城路线”。

第三次是坐标格式错误。我一开始把经纬度写反了,from参数写成了lng,lat,结果API返回“参数错误”。腾讯位置服务的坐标格式是lat,lng,纬度在前,经度在后。这个顺序我记了很久才形成肌肉记忆。

4.3 地图渲染性能优化

路线很长的时候,polyline的点数可能上千个,直接setData会导致页面卡顿。我的优化方案是抽稀,用Douglas-Peucker算法把不必要的点去掉,保留关键转折点。

// 简单的抽稀:每隔N个点取一个 function simplifyPolyline(points, tolerance = 10) { if (points.length <= tolerance) return points; const result = [points[0]]; const step = Math.ceil(points.length / tolerance); for (let i = step; i < points.length - 1; i += step) { result.push(points[i]); } result.push(points[points.length - 1]); return result; }

抽稀之后,1000个点的路线可以压缩到100个点左右,渲染性能提升明显,视觉上几乎看不出差别。

另一个优化点是避免频繁setData。切换出行方式时,我先清空polyline再设置新的,而不是直接覆盖。因为直接覆盖时,map组件会同时渲染新旧两条线,造成闪烁。

4.4 常见问题速查表

问题现象可能原因排查方法解决方案
请求失败,无具体错误合法域名未配置检查小程序后台域名设置添加apis.map.qq.com到request域名
定位偏移几百米坐标格式不匹配确认坐标是gcj02还是wgs84统一转换为gcj02
路线规划返回空距离太近或跨城不支持检查起终点距离和城市距离近时提示用户,跨城时限制出行方式
地图标记闪烁使用了网络图标检查iconPath是否为网络地址改用本地图片或base64
路线绘制卡顿polyline点数过多打印点数确认抽稀处理,保留关键点
逆地址解析返回空坐标在境外或海域检查坐标是否在国内提示用户选择国内地址
搜索无结果boundary参数错误检查城市名是否正确用region(city,0)格式

4.5 几个我踩过的坑和实操心得

第一个坑是KEY的配额。我一开始用个人账号申请,每天只有几千次调用。测试阶段没注意,上线后第一天就把配额用完了,所有地图功能全部失效。后来紧急做了两件事:一是加了请求缓存,同样的坐标不重复请求;二是把KEY升级为企业认证,配额提升了一个数量级。

第二个坑是逆地址解析的并发限制。腾讯位置服务的逆地址解析API有并发限制,同时发太多请求会被限流。我在做“拖动地图实时解析”功能时,用户拖动地图会连续触发解析请求,结果被限流了。解决办法是加防抖,用户停止拖动500毫秒后才发起解析。

第三个坑是map组件的层级问题。小程序里map组件是原生组件,层级最高,普通的view组件盖不住它。我一开始想在map上盖一个半透明的遮罩来做加载状态,结果遮罩被map盖住了。后来改用cover-view组件才解决。但cover-view的支持有限,只能放文本和图片,不能放复杂的布局。

第四个坑是路线规划的时间预估。腾讯位置服务返回的duration是理论时间,没有考虑实时路况。如果项目对时间精度要求高,需要用driving模式下的policy参数,或者接入实时路况接口。我一开始没注意这个,用户看到预计时间不准就投诉,后来加了“预计时间仅供参考”的提示才缓解。

第五个坑是安卓和iOS的定位权限差异。iOS上用户拒绝定位后,再次调用wx.getLocation会直接失败,不会弹权限申请框。安卓上则会再次弹出。所以代码里要做好定位失败的兜底逻辑,不能假设用户一定会授权。

5. 功能扩展与后续优化方向

5.1 周边搜索与分类筛选

基础功能跑通后,我加了周边搜索。用户选点时不一定要搜索具体地址,有时候只是想看看附近有什么。腾讯位置服务的place/v1/search接口支持按分类搜索,比如“美食”“酒店”“加油站”等。

searchNearby(lat, lng, category) { return request('/place/v1/search', { keyword: category, boundary: `nearby(${lat},${lng},1000)`, page_size: 20 }); }

boundary参数支持三种模式:region按城市搜索,nearby按周边搜索,rectangle按矩形区域搜索。周边搜索时半径最大支持1000米,超过这个范围要用region模式。

5.2 地址簿与常用地址管理

用户选过的地址可以存到本地缓存里,下次选点时直接展示“常用地址”。我用wx.setStorageSync存了一个地址数组,最多存10条,超过后淘汰最久未使用的。

saveAddress(address) { const key = 'history_addresses'; let list = wx.getStorageSync(key) || []; list = list.filter(item => item.id !== address.id); list.unshift(address); if (list.length > 10) list = list.slice(0, 10); wx.setStorageSync(key, list); }

这个功能看起来简单,但用户反馈很好。很多用户每次下单都选同一个地址,有了常用地址后操作步骤从五六步减少到一步。

5.3 路线规划的实时刷新

如果项目需要展示实时路况,可以定时刷新路线规划。但要注意刷新频率,太频繁会浪费配额,太慢又失去实时性。我的做法是每30秒刷新一次,只在页面可见时刷新,页面隐藏时停止。

onShow() { this.timer = setInterval(() => { if (this.data.origin && this.data.destination) { this.getRoutePlan(this.data.origin, this.data.destination, this.data.currentMode); } }, 30000); } onHide() { clearInterval(this.timer); }

这个功能在外卖、打车类小程序里很常见,用户能看到骑手或司机的位置实时更新。但要注意,频繁刷新会增加服务器压力和配额消耗,建议根据实际业务需求调整频率。

5.4 多途经点路线规划

腾讯位置服务的路线规划API支持途经点,最多可以传16个。途经点用waypoints参数,格式是lat1,lng1;lat2,lng2。

directionWithWaypoints(from, to, waypoints) { const waypointStr = waypoints.map(p => `${p.latitude},${p.longitude}`).join(';'); return request('/direction/v1/driving/', { from: `${from.latitude},${from.longitude}`, to: `${to.latitude},${to.longitude}`, waypoints: waypointStr }); }

这个功能适合配送场景,一个骑手要送多个订单,可以规划最优路线。但要注意,途经点顺序是固定的,API不会自动优化顺序。如果需要最优顺序,得自己实现路径优化算法,或者用腾讯位置服务的“路径规划-多点路线”接口。

5.5 离线地图与缓存策略

小程序不支持离线地图,但可以缓存路线规划结果。同样的起点和终点,短时间内重复请求没有意义。我用一个简单的缓存对象存了最近100条路线规划结果,key是from_to_mode,有效期5分钟。

const routeCache = new Map(); function getCachedRoute(from, to, mode) { const key = `${from.latitude},${from.longitude}_${to.latitude},${to.longitude}_${mode}`; const cached = routeCache.get(key); if (cached && Date.now() - cached.time < 300000) { return cached.data; } return null; }

这个缓存策略在用户反复切换出行方式时特别有用,避免了重复请求。但要注意缓存过期时间不能太长,否则路况变化后数据就不准了。

6. 一些个人体会

这套地图选点和路线规划的功能,我从零开始搭到稳定运行大概花了两周时间。其中大部分时间不是在写代码,而是在调试各种边界情况和兼容性问题。腾讯位置服务的文档写得还算清楚,但有些细节只有实际跑起来才会发现。

比如逆地址解析返回的formatted_addresses里,recommend和rough两个字段的区别,文档里只有一句话,我试了好几次才搞明白:recommend是适合展示的完整地址,rough是粗略地址,适合做地址分组。还有路线规划的polyline解压,官方示例用的是JavaScript版本,但小程序里没有atob函数,需要自己实现base64解码。

如果你也在做类似的功能,我的建议是先把基础流程跑通,不要一开始就追求完美。先能选点、能规划、能展示,然后再逐步优化体验。遇到问题多看看官方文档的示例代码,大部分坑官方都已经踩过了,只是文档里写得比较简略。

另外,配额管理一定要提前规划。地图服务的调用量很容易超预期,尤其是做实时刷新和周边搜索的时候。建议在开发阶段就加上请求日志,记录每天的调用量,这样上线后心里有数。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询