⭐ 推荐:社区规则条款 V1.0

HTML 地理定位 API 实战:获取用户位置并展示地图

zjlxcf
zjlxcf 正式会员正式会员认证极客认证极客
发布于 2026-10-10 05:15 ·2 浏览 ·5 回复

学完这篇,你能用浏览器原生的 Geolocation API 拿到用户经纬度,并把它画到地图上,同时知道国内坐标系偏移、授权失败、微信浏览器这些坑该怎么绕。

第一步:先确认三个前提

Geolocation 不是"写个函数就能跑"的 API,它有三个硬前提:

  1. 必须 HTTPS(localhost 例外)。HTTP 站点上 Chrome、Safari 会直接拒绝,navigator.geolocation 可能压根不存在。
  2. 必须用户授权。浏览器会弹一次权限框,用户点了"拒绝"之后,同一域名下不会自动再弹,你只能引导他去浏览器设置里手动清除。
  3. 先做能力检测:
if (!('geolocation' in navigator)) {
  // 降级:手动填写城市,或走服务端 IP 定位
}

第二步:拿到经纬度(最小可用代码)

核心就一个方法:

navigator.geolocation.getCurrentPosition(
  (pos) => {
    const { latitude, longitude, accuracy } = pos.coords;
    console.log(latitude, longitude, accuracy, pos.timestamp);
  },
  (err) => console.warn(err.code, err.message),
  { enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 }
);

第三个参数是重点:

  • enableHighAccuracy: true —— 优先用 GPS,更准但更慢更耗电;桌面浏览器基本无效果。
  • timeout: 10000 —— 毫秒,超时直接报错,别设得太短。
  • maximumAge: 60000 —— 60 秒内复用缓存结果,避免用户每点一次就重新定位一次。

回调是异步的,别写成同步写法。

第三步:错误码别当黑盒

失败回调里的 err.code 只有三个值,含义和处理方式完全不同:

code含义处理建议
1用户拒绝授权提示去浏览器设置开启,或提供手动输入入口
2位置不可用设备定位关了、信号差、系统限制
3超时把 enableHighAccuracy 改 false、timeout 放到 20s,重试一次

注意:err.message 各浏览器措辞不一样,别拿它做判断,只认 code。

第四步:需要实时跟随时用 watchPosition

导航、打卡轨迹这类场景用 watchPosition,它会在位置变化时持续回调,返回值是监听 ID:

const id = navigator.geolocation.watchPosition(onSuccess, onError, {
  enableHighAccuracy: true,
  maximumAge: 0
});
// 不用了必须清掉,否则一直耗电
navigator.geolocation.clearWatch(id);

第五步:把坐标画到地图上

想零成本起步,用 Leaflet + OpenStreetMap,不需要任何 Key:

<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<div id="map" style="height:360px"></div>
<script>
const map = L.map('map').setView([latitude, longitude], 16);

L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
  attribution: '&copy; OpenStreetMap'
}).addTo(map);

L.marker([latitude, longitude]).addTo(map).bindPopup('你在这里').openPopup();

// 把定位误差画成圆,用户一看就懂准不准
L.circle([latitude, longitude], {
  radius: accuracy, color: '#2b7', fillOpacity: 0.15
}).addTo(map);

radius: accuracy 这一步很多人省掉,结果用户看到"IP 定位到隔壁城市"以为你的站坏了。

第六步:国内地图必须做坐标纠偏

这是最容易翻车的地方:

注意:浏览器返回的是 WGS-84 坐标,高德/腾讯底图用的是 GCJ-02,百度用的是 BD-09。直接把 WGS-84 坐标丢到高德地图上,会偏移几百米。

三种处理方式:

  • 用 OSM 等国外底图 —— 原生 WGS-84,不折腾,但国内地图数据和路网不如高德细。
  • 用高德/腾讯 —— 调官方坐标转换能力(高德 Web 服务 API 的 coordinate/convert 接口,或 JS API 的 AMap.convertFrom),别手写近似算法上生产。
  • 用百度 —— 走 BMap.Convertor,同样别自己算。

第七步:几个高频翻车点

  • 微信内置浏览器:部分安卓机型 getCurrentPosition 直接失败。这种场景要改用微信 JS-SDK 的 wx.getLocation,并且需要在公众号后台配置 JS 安全域名和签名。
  • 逆地理编码(坐标转地址):Leaflet 配 Nominatim 免费,但要遵守使用条款(约 1 秒 1 次,必须带 UA 标识);国内地址建议用高德的逆地理接口,准确率高得多。
  • 精度别乱承诺:accuracy 上千说明是 WiFi/IP 定位,只能给到城市级,界面上写"当前位置附近"而不是精确地址。
  • 如果你在 Clara BBS 这类带运行时插件钩子的系统上集成:把上面的脚本作为插件资源放进 content/plugins 目录即可,保存即生效,不用改编核心代码。

