使用HTML5-geolocation以及 IP 获取地理位置信息

时间:2020-12-01 10:46:17

一些类似于 饿了么或者 58同城这种具有本地化服务性质的网站,一般都会对在用户访问时进行地理位置的定位,以便于精确服务,例如北京的用户访问 58同城,那么就把他定位到北京市,跳转到北京站点,让他浏览北京的本地信息,饿了么对于地理位置的要求更加严格,巴不得精确到米,所以地理位置的获取在网站中定然不是一个可有可无的东西,经常会用到,好在,关于定位这件事,在当下,特别是移动端已经得到了很好的支持。


HTML5 Geolocation

简单介绍

HTML5 Geolocation API 用于获得用户的地理位置。
鉴于该特性可能侵犯用户的隐私,除非用户同意,否则用户位置信息是不可用的。

HTML5中新增加的 API: navigator.geolocation就是专门用于获取地理位置的,我们一般在访问 类似 58同城 或者 饿了么这种网站的时候,首先可能看到的不是网页内容,而是屏幕上弹出的用于询问是否允许网站获取位置信息的弹窗,如果点了允许,那么网站就可以对我们的位置进行精确定位。

由于是 HTML5中新增的 API,所以并不一定所有的浏览器都支持,以下为在 caniuse上显示的浏览器支持情况:

使用HTML5-geolocation以及 IP 获取地理位置信息

可以看到,这个API的支持情况其实已经是很好了,完全可以运用在实际的网站项目中了,不过据我测试,pc端对于此API的支持似乎有些问题,尽管你有时候已经同意了获取地理位置,但依旧获取不到,我估计应该是浏览器或者系统的安全机制从中作梗,MDN上也对此有一点说明:

Note: For security reasons, when a web page tries to access location information, the user is notified and asked to grant permission. Be aware that each browser has its own policies and methods for requesting this permission.

大致意思如下:

出于安全考虑,当网页请求获取用户位置信息时,用户会被提示进行授权。注意不同浏览器在请求权限时有不同的策略和方式。Windows10在未开启定位的情况下无法获取位置。

不过尽管 pc有些问题,但移动端是没有任何问题的,大部分情况下,只要你允许网站获取位置信息,那么都是可以获取到的,而且实际场景中,网站需要获取地理位置都是的场景都是发生在移动端,所以这个问题完全可以忽略掉。

检测浏览器是否支持此属性的代码如下:

if ("geolocation" in navigator) {
/* 地理位置服务可用 */
} else {
/* 地理位置服务不可用 */
}

API概览

使用HTML5-geolocation以及 IP 获取地理位置信息

在控制台打印 navigator.geolocation,会打印出上图所示内容。

可以看到,此 API一共存在三个方法:

clearWatch
getCurrentPosition
watchPosition

其中, getCurrentPosition就是用于获取地理位置信息的接口。

  • getCurrentPosition

如果继续深入查看每个方法的话,会看到三个方法中,每个方法的 length参数都是 1

使用HTML5-geolocation以及 IP 获取地理位置信息

这就意味着这三个方法都是可以传参的,其中 getCurrentPosition可以接收三个参数(为什么可以接收 3个参数而 length却是 1,这个我也不清楚,知道的方便告知一下?)

navigator.geolocation.getCurrentPosition(successCallback, errorCallback, options)

  • successCallback为方法成功的回调,即成功获取位置信息,为必选参数

  • errorCallback为方法失败的回调,即获取位置信息失败,为可选参数

  • options 为可选参数,是一个对象,支持三个可选的属性: enableHighAccuracy timeout maximumAge

    1. enableHighAccuracy
      表示是否获取高精度的地理位置,Boolean类型,默认为 false,所以如果不指定此参数,一般获取的都是低精度的位置信息,如果开启,则将使得获取位置信息的响应等待时间较长,并且消耗更多的流量与电量。
    2. timeout
      表示等待响应的最大时间,单位为 ms,默认是 0,表示无穷时间,一直尝试获取位置信息,直到获取成功或者失败。
    3. maximumAge
      表示应用程序的缓存时间,单位是 ms,默认是 0,表示无缓存,每次请求都会立即去获取一个新的位置信息。

successCallbackerrorCallback回调函数都存在一个参数,前者的参数表示取得的位置信息对象 position,此对象中存在如下几个属性:

