Skip to content

用户拒绝定位权限怎么办?用 JavaScript 按 IP 归属地做城市级兜底 ​

做同城功能的开发八成遇到过这个场景:功能上线第二天,用户反馈「我人在上海,列表给我推的全是北京」。查了下埋点,三成用户压根没授权定位——有人嫌弹窗烦,有人担心隐私,iOS 上拒绝过一次之后,系统连再弹的机会都不给。GPS 在室内和地铁里还经常漂移,定位模块直接成了整个产品的短板。

定位拿不到,同城推荐、默认城市、本地化内容全跟着瘫。好在还有条兜底路:IP 归属地。浏览器和 App 拿不到精确坐标没关系,通过 IP 至少能确定用户所在的省和市——城市级精度,做同城排序、展示默认城市绰绰有余。这篇文章用 JavaScript 把「定位权限 → IP 兜底」的降级链写完整,前端一套代码 + 后端一个代理接口,半小时落地。

一、为什么要留 IP 这条后路 ​

先分清两类数据的能力边界:

  • GPS/基站定位:能到街道级,但要有权限;被拒、超时、室内漂移都是常态;
  • IP 归属地:只能到城市级(精度是城市中心点,不是街道),但永远拿得到——只要用户能联网,就能查出他所在的城市。

同城匹配、首页默认城市、按地区给默认语言和币种,这些场景要的就是「城市」两个字,IP 归属地完全够用。真正需要街道级的导航、找附近门店,才必须依赖 GPS。所以正确的产品逻辑不是二选一,而是降级链:能拿精确定位就用精确定位,拿不到就降级到 IP,功能照常跑,只是精度降一档,同时 UI 上告诉用户「当前按 IP 显示城市级位置」。

另外 App Store 和各大安卓市场对权限审核越来越严,「不给定位也能用」本身就是加分项——这条兜底链直接帮你过了审核里「权限必要性」那一关。

二、方案设计 ​

  1. 前端三步走:先试 navigator.geolocation.getCurrentPosition(3 秒超时)→ 失败或拒绝 → 请求自己的同源接口 /api/geo → 拿到城市后写 localStorage 缓存(24 小时);
  2. 后端代理:IP9 的免费接口 https://ip9.com.cn/get 不带 CORS 头,浏览器直连会跨域失败,所以由自己的 Node 服务转发一次,顺便做缓存和限速;
  3. 返回结构统一:不管走 GPS 还是 IP,前端拿到的都是 { prov, city, country } 这样的结构,业务层无感切换。

注意一个容易踩的坑:IP 归属地接口返回的是城市中心点经纬度,别拿它去做导航级别的定位,文档里写清楚「城市级」,UI 上标注「按 IP 估算」,既诚实又合规。

三、JavaScript 实现 ​

前端(浏览器):

javascript
// geo.js —— 城市定位降级链:GPS → IP 归属地
const CACHE_KEY = 'city_fallback';
const CACHE_TTL = 24 * 3600 * 1000;   // 24 小时

async function getCity() {
  // 1. 命中缓存直接返回
  try {
    const cached = JSON.parse(localStorage.getItem(CACHE_KEY));
    if (cached && Date.now() - cached.at < CACHE_TTL) return cached.city;
  } catch (e) { /* 缓存损坏忽略 */ }

  // 2. 优先浏览器定位(3 秒超时,失败/拒绝都不阻塞)
  try {
    const pos = await new Promise((resolve, reject) => {
      navigator.geolocation.getCurrentPosition(resolve, reject, { timeout: 3000 });
    });
    // 拿到经纬度后,逆地理转城市走你自己的服务(高德/腾讯 WebService API)
    // 这里省略具体实现,只返回标记
    return { source: 'gps', lng: pos.coords.longitude, lat: pos.coords.latitude };
  } catch (e) {
    // 用户拒绝 / 超时 / 不支持 → 走 IP 兜底
  }

  // 3. IP 归属地兜底:请求自己的同源代理(避免跨域)
  const r = await fetch('/api/geo');
  if (!r.ok) throw new Error('geo fallback failed');
  const d = (await r.json()).data;          // ip9 返回结构:data.country / prov / city
  const city = {
    source: 'ip',
    country: d.country,
    prov: d.prov,
    city: d.city,
  };
  try {
    localStorage.setItem(CACHE_KEY, JSON.stringify({ at: Date.now(), city }));
  } catch (e) { /* 隐私模式下 localStorage 可能不可用 */ }
  return city;
}