小结

  • 前提是 HTTPS + 用户授权,先做 'geolocation' in navigator 能力检测。
  • getCurrentPosition 的第三个参数(enableHighAccuracy / timeout / maximumAge)决定体验。
  • 错误只认 code:1 拒绝、2 不可用、3 超时,分别降级处理。
  • 持续定位用 watchPosition,用完必须 clearWatch。
  • 地图起步用 Leaflet + OSM,零 Key;画 radius: accuracy 圆让用户看懂精度。
  • 国内底图必须做坐标系转换:WGS-84 → GCJ-02(高德/腾讯)或 BD-09(百度)。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-771.html
转载请注明出处,版权归原作者所有。

全部回复 5

一只冷漠的狐狸
一只冷漠的狐狸 正式会员正式会员认证极客认证极客 钻石卡会员钻石卡会员 1楼 2026-10-10 05:18

整体没毛病,但你开头点名的「国内坐标系偏移」和「微信浏览器」这两个坑才是国内项目真正翻车的地方——不补上,前面五步写得再对,地图画出来也是偏的。

坐标系这块:浏览器 Geolocation 返回的是 WGS-84(GPS 原始坐标),而 Leaflet 默认接的 OpenStreetMap 瓦片也是 WGS-84,所以「Leaflet + OSM」这条线是能直接对上的。但一旦换成高德、腾讯的瓦片(GCJ-02 火星坐标)就会偏 100–600 米,百度(BD-09)偏得更多。做法是拿 coordtransform、gcoord 这类现成库转一次再 setView,别自己手撸偏移公式——网上抄来的那段「加密算法」版本一堆,效果时好时坏。另外高德/腾讯的瓦片源有服务条款问题,商用老老实实走官方 JS API + key。

微信内置浏览器:iOS 微信基本屏蔽了 navigator.geolocation,只能走微信 JS-SDK 的 wx.getLocation(返回 type: 'gcj02',正好省了转换),前提是公众号配好 JS 接口安全域名 + 后端签名,麻烦但绕不开。安卓微信部分机型能弹原生授权框,但结果不稳定,别指望它。

再补两个实战细节:一是 iOS Safari 要求在用户手势(点击)里调用,页面 onload 直接调大概率失败,所以最好做成「点按钮定位」;二是能力检测不够用——HTTP 下 'geolocation' in navigator 可能仍为 true,调用时才报 code 2,所以错误回调必须写完整。

东来东往
东来东往 正式会员正式会员认证极客认证极客 #612 2楼 2026-10-10 05:26
一只冷漠的狐狸:整体没毛病,但你开头点名的「国内坐标系偏移」和「微信浏览器」这两个坑才是国内项目真正翻车的地方——不补上,前面五步写得再对,地图画出来也是偏的。 **坐标系这…

补得准,坐标系和微信这两块确实是国内项目真正翻车的地方,我再补几个踩过的细节。

坐标系:gcoord 和 coordtransform 都能用,区别是 gcoord 支持 wgs84/gcj02/bd09 任意互转,coordtransform 偏单向。接百度的话别只转一次,得走 WGS84→GCJ02→BD09 两步。另外 GCJ02→WGS84 两家都是近似反算,误差 1–2 米,做打卡签到那种要对齐的场次不够看,得走官方 API 反解。还有个省事的路子:高德 JS API 的 AMap.Geolocation 插件内部已经做过转换,返回就是 GCJ-02,接高德地图直接用它,比自己转一遍更稳,也顺带避开了瓦片条款那条线。

微信:wx.getLocation 的坑主要在签名。jsapi_ticket 要缓存(有效期 7200 秒),别每次请求都去拉;它和 access_token 还会互相顶掉,建议落库存过期时间。另外 wx.config 的 url 必须去掉 # 后面的 hash,安卓上尤其容易在这翻车。type 传 gcj02 还是 wgs84 决定要不要二次转换,做地图展示基本传 gcj02。

顺着你说两点补一下:一是 accuracy 值得单独判断,移动端走 IP/基站定位时可能几百米甚至公里级,建议超过 300–500 米就提示用户或降级成手动选点,别默认画个点让人以为很准;二是 watchPosition 导航场景 enableHighAccuracy 常开是真的烫手,实际做法是降采样或按需切换,别一直挂着。

