
做网页开发这几年我遇到最多的定位需求并不是什么复杂地图应用反而是我人在哪打卡签到附近门店这类看起来简单、实际坑不少的小功能。HTML5 的地理定位 API 在浏览器里已经是很成熟的能力了十行代码就能拿到经纬度可真要拿到生产环境权限、HTTPS、精度这些事一个都绕不开。这篇文章我把从原理到实操的完整链路写清楚包括可以直接抄的 Demo 代码、几个踩过的坑和排查思路正在做网页设计作业或者想给页面加定位功能的朋友可以参考。1. HTML5地理定位到底是什么能替我们做什么1.1 需要把它放在哪里网页上的定位入口HTML5 地理定位指的是浏览器暴露给网页开发者的navigator.geolocation这一组接口。它不依赖任何第三方插件也不需要你引入额外的 SDK只要用户用的是现代浏览器并且同意授权你的网页就能拿到当前设备的大致经纬度坐标。我经常和学生或刚入门的前端同事说这个 API 是 HTML5 里投入产出比相当高的一块内容。你不用懂 GPS 怎么接收卫星信号也不用管手机上那么多传感器怎么协作浏览器帮你把底层差异都封装好了。你要做的只有三件事询问用户是否同意、调用接口、处理回调结果。从演示价值上看定位功能拿来做网页设计作业也很合适——代码量适中效果直观还能联动地图比单纯做一个静态页面有说服力得多。这里要明确一个边界HTML5 地理定位拿回来的是数字坐标不是一句我在上海市某某路。至于把坐标翻译成人话地址那是逆地理编码的活儿一般交给地图平台提供的服务去完成。后面我会给出对应的实现思路。1.2 典型应用场景从附近门店到打卡签到在实际项目里地理定位最常见的用途大概是这几类本地化服务首页自动定位当前城市展示对应天气、新闻、生活服务。附近位置检索找附近的门店、加油站、餐厅先拿用户位置再把坐标连同搜索半径发给后端。打卡与考勤判断用户是否进入某个地理范围签到时通常还会配合时间限制。运动轨迹记录连续监听位置变化记录跑步、骑行的路径和里程。物流与配送展示配送员当前位置或者配送员在 App 内上报位置。其中打卡签到是我觉得最适合用来做完整 Demo 的场景。它足够典型需要一次性拿坐标、需要精度判断、需要处理授权失败、还要考虑边界情况。做完这个场景基本上大部分定位需求都能触类旁通。第 4 章我会给一个能直接运行的签到页面。1.3 先给网页设计作业的同学画个重点如果这篇博文是你为了网页设计作业搜到的先记三个重点定位功能必须配合HTTPS 环境或 localhost才能正常调用第 5 章会展开讲。页面里一定要有授权提示和错误处理老师或者评委很可能专门测试你点了拒绝会怎样。只展示经纬度数字不太够把坐标画到地图上、或者配合距离计算页面效果会提升一个档次。这三个重点正好对应这篇文章的主要章节。我们先把原理走一遍再上手写代码。2. 定位背后的四大情报来源为什么精度时好时坏2.1 GPS、Wi-Fi、基站、IP四种定位来源谁靠谱用 HTML5 拿位置时你永远不知道浏览器背后到底用了什么定位源但它通常会在 GPS、Wi-Fi、基站和 IP 这几种来源之间做取舍。GPS通过卫星信号计算位置在户外开阔地精度最高能达到几米级但它启动慢室内基本失效功耗也大。Wi-Fi 定位设备扫描周围的 Wi-Fi 信号特征哪怕你没连接上只是能看到然后去和位置数据库比对。城市里很好用室内也能做到几十米的精度而且响应快。基站定位手机连接着运营商基站通过多个基站的信号强度做三角估算。精度大概在几百米到几公里属于粗略但永远可用的保底方案。IP 定位根据网络出口的 IP 归属地估算通常只能精确到城市级别桌面浏览器上没有其他定位源时可能用到它。你可以把 GPS 想象成站在操场上根据太阳方位判断自己在哪Wi-Fi 是靠认周围的店铺招牌猜自己在哪条街基站是通过地标建筑大概判断你在一环还是三环IP 则是看你的收货邮编大概猜你在哪个区。浏览器做的工作就是把这几份情报综合起来给一个它认为最合理的结果。2.2 accuracy字段才是真话别只盯着经纬度拿到定位结果后新手往往只关注position.coords.latitude和longitude但合格的开发者一定会先看accuracy。accuracy代表这次定位的精度半径单位是米。比如accuracy: 30表示真实位置大概在以返回坐标为中心、半径 30 米的圆内。这个数字越小越好。不同定位源返回的 accuracy 差异非常大GPS 冷启动后可能是 5 到 20 米Wi-Fi 可能是 20 到 100 米基站可能是 500 到 3000 米。我见过一个项目需求是判断用户是否在公司 300 米范围内打卡。第一次测试时 accuracy 是 12 米判断非常准后来换到信号差的室内accuracy 变成了 1500 米业务逻辑直接误判。所以任何用到坐标做范围判断的场景都要把accuracy纳入判断条件。比如签到时不光要计算距离还要确认本次定位精度是否足够精度太差就直接提示用户重新定位。2.3 enableHighAccuracy不是上高精度挂getCurrentPosition的第三个参数可以传{ enableHighAccuracy: true }很多人误以为设置这个值就能提升精度。其实它只是告诉浏览器在功耗和时间允许的情况下优先使用更精确的定位源。要不要给你高精度结果由浏览器根据当前环境决定。在支持 GPS 的设备上enableHighAccuracy: true会让定位等待更久、耗电更高换来更精确的结果。在普通笔记本上它可能几乎没影响因为设备本身没有 GPS 模块。在手机浏览器上开启后有时会在定位前强迫系统启动 GPS定位时间可能从 1 秒变成 10 秒。结论是按需开启。签到、轨迹这类对精度有要求的场景可以开只是获取所在城市、做展示性定位开不开意义不大。3. 三个核心方法加一个Position对象把API吃透3.1 getCurrentPosition一次性定位的用法最常用的方法就是getCurrentPosition它的签名是navigator.geolocation.getCurrentPosition(successCallback, errorCallback, options)第一个回调在成功拿到位置时触发收到的参数是Position对象第二个回调在失败时触发收到的是GeolocationPositionError对象第三个参数是配置项。注意它是回调风格的不是 Promise。如果你习惯用async/await可以自己包一层function getPosition(options) { return new Promise((resolve, reject) { if (!navigator.geolocation) { reject(new Error(当前浏览器不支持地理定位)); return; } navigator.geolocation.getCurrentPosition(resolve, reject, options); }); } // 使用 try { const position await getPosition({ enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }); const { latitude, longitude, accuracy } position.coords; console.log(我在 ${latitude}, ${longitude}误差约 ${accuracy} 米); } catch (err) { console.log(定位失败, err.message); }这个封装我在实际项目里用了很久业务代码会清爽很多。要提醒的是getCurrentPosition只会请求一次位置对实时追踪无能为力那是watchPosition的活儿。3.2 watchPosition clearWatch持续追踪和收手如果要做跑步轨迹、车辆实时位置这类持续更新的功能需要用watchPosition。它和getCurrentPosition的传参完全一样区别在于会持续监听位置变化每次有新位置都会调用成功回调。而且它返回一个数字类型的 id想停掉时把它传给clearWatch即可const watchId navigator.geolocation.watchPosition( (position) { const { latitude, longitude } position.coords; drawTrack(latitude, longitude); }, (error) { console.error(追踪失败, error.message); }, { enableHighAccuracy: true, maximumAge: 0, timeout: 8000 } ); // 不需要时及时停止 navigator.geolocation.clearWatch(watchId);这里强调及时停止不是废话。很多页面切到后台了还在继续监听这会一直消耗系统定位资源和电量。我自己的习惯是轨迹记录页在pagehide或visibilitychange事件里调用clearWatch从根上避免页面关了定位还在跑的情况。3.3 Position对象里都有什么数据成功回调拿到的Position对象包含两个核心字段timestamp这次定位结果产生的时间戳。coords更具体的坐标信息里面可用的字段如下表所示字段含义说明latitude纬度范围 -90 到 90正数表示北纬longitude经度范围 -180 到 180正数表示东经accuracy精度半径单位米务必关注altitude海拔部分设备没有可能是 nullaltitudeAccuracy海拔精度同样可能是 nullheading航向相对正北方向的角度0 到 360静止时为 NaNspeed速度单位米/秒静止时为 null 或 0实际开发时latitude、longitude、accuracy这三个字段用到的最多。heading和speed在运动轨迹类应用里才有意义普通业务基本不用管。拿到这些字段后前端只负责展示和简单计算更重的逻辑通常交给后端。3.4 PositionOptions三个参数怎么配不出事第三个参数options里常用配置就三个但很多人直接照抄网上代码从不思考含义参数含义建议值说明enableHighAccuracy是否启用高精度模式按场景开签到、轨迹开展示城市不开timeout允许等待的最长时间毫秒5000 ~ 10000太短会导致 GPS 冷启动时频繁超时maximumAge允许使用多久以内的缓存位置毫秒0 或合理值0 表示必须现场定位不敏感场景可设 30000timeout是容易踩坑的地方。定位不是瞬间完成的尤其是手机端首次请求用户可能要从 WiFi 信息扫描转到 GPS 冷启动这个过程 5 秒很正常。你把 timeout 设成 1000大概率全军覆没。maximumAge则要注意你的业务能不能容忍旧位置求所在城市缓存 30 秒没问题做签到打卡建议设 0否则用户从 A 地跑到 B 地中间缓存没过期签到到的还是 A 地坐标。4. 从0到1手写一个带地图的定位Demo4.1 三步跑通最小定位代码第一步先做一个最纯粹的定位页面。新建一个index.html输入下面的代码!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleHTML5 地理定位演示/title /head body h2我的位置/h2 button idlocateBtn开始定位/button pre idresult/pre script const btn document.getElementById(locateBtn); const result document.getElementById(result); btn.addEventListener(click, () { if (!navigator.geolocation) { result.textContent 当前浏览器不支持地理定位; return; } navigator.geolocation.getCurrentPosition( (position) { const { latitude, longitude, accuracy } position.coords; result.textContent 纬度${latitude}\n经度${longitude}\n精度约 ${accuracy} 米; }, (error) { result.textContent 定位失败${error.message}; }, { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 } ); }); /script /body /html在本地用 Live Server 或者python3 -m http.server起服务后通过localhost打开点击按钮浏览器会弹出授权请求。同意后就能看到坐标。如果你直接在桌面双击用file://协议打开部分浏览器也会放行但我建议还是用本地服务器省得后面踩协议相关的坑。4.2 用Leaflet把坐标画到地图上拿到坐标干巴巴地显示数字实在没什么视觉冲击力。要画到地图上可以接入地图平台或者轻量地图库。对学生和快速原型我比较推荐Leaflet——开源、轻量、不需要申请密钥一个 CDN 链接就能用。高德和百度地图功能更全但需要注册开发者账号申请 Key演示型项目没必要卡在这一步。把上面的代码扩展一下引入 Leaflet!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleHTML5 地理定位演示/title link relstylesheet hrefhttps://unpkg.com/leaflet1.9.4/dist/leaflet.css / style #map { height: 400px; width: 100%; } #result { margin-top: 12px; white-space: pre-line; } /style /head body h2我的位置/h2 button idlocateBtn开始定位/button div idmap/div pre idresult/pre script srchttps://unpkg.com/leaflet1.9.4/dist/leaflet.js/script script const btn document.getElementById(locateBtn); const result document.getElementById(result); let map; // 初始化地图默认中心先放在中国范围 map L.map(map).setView([35.0, 105.0], 4); L.tileLayer(https://tile.openstreetmap.org/{z}/{x}/{y}.png, { attribution: © OpenStreetMap }).addTo(map); btn.addEventListener(click, () { if (!navigator.geolocation) { result.textContent 当前浏览器不支持地理定位; return; } navigator.geolocation.getCurrentPosition( (position) { const { latitude, longitude, accuracy } position.coords; result.textContent 纬度${latitude}\n经度${longitude}\n精度约 ${accuracy} 米; // 把视图重置到用户位置 map.setView([latitude, longitude], 15); // 加一个标记点 L.marker([latitude, longitude]).addTo(map) .bindPopup(我在附近误差约 ${accuracy} 米) .openPopup(); // 画一个精度范围圆方便直观理解 accuracy L.circle([latitude, longitude], { radius: accuracy, color: #3388ff, fillColor: #3388ff, fillOpacity: 0.15 }).addTo(map); }, (error) { result.textContent 定位失败${error.message}; }, { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 } ); }); /script /body /html这里有个很直观的展示小技巧把accuracy作为半径在地图上画一个圆。你会看到在 WiFi 环境好时这个圆很小在 GPS 冷启动或者信号不好时圆会非常大。把精度可视化出来比单纯显示数字更容易让人理解定位误差这件事。4.3 错误回调授权被拒、拿不到位置、超时怎么处理错误处理是定位功能最考验细节的部分。错误回调里收到的error.code有三种常见取值对应的处理逻辑各不相同function handleLocationError(error) { switch (error.code) { case error.PERMISSION_DENIED: // 用户点了不允许或者此前拒绝过 result.textContent 你拒绝了定位权限。请点击地址栏左侧的权限图标把位置权限改为允许后重试。; break; case error.POSITION_UNAVAILABLE: // 位置服务可用但拿不到位置 result.textContent 暂时获取不到你的位置请检查网络或者在室外开阔一点的地方再试。; break; case error.TIMEOUT: // 超过 timeout 时间 result.textContent 定位超时了请点击按钮重新尝试。; break; default: result.textContent 定位失败未知错误; } }注意error.code error.PERMISSION_DENIED这种写法在多数浏览器的GeolocationPositionError对象上是可用的对应数值其实就是 1、2、3。有些团队规范要求直接用数值常量比如error.code 1也没问题关键是注释要写清楚。还有一个很常见的坑用户在第一次弹出的授权框里点了拒绝之后你再次调用接口浏览器记住的是域名的权限状态不会重新弹窗而是直接走PERMISSION_DENIED错误。所以前端一定要给用户明确的引导告诉他去哪里改权限。4.4 加一个签到距离判断做成能演示的Demo把定位跑通之后我们可以加一个是否在签到范围内的判断让它从技术演示变成业务 Demo。思路很简单假设公司门店坐标为固定值用 Haversine 公式计算用户坐标和门店坐标的球面距离距离小于设定阈值比如 300 米就提示签到成功否则提示不在范围内。代码片段如下function calcDistance(lat1, lng1, lat2, lng2) { const R 6371000; // 地球平均半径单位米 const toRad (deg) (deg * Math.PI) / 180; const dLat toRad(lat2 - lat1); const dLng toRad(lng2 - lng1); const a Math.sin(dLat / 2) * Math.sin(dLat / 2) Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2); return 2 * R * Math.asin(Math.sqrt(a)); } // 假设门店坐标是 (30.0000, 120.0000)阈值 300 米 const officeLat 30.0000; const officeLng 120.0000; const range 300; // 拿到用户坐标后 const distance calcDistance(latitude, longitude, officeLat, officeLng); if (distance range) { result.textContent 签到成功距离门店约 ${Math.round(distance)} 米; } else { result.textContent 离门店还有 ${Math.round(distance)} 米不在签到范围; }这个 Demo 在课堂上演示效果很好点一下按钮地图自动飞到你的位置同时给出签到结果一整套业务闭环。如果还想更进一步可以用watchPosition模拟一个移动轨迹在地图上连点成线做路径记录页面。5. HTTPS、权限、浏览器差异这几个坑我替你们踩过了5.1 没有HTTPS一切正常但就是拿不到位置这是我第一次在项目里集成定位时被卡得最久的问题。当时页面部署在测试服务器上用http://192.168.x.x访问点击定位按钮浏览器没有任何报错但回调一直不触发。后来打开控制台才看到一行警告Geolocation API has been blocked in this origin because it is not secure.原因很直接从 Chrome 50 开始地理位置接口只在安全上下文中可用。所谓安全上下文最典型的就是https://域名和http://localhost。也就是说你用localhost开发没事但一旦换成本机 IP 或者随意一台远程服务器只要协议是http://定位就会静默失败。排查思路给大家还原一下先看页面是不是 HTTPS再看域名是不是 localhost最后看浏览器版本。大部分定位没反应的问题根因都在域名协议上。开发阶段如果要在手机上真机测试可以先把页面托管到支持 HTTPS 的静态平台或者用本地开发服务器加证书处理别在http://环境里死磕。5.2 用户拒绝授权之后怎么引导恢复浏览器把用户的授权决定记住了这是设计如此但给用户带来的问题是误点了拒绝之后再返回页面怎么都触发不了授权弹窗。前端能做的事情有两件。第一在检测到PERMISSION_DENIED时明确提示用户去修改浏览器权限设置。Chrome 的操作路径是点击地址栏左侧的权限图标在网站设置里重新允许位置访问。Safari、Edge 和 Firefox 的位置略有不同但在各自的菜单里都能找到网站权限或位置服务入口。第二在支持Permissions API的浏览器里可以主动查询权限状态提前判断授权结果从而更早地提示用户。参考写法如下if (navigator.permissions navigator.permissions.query) { navigator.permissions.query({ name: geolocation }).then((status) { if (status.state denied) { showTip(本站的位置权限已被拒绝请到浏览器设置中修改); } else if (status.state granted) { // 已授权可以直接调用定位 } }).catch(() { // 不支持权限查询时忽略直接走到定位调用逻辑 }); }这个 API 的兼容性不算完美Safari 的部分版本支持度一般所以我把整段逻辑放在if判断里失败也不影响原有定位流程。5.3 手机和电脑上的体验差异测试时别自欺欺人同一个定位页面在电脑上和手机上简直是两套体验。我做过一个真实项目PC 端测试精度一直很好结果用户反馈说在手机上定位不准、偶尔失败一查才发现问题集中在设备差异上PC 通常没有 GPS 模块定位依赖 WiFi 和 IP。在家里、办公室开着 WiFi精度能到几十米在没 WiFi 的户外精度可能掉到公里级。手机有 GPS 但受环境影响大。室内、地下室、隧道定位源会自动切回基站或 WiFi精度下降非常明显。iOS 和 Android 的系统级授权弹窗出现在浏览器弹窗之前。部分手机甚至默认限制网页获取精确定位需要用户在系统设置里把精确定位选项打开。iOS 的 Safari 对后台定位限制非常严格。watchPosition在页面进入后台后基本停止回调切回前台才会恢复。所以测试定位功能一定要在真实手机、多种网络环境下跑一遍别只看电脑浏览器的结果。还有个小经验把精度信息直接显示在调试页面上测试时一旦发现精度异常能立刻定位是环境问题还是逻辑问题。5.4 持续监听的耗电问题别让页面在后台偷跑watchPosition开着的时候浏览器会持续请求定位结果这对耗电影响极大。尤其是开启enableHighAccuracy: true的手机GPS 模块长时间工作电量掉得飞快。我处理这类问题有几个习惯。第一不需要实时监听时就调用clearWatch比如用户完成轨迹记录、或者页面切到后台。第二监听期间设置合理的maximumAge避免相邻两次回调之间重复做高成本定位。第三在visibilitychange事件里做处理页面可见时才监听页面隐藏时暂停document.addEventListener(visibilitychange, () { if (document.hidden) { if (watchId ! null) { navigator.geolocation.clearWatch(watchId); watchId null; } } else if (needTrack) { startWatch(); } });这样能省下大量无意义的定位消耗而且对业务影响很小——毕竟页面都看不见了还在那更新轨迹也没什么意义。6. 隐私合规和产品化不能只管功能不管规矩6.1 位置信息是敏感个人信息页面要做告知-同意最后这部分想聊点功能之外的事但我觉得比功能本身更重要。位置信息属于敏感个人信息现在正规产品收集位置前都要明确告诉用户为什么收集、收集什么、用来做什么。国家对于这类个人信息的收集也提出了告知-同意的明确要求开发者照做既是合规需要也能减少后续麻烦。落地到页面上建议在真正调用定位接口之前先用页面上的一段说明文字甚至弹窗告知用户定位用途。比如为了帮你找到最近的提货点需要获取你的位置位置信息仅用于该次查询。把话说清楚之后用户可以自行决定是否授权而不是打开页面就被浏览器弹窗偷袭。这样看起来多了一步实际上用户的信任度和授权率都会更高。6.2 最小必要不用的位置数据别拿、拿了别多存最小必要是个很实用的原则。你的功能只需要城市级别的位置就不要开启高精度定位只需要一次定位结果就不要watchPosition一直监听只需要在页面停留期间用位置页面关闭后就不该有地方继续保存或上传位置信息。具体操作上我见过不少页面为了展示一个附件门店的列表就持续监听用户位置整个页面停留期间都在向服务器上报坐标。这在业务上完全没必要还非常招用户反感。正确的做法是一次性获取位置请求完附近的门店后立刻在成功回调里把数据展示出来不再持有坐标。坐标若需上传给后端也应当通过 HTTPS 传输并且在后端明确好保留周期避免无限期存储。6.3 与后端对接时的技术安全建议如果你的定位页面涉及上报坐标到服务器几个技术安全建议值得记住坐标上传必须走 HTTPS避免在网络传输中被截获。后端不要为了以后可能有用而留存全部精确坐标。不需要时可以直接丢弃或者只保留到城市粒度。高精度坐标属于敏感数据存得多意味着风险大。精确的地址不一定要前后端之间传来传去。很多场景可以把用户坐标 业务范围半径发给后端由后端判断而不是把用户详细地址暴露给服务端。如果在页面上集成第三方地图 SDK要注意你向外发送的数据是什么。地图平台拿到的坐标属于第三方选择可信的服务商、并且明确接口用途也是合规的一部分。把功能做出来只是第一步让它经得起隐私和合规的审视才算真正能上线的东西。最后分享一点个人经验。我现在做任何带定位的页面都会先设计一个定位开关让用户随时可以停用定位同时页面里放一句话说明定位用途。很多用户不是不愿意给权限而是怕被悄悄跟踪。你把逻辑摊开了讲明白授权率反而是实打实上去的。HTML5 地理定位的技术本身不难真正拉开差距的是背后的权限管理、误差处理和产品细节。希望这篇文章能让你少走几次弯路。