属性 释义
coords.latitude 纬度数值
coords.longitude 经度数值
coords.altitude 海拔高度,可能返回 null
coords.accuracy 精确度
coords.altitudeAccuracy 海拔高度的精确度,可能返回 null
coords.heading 移动方向,可能返回 null
coords.speed 移动速度,单位为 m/s
timestamp 当位置捕获到时的时间戳

errorCallback的参数同样也是一个对象,存在两个属性值:

属性 释义
message 错误信息
code 错误代码

其中,错误代码 code存在以下几个值:

错误代码值 释义
PERMISSION_DENIED 表示用户拒绝浏览器获取位置信息的请求
POSITION_UNAVALIABLE 表示网络不可用或者连接不到卫星
TIMEOUT 表示获取超时。必须在options中指定了timeout值时才有可能发生这种错误
UNKNOW_ERROR 表示除了以上几种错误之外的错误

options 的三个参数都比较有用,当需要高精度的位置信息时,例如 饿了么网站对用户的定位,就需要将 enableHighAccuracy设为 true

如果与网站页面渲染有关的请求的数据依赖于经纬度信息,那么最好设置一个 timeout的最大值,因为有时候可能设备获位置信息的响应时间会比较长,这将导致页面长时间的空白,用户体验很不好。

如果需要对地理位置进行实时或者一定时间内的实时刷新,那么可指定 maximumAge的值。

基于以上,我们就可以写出获取地理位置信息的代码了,下面是我从 菜鸟教程 直接抄下来的代码:

var x=document.getElementById("demo");
function getLocation(){
if (navigator.geolocation){
navigator.geolocation.getCurrentPosition(showPosition,showError, {timeout:2000});
}
else{
x.innerHTML="该浏览器不支持定位。";
}
}
function showPosition(position){
x.innerHTML="纬度: " + position.coords.latitude +
"<br>经度: " + position.coords.longitude;
}
function showError(error){
switch(error.code) {
case error.PERMISSION_DENIED:
x.innerHTML="用户拒绝对获取地理位置的请求。"
break;
case error.POSITION_UNAVAILABLE:
x.innerHTML="位置信息是不可用的。"
break;
case error.TIMEOUT:
x.innerHTML="请求用户地理位置超时。"
break;
case error.UNKNOWN_ERROR:
x.innerHTML="未知错误。"
break;
}
}

由于此方法存在两个回调参数,所以写起来可能有点不太优雅,当然你也可以为这两个回调函数单独命名,使用命名函数代替,但还有一个问题,如果你想将此方法作为工具方法,然后在别的地方调用这个方法,只想取得返回值,不想关心什么成功回调、失败回调什么之类的东西,那么可能就需要稍微动些手脚,下面给出一种实现示例,主要是利用 Promise进行包装:

const getLocationLatLon = (options) => {
return new Promise((resolve, reject) => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition((position) => {
resolve({ isGet: true, data: { position } })
}, (error) => {
switch (error.code) {
case error.PERMISSION_DENIED:
reject({ isGet: false, data: 'User denied the request for Geolocation' })
break
case error.POSITION_UNAVAILABLE:
reject({ isGet: false, data: 'Location information is unavailable' })
break
case error.TIMEOUT:
reject({ isGet: false, data: 'The request to get user location timed out' })
break
case error.UNKNOWN_ERROR:
reject({ isGet: false, data: 'An unknown error occurred' })
break
default:
reject({ isGet: false, data: 'other error' })
}
}, options)
} else {
reject({ isGet: false, data: 'Geolocation is not supported by this browser' })
}
})

}

这样,想要获取地理位置信息的时候,只需要:

getLocationLatLon(options).then(res => {
// 成功获取到地理位置
}).catch(error => {
// 获取地理位置失败或者其他错误
})
  • watchPosition

用于注册监听器,在设备的地理位置发生改变的时候自动被调用。也可以选择特定的错误处理函数。
此方法同样存在 3个参数:successCallback, errorCallback, options,并且与上面 getCurrentPosition相同,就不多加累赘了。

另外,此方法会返回一个 ID,可以通过此 ID来取消监听器

id = navigator.geolocation.watchPosition(success[, error[, options]])

  • clearWatch()

