一、map 组件能力全景
1.1 基础用法
map 是小程序的原生组件,需要显式指定 id 以便通过 createMapContext 拿到控制器:
<map
id="mainMap"
class="map"
latitude="{{latitude}}"
longitude="{{longitude}}"
scale="16"
markers="{{markers}}"
polyline="{{polyline}}"
show-location
enable-zoom
enable-scroll
enable-rotate="{{false}}"
bindmarkertap="onMarkerTap"
bindcallouttap="onCalloutTap"
bindregionchange="onRegionChange"
/>
map 组件必须有明确高度(如 .map { width: 100%; height: 100vh; }),否则渲染为空,Skyline 渲染下也不能靠 flex 自动撑高。
1.2 组件属性分组
| 分组 | 属性 | 说明 |
|---|---|---|
| 视野 | latitude、longitude、scale、include-points | scale 取 3 到 20,越大越近;include-points 自动缩放包含所有点 |
| 覆盖物 | markers、polyline、polygons、circles | 标记、线、面、圆 |
| 交互 | enable-zoom、enable-scroll、enable-rotate | 手势开关 |
| 显示 | show-location、show-compass、show-scale | 定位点、指南针、比例尺 |
| 事件 | bindmarkertap、bindcallouttap、bindregionchange | 交互回调 |
1.3 地图控制器
在 onReady 中调用 this.mapCtx = wx.createMapContext('mainMap', this) 即可拿到控制器,常用方法如下:
| 方法 | 作用 |
|---|---|
getCenterLocation() | 获取地图中心点坐标 |
getRegion() | 获取当前视野的西南、东北角坐标 |
includePoints() | 缩放视野包含指定点 |
moveAlong() | 让 marker 沿路径移动 |
fromScreenLocation() / toScreenLocation() | 屏幕坐标与地图坐标互转 |
openMapApp() | 唤起第三方地图 App 导航 |
addMarkers() / removeMarkers() | 动态增删标记 |
setCenterOffset() | 设置中心点偏移 |
setCenterOffset 是做「底部弹层加地图标记居中」时最实用的方法:底部有 300px 面板时,this.mapCtx.setCenterOffset({ offset: [0, 150] }) 就能把中心点往上偏移 150px。
二、定位授权与 API 选型
2.1 定位 API 对比
| API | 返回精度 | 是否需声明 | 适用场景 |
|---|---|---|---|
wx.getLocation | 精确(十米级) | 需 requiredPrivateInfos | 打卡、导航、轨迹 |
wx.getFuzzyLocation | 模糊(城市级) | 需单独申请 | 天气、城市推荐 |
wx.chooseLocation | 用户选点 | 需声明 | 选收货地址 |
wx.openLocation | 展示地图 | 无需 | 查看位置详情 |
wx.onLocationChange | 连续定位 | 需声明 | 轨迹记录、跑步 |
2.2 getLocation 的参数取舍
wx.getLocation({
type: 'gcj02', // wgs84 或 gcj02,国内地图必须用 gcj02
isHighAccuracy: true, // 开启高精度定位
highAccuracyExpireTime: 4000, // 超时后降级返回当前结果
success: (res) => console.log(res.latitude, res.longitude, res.accuracy),
fail: (err) => handleLocationFail(err)
});
| 参数 | 取值 | 影响 |
|---|---|---|
type | gcj02 | 可直接用于 map 组件与腾讯地图服务 |
type | wgs84 | GPS 原始坐标,需转 gcj02 才能上图 |
isHighAccuracy | true | 精度提升到十米级,但更慢更耗电 |
highAccuracyExpireTime | 毫秒 | 超时未拿到高精度结果就返回当前结果 |
国内业务一律用 type: 'gcj02',需要轨迹精度时再开 isHighAccuracy。用 wgs84 直接上图会出现「位置偏移几百米」的经典问题。
2.3 三层权限
| 层级 | 内容 | 检查方式 |
|---|---|---|
| 系统层 | 手机定位服务是否开启 | wx.getSystemSetting().locationEnabled |
| 小程序层 | 用户是否授权小程序 | wx.getSetting().authSetting['scope.userLocation'] |
| 隐私层 | 是否同意隐私协议 | wx.getPrivacySetting() |
任何一层不通过都会导致定位失败,提示语必须能区分,否则用户永远不知道该去哪里开。小程序层被拒后只能引导去设置页:判断 res.authSetting['scope.userLocation'] === false 时弹 wx.showModal,用户确认后调用 wx.openSetting();未授权过则用 wx.authorize({ scope: 'scope.userLocation' }) 主动拉起。连续定位则用 wx.startLocationUpdate() 启动、wx.onLocationChange 接收回调、wx.stopLocationUpdate() 停止;需要在后台继续记录时改用 wx.startLocationUpdateBackground,并在 app.json 声明 requiredBackgroundModes: ["location"]。
三、坐标系与转换
3.1 三套坐标系
| 坐标系 | 全称 | 使用方 | 特点 |
|---|---|---|---|
| WGS84 | 世界大地测量系统 | GPS 硬件、国际标准 | 真实坐标 |
| GCJ-02 | 国测局坐标(火星坐标) | 腾讯地图、高德、微信 | 加密偏移 |
| BD-09 | 百度坐标 | 百度地图 | 在 GCJ-02 上二次偏移 |
国内直接使用 WGS84 坐标在地图上打点,会出现几百米的偏移。这不是 bug,而是坐标加密的结果。
3.2 转换实现
// utils/coord.js
const PI = Math.PI;
const A = 6378245.0; // 长半轴
const EE = 0.00669342162296594323; // 偏心率平方
// 经纬度偏移量(GCJ-02 加密核心)
function delta(lng, lat) {
let dLat = -100 + 2 * lng + 3 * lat + 0.2 * lat * lat + 0.1 * lng * lat
+ 0.2 * Math.sqrt(Math.abs(lng));
dLat += (20 * Math.sin(6 * lng * PI) + 20 * Math.sin(2 * lng * PI)) * 2 / 3;
dLat += (20 * Math.sin(lat * PI) + 40 * Math.sin(lat / 3 * PI)) * 2 / 3;
dLat += (160 * Math.sin(lat / 12 * PI) + 320 * Math.sin(lat * PI / 30)) * 2 / 3;
let dLng = 300 + lng + 2 * lat + 0.1 * lng * lng + 0.1 * lng * lat
+ 0.1 * Math.sqrt(Math.abs(lng));
dLng += (20 * Math.sin(6 * lng * PI) + 20 * Math.sin(2 * lng * PI)) * 2 / 3;
dLng += (20 * Math.sin(lng * PI) + 40 * Math.sin(lng / 3 * PI)) * 2 / 3;
dLng += (150 * Math.sin(lng / 12 * PI) + 300 * Math.sin(lng / 30 * PI)) * 2 / 3;
const radLat = lat / 180 * PI;
let magic = Math.sin(radLat);
magic = 1 - EE * magic * magic;
const sqrtMagic = Math.sqrt(magic);
return [
(dLng * 180) / (A / sqrtMagic * Math.cos(radLat) * PI),
(dLat * 180) / ((A * (1 - EE)) / (magic * sqrtMagic) * PI)
];
}
// WGS84 -> GCJ-02
function wgs84ToGcj02(lng, lat) {
// 境外不偏移,直接返回原始坐标
if (lng < 72.004 || lng > 137.8347 || lat < 0.8293 || lat > 55.8271) return [lng, lat];
const [dLng, dLat] = delta(lng - 105, lat - 35);
return [lng + dLng, lat + dLat];
}
// GCJ-02 -> WGS84(反向近似,精度足够业务使用)
function gcj02ToWgs84(lng, lat) {
const [gLng, gLat] = wgs84ToGcj02(lng, lat);
return [lng * 2 - gLng, lat * 2 - gLat];
}
百度坐标系 BD-09 是在 GCJ-02 基础上再做一次极坐标偏移,需要与百度地图对接时由服务端统一转换输出即可,端上不必实现。
3.3 坐标转换的工程约定
| 场景 | 处理方式 |
|---|---|
wx.getLocation({ type: 'gcj02' }) | 直接上图,无需转换 |
| 设备 GPS 原始数据(wgs84) | 先转 GCJ-02 再上图 |
| 服务端存储 | 统一存 GCJ-02,避免前端反复转换 |
| 与百度地图对接 | 由服务端统一转 BD-09 输出 |
最容易被忽略的一条:数据库里存的到底是什么坐标系,必须写在接口文档里。坐标系混乱导致的「位置对不上」问题,排查成本远高于提前约定。
四、marker 与自定义气泡
4.1 marker 配置
const markers = [{
id: 1,
latitude: 39.908823,
longitude: 116.397470,
width: 32,
height: 32,
iconPath: '/assets/marker-shop.png',
anchor: { x: 0.5, y: 1 }, // 锚点在图标底部中心
callout: {
content: '门店 A\n营业中',
color: '#323233',
fontSize: 13,
borderRadius: 8,
bgColor: '#ffffff',
display: 'BYCLICK' // ALWAYS | BYCLICK
},
label: { content: '门店 A', color: '#1989fa', fontSize: 12, anchorX: -24, anchorY: -48 }
}];
callout 与 label 的区别:
| 字段 | 位置 | 交互 | 适用 |
|---|---|---|---|
callout | 图标上方气泡 | 可点击(bindcallouttap) | 展示详情、可跳转 |
label | 可自由定位的文本 | 不可点击 | 常驻名称标注 |
4.2 自定义气泡与 customCallout
callout 样式能力有限(只有纯文本)。需要图文混排、圆角阴影、按钮时用 customCallout:
<map id="mainMap" markers="{{markers}}" bindmarkertap="onMarkerTap">
<cover-view slot="callout">
<cover-view
wx:for="{{markers}}"
wx:key="id"
marker-id="{{item.id}}"
class="custom-callout"
>
<cover-view class="title">{{item.name}}</cover-view>
<cover-view class="desc">距离 {{item.distance}} 米</cover-view>
</cover-view>
</cover-view>
</map>
要点:customCallout 内的节点必须用 cover-view,普通 view 在原生组件上不可见;每个气泡通过 marker-id 与 marker 关联;气泡内容变化时更新 markers 数组即可,但要避免高频更新,因为每次更新都会重建原生层。marker 数量超过 200 时应关闭 callout 默认展示、改为点击展示,并按 getRegion() 拿到的视野范围筛选,在 bindregionchange 的 type 为 end 时触发加载(拖动过程中不请求)再配 200ms 防抖;超过 1000 个点则改用聚合或点图层。
五、路线规划与距离计算
5.1 两点距离
// 球面距离(Haversine 公式),返回米
function distance(lat1, lng1, lat2, lng2) {
const R = 6371008.8; // 地球平均半径,米
const rad = Math.PI / 180;
const dLat = (lat2 - lat1) * rad;
const dLng = (lng2 - lng1) * rad;
const a = Math.sin(dLat / 2) ** 2
+ Math.cos(lat1 * rad) * Math.cos(lat2 * rad) * Math.sin(dLng / 2) ** 2;
return 2 * R * Math.asin(Math.sqrt(a));
}
Haversine 适合「直线距离」展示,不能用于导航距离:实际路径距离通常是直线距离的 1.2 到 1.5 倍。
5.2 调用腾讯位置服务
小程序不能直接使用腾讯地图的 WebService API 而不配置域名,需要把 https://apis.map.qq.com 加入 request 合法域名,并在腾讯位置服务控制台创建应用拿到 key。
async function planDriving(from, to) {
const url = 'https://apis.map.qq.com/ws/direction/v1/driving/'
+ `?from=${from.latitude},${from.longitude}&to=${to.latitude},${to.longitude}`
+ `&key=${MAP_KEY}&output=json`;
const res = await promisify(wx.request)({ url, method: 'GET' });
if (res.data.status !== 0) throw new Error(`路线规划失败: ${res.data.message}`);
const route = res.data.result.routes[0];
// route.distance 单位米,route.duration 单位秒
// route.polyline 是压缩坐标串,需按每 8 个字符表示一个坐标增量的规则解压后再上图
return { distance: route.distance, duration: route.duration, polyline: route.polyline };
}
5.3 唤起外部导航
wx.openLocation({
latitude: lat, longitude: lng, name: '门店 A',
address: '北京市东城区某路 1 号', scale: 18,
fail: () => {
// 兜底:唤起腾讯地图小程序
wx.openEmbeddedMiniProgram({ appId: 'wx76a9a06e5b4e693e', path: `pages/index/index?lat=${lat}&lng=${lng}` });
}
});
距离计算在业务中的用法与注意事项:
| 场景 | 计算方式 | 注意事项 |
|---|---|---|
| 附近门店排序 | 直线距离加前端排序 | 数据量小于 500 时前端算即可 |
| 配送费计算 | 服务端按路径距离 | 必须服务端算,防篡改 |
| 电子围栏判定 | 直线距离与半径比较 | 边界要考虑定位精度 |
所有涉及权益的计算都必须在服务端复算:前端距离只用于展示与交互反馈。
六、地理围栏与签到打卡
6.1 客户端围栏判定
function checkGeofence(current, fence) {
const d = distance(current.latitude, current.longitude, fence.latitude, fence.longitude);
// 定位精度会带来误差,判定半径要留出余量
return { inside: d <= fence.radius + 50, distance: d };
}
实际工程中围栏半径要大于定位误差。城市峡谷环境下 accuracy 可能达到 100 米以上,此时半径 50 米的围栏判定完全不可靠。
6.2 签到打卡流程
async function punchIn(fenceId) {
const loc = await promisify(wx.getLocation)({
type: 'gcj02', isHighAccuracy: true, highAccuracyExpireTime: 5000
});
// 精度太差时先提示,避免无意义的服务端请求
if (loc.accuracy > 200) {
wx.showToast({ title: '定位精度不足,请到开阔处重试', icon: 'none' });
return;
}
const res = await request({
url: '/api/checkin',
method: 'POST',
// accuracy 供服务端判断是否为模拟定位
data: { fenceId, latitude: loc.latitude, longitude: loc.longitude, accuracy: loc.accuracy, timestamp: Date.now() }
});
wx.showToast({ title: res.code === 0 ? '打卡成功' : res.message, icon: res.code === 0 ? 'success' : 'none' });
}
防作弊手段:
| 手段 | 原理 | 局限 |
|---|---|---|
| 精度阈值过滤 | 模拟定位的 accuracy 常为异常值 | 高级模拟工具可伪造 |
| 时间窗口校验 | 服务端比对请求时间与打卡时间 | 需防重放 |
| 速度合理性 | 两次打卡间位移速度超阈值则拒绝 | 需历史数据 |
| 服务端复算 | 所有规则在服务端执行 | 必须做 |
前端判定只用于体验,服务端判定才用于权益。这条原则在签到、配送、考勤类业务里是底线。
七、轨迹绘制与回放
7.1 轨迹采集与抽稀
采集阶段就要做过滤:accuracy 差于 50 米的点直接丢弃,与前一个点距离小于 5 米的不记录,这样能挡掉大量抖动点。原始轨迹点动辄上千,直接绘制会明显卡顿,还需要用 Douglas-Peucker 算法抽稀:取首尾两点连线,找出距离该线最远的点,若最远距离大于容差就以此为界把轨迹切成两段递归处理,否则用首尾直线替代整段。容差取 10 米时,1000 个点通常能压到 100 到 200 个,视觉上几乎无差异。
7.2 绘制与回放
function buildPolyline(points) {
return [{
points: points.map((p) => ({ latitude: p.latitude, longitude: p.longitude })),
color: '#1989faCC', // 支持 8 位十六进制带透明度
width: 6,
arrowLine: true,
borderColor: '#ffffff'
}];
}
// 用 moveAlong 让标记沿路径移动,比定时 setData 平滑得多
const ctx = wx.createMapContext('mainMap', this);
ctx.moveAlong({
markerId: 1,
path: points.map((p) => ({ latitude: p.latitude, longitude: p.longitude })),
duration: 10000, // 整条路径的总时长,单位毫秒
fail: (err) => console.error('回放失败', err)
});
如果需要按时间轴回放(还原真实速度),把路径按时间间隔切成多段,逐段 await 调用 moveAlong,每段 duration 取两点的真实时间差并限制在 100 到 3000 毫秒之间。回放时同时展示速度曲线、海拔曲线是运动类小程序的常见做法:用 polyline 画轨迹,用 canvas 2D 画曲线图,两者通过共享时间轴联动,具体绘制要点可对照小程序动画与 Canvas 可视化
。
八、定位精度与隐私合规
8.1 精度影响因素
| 因素 | 影响 | 缓解 |
|---|---|---|
| 室内 | 无 GPS 信号,退化为基站或 WiFi 定位 | 提示到室外,放宽精度阈值 |
| 城市峡谷 | 高楼反射导致漂移 | 开启 isHighAccuracy,多次采样取中位数 |
| 首次定位 | 冷启动需下载星历,耗时 5 到 30 秒 | 先展示上次位置,再逐步修正 |
| 设备差异 | 低端机定位芯片较差 | 用 accuracy 字段动态调整策略 |
多次采样提升精度的做法是连续取 3 次定位,每次间隔 200 毫秒,然后按纬度、经度分别取中位数,accuracy 取最小值,这样能有效抵抗离群点。单次失败可以忽略,只要有一次成功即可返回。
8.2 隐私合规要求
// app.json
{
"requiredPrivateInfos": ["getLocation", "chooseLocation", "startLocationUpdate", "onLocationChange"],
"requiredBackgroundModes": ["location"]
}
| 合规项 | 要求 |
|---|---|
| 接口声明 | 用到哪个位置接口就在 requiredPrivateInfos 里声明 |
| 隐私协议 | 在微信公众平台配置用户隐私保护指引,说明采集位置的目的与用途;首次调用定位前用弹窗做前置告知 |
| 拒绝可降级 | 用户拒绝定位后,功能应有可用降级路径,不能直接卡死 |
| 最小必要 | 只需城市级信息时用 wx.getFuzzyLocation,不要申请精确定位 |
| 后台定位 | 非必要不申请,申请时必须在隐私指引里说明 |
| 数据存储 | 轨迹等敏感数据要加密存储,明确保留期限 |
处理隐私授权的入口是 wx.getPrivacySetting():返回 needAuthorization 为 true 时调 wx.requirePrivacyAuthorize() 让系统弹出协议内容,用户同意后继续。拒绝定位后的降级路径是:wx.getLocation 失败时捕获异常,改为按 wx.getStorageSync('last_city') 或用户手选城市拉取热门门店,而不是白屏或反复弹窗。
降级路径不是可选项:审核会重点检查「拒绝授权后是否仍可使用」,直接白屏或死循环弹窗都会被判定为不合规。
九、总结
小程序 LBS 开发的难点集中在三处:坐标系、精度、合规。坐标系上,国内业务统一用 type: 'gcj02',服务端统一存 GCJ-02,需要 WGS84 或 BD-09 时在边界处一次性转换,并把「接口返回什么坐标系」写进文档,这是避免「位置差几百米」的唯一可靠办法。
精度上,isHighAccuracy 与多次采样取中位数能显著改善城市环境表现,但必须承认室内与低端机的能力上限:用 accuracy 字段动态调整判定阈值,围栏半径永远大于定位误差,精度过差时宁可提示重试也不要提交无效数据。合规上,requiredPrivateInfos 声明、隐私保护指引配置、拒绝授权后的降级路径三者缺一不可,且能用模糊定位就别申请精确定位。
功能层面,map 组件的 customCallout 能做出完整的图文气泡,getRegion 配合视野筛选是 marker 数量上千时的必备优化,moveAlong 让轨迹回放比定时 setData 平滑得多,而 setCenterOffset 是解决「底部面板挡住标记」的现成答案。最后记住一条底线:前端算出的距离只用于展示,一切涉及权益的判定都必须在服务端复算。地图上的大量 POI 与门店数据如果要被搜索到,还需要配合小程序搜索与 SEO
做服务直达与内容索引,让位置数据真正产生流量价值。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。