简介:微信小程序地图Demo是一份面向小程序初学者的轻量示例项目,旨在演示如何在地图组件中实现自定义气泡标签、根据两个地理位置绘制线路等常见功能。资源包共包含24个文件,其中6个js逻辑脚本、5个wxss样式、4个wxml页面结构、4个json配置文件,外加图片与辅助资源,压缩包整体仅296KB,结构精简便于逐文件拆解。目前已有568人学习查看,适合需快速上手微信地图开发的开发者参考。通过研究该demo,读者能掌握wx.createMapContext获取地图实例、addMarker添加带callout气泡的标记、addPolyline连接起点终点的核心调用方式,还能学习app.json、project.config.json等工程配置的写法,理解页面、组件与工具模块间的组织关系,为后续扩展实时定位、路线规划等高阶地图应用提供清晰起点。
1. 微信小程序地图demo:为什么照着文档写还是原地翻车
微信小程序地图demo,说白了就是在小程序里嵌入一张可交互的地图,标出店铺、活动点位,再画上几条路线。听着简单,真做起来翻车点不少。我见过太多人卡在“模拟器里一切正常,一上真机就白屏”“marker点了没反应”“路线画出来歪了十万八千里”,然后开始怀疑人生,甚至怀疑是微信的错。其实大多数情况下,问题出在大家对地图组件的工作方式理解不到位,以及忽略了一些关键的初始化步骤。
这个demo能解决什么?它让你快速在项目里落地一个带定位、标点、路线展示的地图页面。适合谁?适合那些在后台管理系统里管理位置数据,或者做门店展示、活动导览、周边服务的小程序开发者和产品。一句话总结:这活儿不需要你从零造轮子,但需要你知道轮子该怎么装、往哪打气、什么时候不该硬拧螺丝。
2. 从零在页面里塞进一张地图:核心是map组件不是SDK接入
2.1 先建页面还是先配权限?顺序错了白忙一场
很多人一上来就急着写wxml,结果map组件一片空白,报错也看不见。我一般的操作是先把页面骨架建好,再回头配置权限和域名。因为地图demo的卡点不在代码,而在基础配置,配置不对,代码再对也渲染不出来。
在小程序项目的根目录找到app.json文件,先注册页面路径。假设我们新建了一个pages/map/index页面:
{ "pages": [ "pages/index/index", "pages/map/index" ], "permission": { "desc": "获取你的位置信息用于展示附近门店" }, "requiredPrivateInfos": [ "getLocation" ] }这段配置里最关键的是requiredPrivateInfos,从基础库2.21.3开始,微信要求开发者主动声明需要用到的地理位置接口,不然wx.getLocation实际调用时会被直接拒掉。permission.desc是用户在授权弹窗里看到的说明文字,写得越直白越能降低拒绝率。
配完这两项,map组件本身的wxml才能正常干活。很多人漏了这一步,一直以为是map组件的问题,其实根本没走到组件那一步。建议每次新建地图相关页面时,先检查app.json里这两个字段,避免后期排查定位问题时再去翻文档,走弯路。
有了页面和权限,我们才能在wxml里放map组件。别小看这个顺序问题,它直接影响demo能不能跑起来。
2.2 让地图渲染出来的最小代码块长什么样
wxml放map组件,js里塞数据,这几乎是所有地图demo的基本盘。新手容易卡在数据格式上,markers必须是一个数组,里面每一项的经纬度是必填,其他字段按需补充。把下面这段直接贴到页面的wxml里,再配合js初始化就能跑出第一张地图。
<view class="container"> <map id="myMap" latitude="{{latitude}}" longitude="{{longitude}}" markers="{{markers}}" polyline="{{polyline}}" scale="12" show-location enable-overlooking style="width: 100%; height: 400rpx;" bindmarkertap="onMarkerTap" /> </view>对应js部分:
Page({ data: { latitude: 22.540503, longitude: 113.934528, markers: [ { id: 1, latitude: 22.540503, longitude: 113.934528, title: '南山店', iconPath: '/assets/marker.png', width: 32, height: 32, callout: { content: '南山区科技园', display: 'BYCLICK', borderRadius: 6, padding: 8 } } ], polyline: [] }, onReady() { // 地图组件ready之后再操作,避免拿不到实例 console.log('map ready'); }, onMarkerTap(e) { const markerId = e.detail.markerId; console.log('点击了标记点', markerId); } });这段代码里的scale="12"控制缩放级别,范围是3到20,数字越大视野越精细。show-location会在当前位置显示蓝点,这是做门店导览类demo最常用的开关。enable-overlooking是倾斜视角,适合展示路线全貌,但说实话在2D地图上效果有限,要不要开启看产品需求。
bindmarkertap是marker的点击事件,后续放自定义弹窗、跳转详情页都靠它。注意这里的回调参数e.detail.markerId是数字,不是字符串,做比较的时候别用===去比字符串,容易踩坑。
地图的宽高千万不要用100%,map组件的渲染机制决定了它必须有一个确定的尺寸,百分比在某些基础库版本下会计算出0的高度,导致地图直接不显示。给个固定的px或rpx值,是最稳妥的。
2.3 为什么说map组件是一个披着标签外衣的渐进式画布
map组件看着像一个普普通通的标签,实际上它是一个原生的渐进式画布。在日常开发中,你拖动地图、缩放地图、点击标记,所有交互都是原生层在接管,小程序的逻辑层只是被动接收事件回调。这意味着两件事:一是你在页面里写的覆盖物如果层级不对,会被原生地图盖住;二是当你用wx.createMapContext获取实例之后,很多操作是异步的,需要等到画布就绪。
我一般会在onReady里创建MapContext,然后调用moveToLocation把视野移动到当前定位点。注意,onReady是页面的生命周期,地图组件内部可能还没完全就绪,如果下次调用不生效,可以加一个延时兜底,比如:
onReady() { this.mapCtx = wx.createMapContext('myMap'); setTimeout(() => { this.mapCtx.moveToLocation({ latitude: this.data.latitude, longitude: this.data.longitude }); }, 300); }这个延时就是给地图内层做初始化留时间,实战里300毫秒够用。把它当作固定套路写进代码里,能省掉很多莫名奇妙的bug。理解map组件的这一层“画布”属性之后,你才能更好地控制标记点、路线和视野,这就是整个地图demo的核心操作思路。
3. 在demo里把用户定位变成地图上的蓝点:授权与getLocation的边界条件
3.1 绕不开的两个API:先授权再拿坐标
地图要实用,拿用户位置是常见需求。微信的位置能力分两层:wx.getLocation负责拿坐标,wx.chooseLocation负责让用户选点,后者的结果会返回名称、地址和经纬度。demo阶段先用wx.getLocation就够了,因为它是自动的,不需要用户额外操作。
先看代码,一个能拷贝到项目里直接用的授权与定位逻辑:
const getLocationWithAuth = () => { return new Promise((resolve, reject) => { wx.getSetting({ success: (res) => { const hasAuth = res.authSetting['scope.userLocation']; if (hasAuth) { wx.getLocation({ type: 'gcj02', success: resolve, fail: reject }); } else { wx.authorize({ scope: 'scope.userLocation', success: () => { wx.getLocation({ type: 'gcj02', success: resolve, fail: reject }); }, fail: () => { wx.showModal({ title: '需要位置权限', content: '请在设置页开启位置权限,否则无法显示附近服务', confirmText: '去设置', success: (res) => { if (res.confirm) { wx.openSetting(); } } }); } }); } } }); }); };这段代码把授权判断、主动授权、被动引导三层逻辑串起来了。第一次进入页面时会弹窗问用户,用户拒绝后,下一次再调用就直接引导到设置页手动打开。这个交互是微信官方的推荐路径,demo做成这样才像一个能上线的东西。
type: 'gcj02'是国测局坐标,国内的所有小程序地图都必须用这个坐标系,千万别传wgs84,不然marker和路线全都偏移。这个参数几乎决定了整个地图demo是否正常,因为它直接关系到位置数据的正确性、路线的准确性。
拿到坐标之后,把结果setData到markers数组里,蓝点就能出来。getLocation返回的accuracy字段是精度,单位是米,如果精度大于500米,那大概率是环境遮挡太严重,或用户在室内,这种数据直接画上去会闹笑话。
3.2 markers数组是地图的骨架:从单个坐标到多点标注
单点标蓝点是demo的基础,但在实际使用场景里,比如门店列表、活动点位、设备分布,我们几乎总是需要把一堆点同时标上去。markers数组就是为此设计的,每个元素是一个marker对象,抽取公共字段作为模板,循环拼接即可。
我常用的一种写法是,把后台传回来的原始数据,加工成markers要求的格式:
const rawData = [ { name: '店A', latitude: 22.542, longitude: 113.930, type: 'shop' }, { name: '店B', latitude: 22.531, longitude: 113.918, type: 'activity' } ]; const markers = rawData.map((item, index) => ({ id: index, latitude: item.latitude, longitude: item.longitude, title: item.name, iconPath: item.type === 'shop' ? '/assets/shop.png' : '/assets/activity.png', width: 28, height: 28 })); this.setData({ markers });这样一来,新增点位只需要改rawData,地图上的标记会自动同步更新。这里需要特别注意的是:markers的id必须是数字且唯一。如果你在循环里用的是字符串的id,比如'id_' + index,在某些基础库版本下点击事件会拿到undefined,定位问题会让你怀疑人生。
另外,iconPath的路径必须放在项目里,不能用网络图片。一旦用网络URL,marker在真机上会神秘消失,模拟器却正常。这种“模拟器能用、真机不能用”的反差,是地图demo阶段最常见的坑。把图标做成小尺寸的PNG放到assets目录,最好是32px以内,既能减少包体积,也能避免加载不出来导致的空白。
4. 画路线与模拟轨迹:polyline的坐标点从哪来,参数怎么调
4.1 路线数据的真实来源与折线绘制的三种玩法
polyline是地图上画线条的字段,核心数据是一个包含经纬度坐标的数组。Demo里最简单的做法是写死几个坐标点,把线画出来。但真实项目里,路线数据一般来自三种渠道:用户在页面上的点击轨迹、后台接口返回的路线规划结果、还有通过定时器模拟出来的运动轨迹。
我先讲解最通用的系统方案——用接口返回的坐标点画路线。假设有一个接口返回了这样一组数据:
const routeData = { points: [ { latitude: 22.540503, longitude: 113.934528 }, { latitude: 22.541200, longitude: 113.935100 }, { latitude: 22.542300, longitude: 113.936800 } ] };把坐标点转成polyline数组:
const polyline = [{ points: routeData.points, color: '#FF6A00', width: 6, dottedLine: false, arrowLine: true, borderColor: '#FFFFFF', borderWidth: 2 }]; this.setData({ polyline });这里arrowLine: true会在每条线段上绘制方向箭头,特别适合轨迹展示,比如物流配送、运动记录。borderColor和borderWidth是描边参数,用白色描边可以让路线在深色地图底图上更清晰。实测中,宽度设置成6,视觉上是一根不粗不细的线,配合2px描边,在所有地图样式下都足够醒目。
如果要做运动轨迹那种实时轨迹,需要借助wx.createMapContext的translateMarker接口,它可以平滑移动指定marker的位置。这是一个经典的动画系统:定时器每隔一段时间更新坐标,marker沿着路线动起来,视觉上就是一个人在移动,同时,你还可以用polyline来画走过的路程,一举两得。
4.2 路线跑飞了?坐标格式和缩放级别是两大元凶
路线画出来歪了,最常见的原因是坐标系混用。接口返回的如果是GPS原始坐标(wgs84),直接塞给polyline,路线就会在地图上整体偏移个几十米到几百米,看起来就像路线跑飞了。正确姿势是前端统一转成gcj02。
转换代码在真实项目里属于标配工具函数,我自己常用下面这套,是业界公开的算法,直接拷贝就能用:
function gcj02encrypt(lng, lat) { const a = 6378245.0; const ee = 0.006693421622965943; let dLat = transformLat(lng - 105.0, lat - 35.0); let dLng = transformLng(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 { longitude: lng + dLng, latitude: lat + dLng }; } function transformLat(x, y) { let ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x)); ret += (20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0; ret += (20.0 * Math.sin(y * Math.PI) + 40.0 * Math.sin(y / 3.0 * Math.PI)) * 2.0 / 3.0; ret += (160.0 * Math.sin(y / 12.0 * Math.PI) + 320 * Math.sin(y * Math.PI / 30.0)) * 2.0 / 3.0; return ret; } function transformLng(x, y) { let ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x)); ret += (20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0; ret += (20.0 * Math.sin(x * Math.PI) + 40.0 * Math.sin(x / 3.0 * Math.PI)) * 2.0 / 3.0; ret += (150.0 * Math.sin(x / 12.0 * Math.PI) + 300.0 * Math.sin(x / 30.0 * Math.PI)) * 2.0 / 3.0; return ret; }这段算法把坐标系转换的逻辑封装好了,用到的时候把GPS坐标传进去,拿到的对象直接喂给polyline,偏移问题基本就解决了。注意,这个方案在绝大多数国内标准地图上验证是靠谱的,但如果你的地图底图是自定义瓦片,算法不一定适用,因为瓦片底图坐标系如果是其他坐标系,拼在一起会有更大的视觉偏移。
除了坐标系,缩放级别也会让路线显得不对劲。scale="12"是市级的视野,如果路线只有几百米长,在这个级别下看起来就是一小段短线,用户体会不到方向感。建议根据路线的整体跨度算一下合适的缩放级别,比如对角线距离:
const distance = getDistance(lat1, lng1, lat2, lng2); // 两个端点的距离 let scale = 16; if (distance > 5000) scale = 13; else if (distance > 2000) scale = 14; else if (distance > 800) scale = 15; this.setData({ scale });这里getDistance可以直接用微信API里的wx.getLocation搭配MapContext,也可以自己写一个简单的Haversine公式。实践上,先用两点的经纬度算出直线距离,再映射到scale,体验比固定写死要好很多。因为每段路线的跨度不同,一套自适应算法能覆盖所有场景,避免路线过长被截断、路线过短看不清的问题。
5. 地图demo避坑手册:覆盖层盖不住、坐标系漂移、真机白屏这些玄学
5.1 覆盖层盖不住地图原生组件:现象是弹窗和按钮被地图压在底下
现象:把弹窗、底部菜单、自定义气泡放在地图上方,结果在真机上一运行,地图全给盖住了,模拟器上看起来却好好的,因为在模拟器上组件层级是虚拟的,而真机上原生地图用的是原生层渲染。
原因:map是原生组件,它有特殊的同层渲染策略,严格遵循“地图永远在最上层”的规则。在旧版基础库上,不能用普通view覆盖地图。在支持同层渲染的基础库上,需要地图组件开启enable-scroll、enable-zoom等属性,并调整组件的cover-view或同层渲染开关。
解决:套路分两步。第一步,把基础库调高,至少要2.7.0以上;第二步,优先使用cover-view和cover-image标签放在地图内部来承载按钮和弹窗,这个组件专门设计成能盖在地图上面。如果只是展示文字标签,直接使用callout字段,它本身就是marker上长出来的原生气泡。
实战里最省事的做法是:弹窗这种级别的内容,别放在map组件里面,把它放到地图外面平级。等到地图数据交互不需要弹窗时再挂在地图上。这个思路虽然没那么炫,但是可靠性极高,适合新手期。
5.2 marker在地图上漂移是坐标系没对齐:现象是定位点对不上实际建筑
现象:拿着手机站在某栋楼前,地图上蓝点却漂到对面马路甚至隔壁街区。轻则造成用户不信任,重则把用户导航到错误地点,这种情况在室内和桥下尤其明显。
原因:小程序给APP的交还坐标是gcj02,但你的marker数据来源五花八门,有的是GPS设备采集的直接用的原始坐标,有的是外部地图服务返回的,坐标系没统一成gcj02,就会漂移。
解决:我前面给的那段gcj02加密算法可以直接用。另外,如果数据来源是后台SQL里拉出来的,记得在写入的时候就转好,不要在端上做转换,让后台做完变换再下发,前端永远只处理gcj02。这样能减少很多不必要的麻烦。
还有一个肉眼可见的漂移原因:精度不足。wx.getLocation返回的accuracy如果大于100米,直接提示用户“信号弱,请到开阔地”远远好过拿这个点去画图。因为数据一旦不可靠,后面所有的展示和交互就全在错误的基础上,用户看到的自然是一堆乱象。
5.3 真机白屏但模拟器正常:现象是预览时地图区域空荡荡
现象:开发工具里一切正常,预览二维码扫出来,地图区域一片白。而且报错插件也不弹,控制台也没输出,肉眼排查无处下手。
原因:第一,基础库版本过低;第二,map组件的宽高异常,我前面提过别用100%;第三,项目里的iconPath用的是网络图片,真机不给加载。
解决:先看详情里的本地基础库版本,低于2.7.0直接升级。然后检查wxml里map标签的style,不要用百分比。最后,把markers里所有的iconPath换成本地路径,清掉缓存重新编译。实测中,一半以上的白屏都是这个原因。
如果这三个都查了还是白屏,看一下后台配置的域名里,有没有把地图瓦片相关的域名加到downloadFile合法域名。虽然map组件内部渲染不走业务域名,但SDK版本升级后会有额外的请求需求,域名没配上,真机就静默失效。这一点属于“黑匣子”范畴,不看网络请求面板很难定位。
5.4 polyline不显示或路线残缺:数据处理要小心空坐标点和数量上限
现象:路线画出来了,但断断续续,或者一整段路直接不显示。这比白屏还让人崩溃,因为你得确认是数据断了还是组件没渲染对。
原因:polyline的points数组里有空的经纬度,或者坐标点存在[0, 0]这种合法但实际无意义的数据。此外,基础库对polyline的坐标点数量有上限,超过一定数量需要分段画。
解决:在加工polyline之前,先做一层清洗:
const validPoints = routeData.points.filter(p => { return p.latitude && p.longitude && Math.abs(p.latitude) > 0.1; }); if (validPoints.length < 2) return;过滤掉空值和全0坐标,确保至少有起点和终点,因为polyline至少需要两个点才能画线。这个清洗逻辑放在数据进入setData之前执行,能解决99%的线路不显示问题。
碰到坐标点特别多的大跨径路线,可以拆分成多条polyline,每条polyline管理一段,视觉上还是连续的。可以每次最多喂100个点给一条polyline,然后用相同颜色和宽度拼接起来,视觉上无缝隙。
6. 把demo做出可用性:自定义气泡、视野自适应与性能体检一次说清
走到这一步,你已经能跑通地图、标点、画线了。接下来要做的,是把demo从“能看”打磨成“能用”。这里面有三个收尾必做的动作,每一项都不复杂,但价值直接拉满。
第一个动作是自定义气泡,替代默认的callout。默认气泡长得很素,连样式都不好调,关键时刻不显内容。用cover-view盖在地图上是个办法,但最优雅的还是用marker的customCallout能力,结合slot渲染一段布局进去。代码演示如下:
<map markers="{{markers}}" bindmarkertap="onMarkerTap" style="width: 100%; height: 500rpx;" > <cover-view slot="callout" class="custom-callout" wx:if="{{selectedMarker}}"> <cover-view class="callout-title">{{selectedMarker.title}}</cover-view> <cover-view class="callout-desc">{{selectedMarker.desc}}</cover-view> </cover-view> </map>slot="callout"是map组件内置的插槽,专门用来承载自定义气泡内容。wx:if控制只有你点击了某个marker时才渲染,这个点配合selectedMarker这个数据字段,保证气泡跟随点击动态更新。要注意,插槽里的标签只能用cover-view或者cover-image,普通view在这里是会出层级的。在后续布局上,气泡里别放太复杂的内容,因为它是原生层,交互能力和普通网页元素不完全一样,事件穿透也要仔细处理。
第二个动作是视野自适应。我前面提过用距离算scale,这里补充一个更直接的工具——MapContext.includePoints,它可以把一组坐标点缩放到当前视野内,让所有点刚好完全可见:
this.mapCtx.includePoints({ points: allPoints, padding: [50, 50, 50, 50] });padding是四个方向留白,单位是px,50px的留白能让气泡不被卡在窗口边缘,头部的标题栏也不会遮住视野。当用户定位完成后,调用moveToLocation或者includePoints,视野自动框住所有标记,体验立刻高级起来,从脚本时代一步跨到产品可用。
第三个动作是性能体检。地图是很吃性能的组件,一个页面如果加了好几个地图实例,内存和渲染开销直接拉满。我的建议是:一个页面只放一个map组件,多个地图场景用不同页面承载,别堆在同一个页面里。如果markers数量特别多,超过100个,就要考虑做聚合或按视野动态加载。可以用bindregionchange监听视野变化,只渲染当前视野内的markers,没在视野里的全部过滤掉。
最后,养成一个习惯:每次改完地图相关代码,一定要真机预览,而不是只在开发工具里看。模拟器上地图的表现和真机差别太大,从层级、权限到坐标精度,完全是两个世界。我曾经在模拟器上花了两个小时调一个气泡位置,结果一上真机就错位,后来直接用真机调试,每分钟改完都预览,反而省时很多。期待你的地图demo顺利落地上线,希望帮到你。
本文还有配套的精品资源,点击获取