此方法主要用于上面提到的使用 watchPosition()注册的 位置/错误 监听器,让设备不再进行监听地理位置信息的操作,语法如下:

navigator.geolocation.clearWatch(id);

以上,基本上就是 navigator.geolocation这个 API的大概内容。


根据经纬度获取实际对应的位置信息

获取到经纬度后,不可能直接就这样呈现出来的,还需要将经纬度进行与实际地理位置进行映射,一般都是通过调用第三方开放 api实现的,例如下面这个 api:

http://apis.map.qq.com/jsapi?qt=rgeoc&lnglat=113.743553,23.023746&key=FBOBZ-VODWU-C7SVF-B2BDI-UK3JE-YBFUS&output=jsonp&pf=jsapi&ref=jsapi&cb=qq.maps._svcb3.geocoder0

返回的数据如下:

使用HTML5-geolocation以及 IP 获取地理位置信息

因为这里获取到的经纬度还是不够精确,所以定位位置的覆盖面依旧是一片大概的区域,而不是精确的某个点,但在实际应用中,这已经足够了。

另外,如果真是需要应用在实际网站中,最好找个靠谱点的api,不然万一 api随时挂了,你都没地方说理,靠谱点的 api例如百度地图 或者 高德地图 等都有提供, 不过可能需要你注册一下。


基于 IP的模糊地理位置信息

上面已经提到,基于用户隐私安全方面的原因,所以地理位置信息的获取是必须要经过用户的同意,如果用户不允许设备读取地理位置信息,那么 navigator.geolocation就是毫无用武之地的。

但是,有时候我们在访问 天猫超市 或者 58同城这类网站的时候,我们明明已经是禁止了设备读取地理位置信息,为什么最后访问的站点依旧可以定位到当前所在的城市呢?例如,当我们在北京访问 58同城网站的时候,禁止设备读取位置信息,但我们最终看到的站点依旧是北京站,而不是其他城市或者全国站,这是怎么做到的呢?

这就要使用到另外一种历史更加久远的技术了:基于 IP地址进行的定位。

不论是电脑还是 手机,只要能正常上网,肯定都需要有一个外网 IP,而这些 IP的归属地是可以公开查询到的,网站后端一般都是能够获取到访客的 IP,根据 IP的归属地来源,自然也就能得到访客的位置信息了。

例如,以下就是在 php程序中,获取访客 IP信息的一种简单实现:

function getIp(){    
$ip = false;
if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])){
// 有可能获取不到,并且有可能获取到的 ip 不止一个,需要进行判断,
// 这种方法能在一定程度上分辨出代理 ip 和 真实 ip
$ips = $_SERVER['HTTP_X_FORWARDED_FOR'];
for ($i = 0; $i < count($ips); $i++) {
if (!eregi ("^(10│172.16│192.168).", $ips[$i])) {
$ip = $ips[$i];
break;
}
}

} else if (isset($_SERVER['HTTP_CLIENT_IP'])){
// 非标准化方法,很可能为空
$ip = $_SERVER['HTTP_CLIENT_IP'];
} else {
// 基本上能够获取到值,但获取到 ip不一定是真实设备的 ip,
// 有可能是代理服务器的 ip
$ip = $_SERVER['REMOTE_ADDR'];
}
return $ip;
}

echo getIp();

如代码中提到的,服务端获取到的访问 ip不一定就是用户的真实 ip,如果用户使用了 vpn或者其他手段,让到达服务器的 ip不是原始设备的真实 ip,那么就是不可靠的,不过这种用户比较是少数,而且真的不想让你获取 ip的话,你怎么做都没用,不过在真实场景中,能做到这一步已经算是足够了。

获取到 ip当然还不够,我们需要的是用户地理位置信息,所以还需要判断 ip的归属地,这种服务早就有了,网上随便一搜就是一大堆,比如 360提供的一个开放 api示例:

https://open.onebox.so.com/dataApi?callback=jQuery183016764307423605596_1503671209230&type=ip&src=onebox&tpl=0&num=1&query=ip&ip=123.127.52.192&_=1503671215783

返回字段示例如下:

使用HTML5-geolocation以及 IP 获取地理位置信息

由此,就可以得到访客所在的城市信息,虽然精度有点低,但在某些情况下也已经足够使用了。