// 使用:同城列表默认排序
async function initHome() {
  const city = await getCity();
  if (city.source === 'ip') {
    showToast('已按 IP 显示城市级位置:' + city.city);
  }
  // 拿 city.city 去请求同城列表接口……
}

后端代理(Node.js,同一种语言):

javascript
// server.js —— /api/geo 同源代理,转发 IP9 并做缓存
const express = require('express');
const app = express();

const cache = new Map();   // ip -> { data, at }

app.get('/api/geo', async (req, res) => {
  // 取真实客户端 IP(前面挂了 Nginx/CDN 时用 x-forwarded-for)
  const clientIp = (req.headers['x-forwarded-for'] || req.socket.remoteAddress || '')
    .split(',')[0].trim();

  // 每个 IP 缓存 10 分钟,代理层就把免费版 60 次/分钟的额度省下来了
  const hit = cache.get(clientIp);
  if (hit && Date.now() - hit.at < 10 * 60 * 1000) {
    return res.json(hit.data);
  }

  const r = await fetch(`https://ip9.com.cn/get?ip=${encodeURIComponent(clientIp)}`);
  const data = await r.json();
  cache.set(clientIp, { data, at: Date.now() });
  if (cache.size > 5000) cache.clear();     // 简单防内存膨胀
  res.json(data);
});

app.listen(3000);

关键环节拆解:

  • 降级链的顺序不能反:GPS 永远优先,IP 永远兜底。反过来,「先查 IP 再试 GPS」会让同城用户多等一轮网络请求;
  • 代理是必须的,不是可选项:IP9 接口没有 CORS 头,浏览器直连直接报跨域,走自己的 Node 服务转发一次,顺带把缓存做了,双赢;
  • 缓存分层:浏览器 localStorage 管 24 小时,后端 Map 管 10 分钟,两层一起把接口调用压到极低——免费额度 60 次/分钟/IP,对绝大多数站点是富余的。

四、落地注意事项 ​

  • 精度文案要诚实:IP 归属地是「城市中心点」,文案写「按 IP 显示的城市级位置」,别写「精准定位」;涉及用户位置展示的合规要求,把「通过 IP 推断城市」写进隐私政策;
  • iOS 特殊处理:用户曾在设置里关过定位,getCurrentPosition 会秒拒,不要反复弹窗求授权——直接走 IP 兜底,比三番五次打扰用户体验好得多;
  • 代理也要防滥用:/api/geo 加个简单的频率限制(单 IP 每分钟最多 60 次),防止被刷;生产环境缓存建议按 IP 用 Map 或 Redis,并定期清理;
  • 灰度验证:上线后对比「GPS 城市」与「IP 城市」的命中差异,同城推荐转化率不掉即可放心铺量。

总结 ​

定位权限是用户隐私和产品功能之间的拉扯,硬要权限只会把人推走。降级链的思路简单且实用:navigator.geolocation 拿得到就用,拿不到就调自己的 /api/geo 代理,代理层转发 https://ip9.com.cn/get?ip=<客户端IP>(免费版无需注册,返回国家/省市/运营商,城市级精度),前端无感切换、缓存 24 小时。同城推荐、默认城市、语言币种兜底,这一套就够了。真需要街道级的场景,再上 GPS 和逆地理,不迟。