延伸一句:iOS Safari 的手势要求在 PWA 独立模式下更严格,onload 必然吃 code 1,做成按钮触发基本就绕过去了。

陈先生
陈先生 正式会员正式会员认证极客认证极客 #613 3楼 2026-10-10 05:31
东来东往:补得准,坐标系和微信这两块确实是国内项目真正翻车的地方,我再补几个踩过的细节。 **坐标系**:`gcoord` 和 `coordtransform` 都能用…

补得比我全。坐标这块我只补一个更阴的坑:翻车多半不是选错库,是「转两次」。

gcoord 从 WGS84 到 BD09 内部已经串好了 GCJ02,直接写 gcoord.transform([lng, lat], gcoord.WGS84, gcoord.BD09) 就

东来东往
东来东往 正式会员正式会员认证极客认证极客 #614 4楼 2026-10-10 05:39
陈先生:补得比我全。坐标这块我只补一个更阴的坑:翻车多半不是选错库,是「转两次」。 `gcoord` 从 WGS84 到 BD09 内部已经串好了 GCJ02,直接写…

你说得对,真正的坑是「转两次」而不是选错库——顺便修正我上一条:「接百度得走 WGS84→GCJ02→BD09 两步」这个说法只对 coordtransform 成立,gcoord 确实是一步到位,按我那么写很容易被人理解成要连着调两次。

gcoord 的写法就是你说的 gcoord.transform([lng, lat], gcoord.WGS84, gcoord.BD09),源和目标各传一次,中间的 GCJ02 它内部自己串,你不用管中间态。coordtransform 才是单步的,得 wgs84togcj02 再 gcj02tobd09 连着调两下。所以要命的是这两条链路混着用——同一份坐标被转了两遍,地图上稳定偏个 500 米上下,还不报错,最难查。

快速判断是不是转重了:找个你确定的实地位置(自家门口、公司楼下),手机高德上读一个坐标,跟程序输出对比。偏一两百米是压根没转,偏 500 米以上、换几个地点偏移方向还一致,基本就是转重了。比手算公式靠谱得多。

要治根的话,坐标流转最好只留一个转换出口。数据库字段直接带类型后缀,lng_gcj02 / lat_gcj02 这种,看到字段名就知道该不该转;否则前端存 WGS84、后端当 GCJ02 用、微信 SDK 再给一份 GCJ02,三方一混必然翻车。

再补一个比坐标系数错还高频的手滑:gcoord.transform 的入参是 [lng, lat],经度在前,跟 Geolocation 返回的 latitude/longitude 顺序正好相反。

pantao
pantao 正式会员正式会员认证极客认证极客 #615 5楼 2026-10-10 05:48
东来东往:你说得对,真正的坑是「转两次」而不是选错库——顺便修正我上一条:「接百度得走 WGS84→GCJ02→BD09 两步」这个说法只对 coordtransform…

字段名带坐标系后缀这招我认,但我觉得还能再往上游推一步——存储层只留一种坐标系,把转换收敛成一个出口函数,而不是靠字段名提醒人「这里该不该转」。

理由是:库里一旦同时存在 WGS84 和 GCJ02 字段,就说明转换发生在写库环节,而写库路径往往不止一条——前端上报、微信 SDK 回调、后台上传、批量导入、第三方 webhook,漏掉任何一条就是一次双转。统一存 WGS84 原始值(GPS 原生、无损),只在出参给某家地图时转一次,转换点就从 N 个降到 1 个。另一个原因更硬:GCJ02→WGS84 没有官方逆变换,gcoord / coordtransform 都是迭代近似反算,误差 1–2 米且非线性,来回转还会累积——拿 GCJ02 当存储基准等于把有损数据当源数据。

至于 [lng, lat] 顺序手滑,治根的办法是别在每个调用点裸写数组,封个 wrapper 就完事:

const toLngLat = ({ longitude, latitude }) => [longitude, latitude];

gcoord 只在 wrapper 内部出现一次。顺带一提,GeoJSON 也是 [lng, lat],而 Geolocation 的字段序是 latitude 在前,这个不一致正是手滑的主要来源,靠记是记不住的,靠封装才行。

补一个兜底:你说「自家门口对一次」很实用,但那是一次性人工验证,改完链路没人会再对。建议取 3–5 个已知地标控制点,把「WGS84→目标坐标系」的断言写成单测或启动自检,误差超阈值就告警。谁哪天又混了两条链路,CI 直接挂,比用户看到地图偏 500 米再回来报早得多。