网页定位踩坑记
之前我写过一个新加坡天气云图的小页面,照着官方网站糊的,补上了官方没有的”你所在的位置”,还能看历史。自己天天在用,挺顺手。
直到前两天,我在 Unihertz Titan 2 的 Chrome 上打开它,面板底部赫然一行红字:
|
1 |
获取位置信息失败,请参考这里。 |
奇怪的是,同一部手机上 Google Maps 定位好好的,Chrome 的位置权限我也手动给了,Mac 和 iPhone 上的 Safari 打开同一个页面也都正常。我意识到,不妙,又来活儿了~

这篇就把这次排查的结果记下来:一个把定位”悄悄”拦掉的响应头,一个 nginx 里关于 add_header 的老坑,还有后来发现的定位慢的问题。值得写一篇,分享给有需要的朋友。
权限明明给了,为什么还是失败?
先排除手机本身。系统定位是开的,Chrome 的定位权限也确实是已授予。连上电脑用 adb 确认:
|
1 2 |
adb shell dumpsys package com.android.chrome | grep ACCESS_FINE_LOCATION android.permission.ACCESS_FINE_LOCATION: granted=true, flags=[ ... ] |
那问题就在页面这边。看了一眼页面代码,取位置失败的回调里只有一句 console.log 加一个红字提示,没有任何有用的信息。于是我换个思路,直接看服务器给了什么响应头:
|
1 2 |
curl -sI https://luy.li/data/w/ | grep -i permissions permissions-policy: camera=(), microphone=(), geolocation=(), payment=() |
就是它!geolocation=() 的意思是:本站任何页面都不许使用定位。这是 Permissions-Policy 响应头,浏览器会在权限检查之前就把请求拒掉,所以不管你在浏览器里给了多少权限,页面里的 navigator.geolocation 都只会走到错误回调,也不会弹授权框。
那为什么 Safari 正常?据我所知,WebKit 对这个响应头基本不执行(它对 iframe 的 allow 属性支持得比较好),所以头里写了禁止,Safari 照样能弹框、能定位。Google Maps 是原生 App,更不走网页策略,当然更没事。
说起来,这个头还真不是什么神秘的东西,是我自己加的。九月底那阵子,博客的安全插件一直提示我不安全,扫描器里六个安全头全是 Not Found,我就照着博客安全头踩坑记里写的,在 nginx 里一口气补齐了。其中 Permissions-Policy 这一行的想法特别朴素:摄像头、麦克风、定位这些用不上的浏览器能力全关了。但没想到我之前偷懒挂在博客域名下的这个天气页面受到了影响。哈哈!
只放开一个路径:nginx 的 add_header 继承坑
当然不能整站都改成允许定位,我只想放开 /data/w/。第一反应是给它单独写个 location,再加一行 add_header。但是 nginx 有个很经典的坑(上面那篇里我也踩过一次):某个 location 里只要写了自己的 add_header,就不会再继承外层(server 级)的所有 add_header。这意味着我得把 HSTS、CSP 那一堆安全头在新 location 里全抄一遍,以后改一处要同步两处,迟早漏。
更省事的办法是用 map,让同一个头的值随路径变化。add_header 只保留 server 级那一行,其他什么都不用动:
|
1 2 3 4 5 6 7 8 9 |
map $uri $permissions_policy { default "camera=(), microphone=(), geolocation=(), payment=()"; ~^/data/w/ "camera=(), microphone=(), geolocation=(self), payment=()"; } server { ... add_header Permissions-Policy $permissions_policy always; } |
map 必须放在 server 块外面、http 上下文里(我的配置文件第一个就是 server 块,说明它是被 http 块 include 进来的,放文件开头就行)。reload 之后验证一下,只有目标路径放开了:
|
1 2 3 4 5 |
curl -sI https://luy.li/data/w/ | grep -i permissions permissions-policy: camera=(), microphone=(), geolocation=(self), payment=() curl -sI https://luy.li/ | grep -i permissions permissions-policy: camera=(), microphone=(), geolocation=(), payment=() |
手机上刷新,Chrome 弹出授权框,点允许,蓝点出现了~
蓝点出现得好慢?
问题解决了吗?没有,因为我发现蓝点要过好几秒才出来。我一开始的判断很干脆:页面里写的是 enableHighAccuracy: true,这会强制走 GPS,室内搜星慢,改成 false 用 Wi-Fi / 基站定位应该秒出。
但是光猜不行,得量。Chrome 在安卓上可以通过 adb 转发出 DevTools 端口:
|
1 2 |
adb forward tcp:9223 localabstract:chrome_devtools_remote curl -s localhost:9223/json # 列出当前所有标签页 |
然后通过 CDP 的 Page.addScriptToEvaluateOnNewDocument 在页面脚本之前注入一小段代码,包装 navigator.geolocation 的回调,记录每次定位什么时候返回、蓝点什么时候显示。这样量出来的是真实耗时,不靠肉眼。
先是普通刷新三次,蓝点 0.86 到 1.36 秒出现,其实不算慢,也没复现我肉眼看到的”好几秒”。后来我直接用 screenrecord 录下自己手动操作的过程,再用 ffmpeg 拆成每秒 10 帧、逐帧检测蓝点的颜色,终于抓到了:连续点三次刷新,蓝点出现的时间是 1.4 秒、2.1 秒、3.0 秒,一次比一次慢。
我用脚本模拟同样的快速刷新(间隔约 4 秒),也复现了:1.3 → 1.6 → 2.5 → 2.8 秒。间隔拉开到 8 秒以上就没有这个现象。开不开录屏结果都一样。
那是不是 enableHighAccuracy 的问题?我把三种写法放在同样的条件下对比(快速刷新 5 次,每组之间等 25 秒):
|
1 2 3 |
现行(低精度+GPS监听) 0.67s 2.14s 未出现 0.55s 2.04s 仅低精度 1.27s 2.74s 未出现 1.37s 2.86s 原版(仅高精度) 1.34s 2.83s 未出现 1.46s 2.98s |
三种写法的波动规律完全一致。结论很清楚:慢不是页面参数造成的,是安卓系统和 Chrome 的定位层在短时间内连续请求时本身就时快时慢,页面这边改什么参数都没用。我最开始那个”改成低精度就秒出”的判断,被数据直接打脸了。。。
页面层能做的:缓存一个短时间的位置
系统定位快慢我控制不了,那就让蓝点不依赖它。思路很简单:把上一次拿到的位置连同时间戳存进 localStorage,打开页面先拿缓存画蓝点,同时照常发起真正的定位,成功后再更新。
有两个细节我是想清楚了才加的:
1. 缓存只认 3 分钟内的。位置过期了就不显示,免得定位一直失败的时候,蓝点永远停在旧地方。
2. 只有真正定位成功才更新缓存,失败了缓存不动。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
const CACHE_KEY = 'weather.lastPosition'; const CACHE_TTL = 3 * 60 * 1000; try { const cached = JSON.parse(localStorage.getItem(CACHE_KEY)); if (cached && Date.now() - cached.time <= CACHE_TTL) { this.userPosition = { lat: cached.lat, lng: cached.lng }; this.updateUserLocationPosition(); } } catch (e) { /* 缓存不可用或已损坏,忽略 */ } navigator.geolocation.getCurrentPosition( (position) => { this.userPosition = { lat: position.coords.latitude, lng: position.coords.longitude }; this.updateUserLocationPosition(); try { localStorage.setItem(CACHE_KEY, JSON.stringify({ ...this.userPosition, time: Date.now() })); } catch (e) {} }, (error) => { // 没有可显示的缓存位置时才提示失败 if (!this.userPosition) this.locationErrorMessage.style.display = 'block'; }, { enableHighAccuracy: true, timeout: 10000, maximumAge: 300000 } ); |
我通过 DevTools 把线上页面临时换成本地新文件,在手机上连续刷新四次:蓝点出现的时间都是 0.01 秒。当然,缓存是空的时候(第一次打开,或者隔了三分钟以上),还是得老老实实等系统定位,这点没法改。
总结一下几个坑
1. Permissions-Policy 拦截在权限弹窗之前。浏览器里权限给了也没用,页面也不会弹框,只会静悄悄地走错误回调。遇到”权限明明给了却定位失败”,先 curl -I 看一眼响应头,比反复折腾手机设置快得多。
2. Safari 正常不代表没问题。这个响应头在不同浏览器上的执行力度不一样,测定位功能,Chrome 一定要测。
3. 测速要把刷新间隔拉开。连续快速刷新时,系统定位会越来越慢,拿这个当基线会得出错误结论。
4. 别光靠猜。“高精度会强制 GPS 所以慢”听起来天经地义,实际一量就不是那么回事。
全文完。
发表回复