一些类似于 饿了么或者 58同城这种具有本地化服务性质的网站,一般都会对在用户访问时进行地理位置的定位,以便于精确服务,例如北京的用户访问 58同城,那么就把他定位到北京市,跳转到北京站点,让他浏览北京的本地信息,饿了么对于地理位置的要求更加严格,巴不得精确到米,所以地理位置的获取在网站中定然不是一个可有可无的东西,经常会用到,好在,关于定位这件事,在当下,特别是移动端已经得到了很好的支持。
HTML5 Geolocation
简单介绍
HTML5 Geolocation API
用于获得用户的地理位置。
鉴于该特性可能侵犯用户的隐私,除非用户同意,否则用户位置信息是不可用的。
HTML5
中新增加的 API
: navigator.geolocation
就是专门用于获取地理位置的,我们一般在访问 类似 58同城
或者 饿了么
这种网站的时候,首先可能看到的不是网页内容,而是屏幕上弹出的用于询问是否允许网站获取位置信息的弹窗,如果点了允许,那么网站就可以对我们的位置进行精确定位。
由于是 HTML5
中新增的 API
,所以并不一定所有的浏览器都支持,以下为在 caniuse上显示的浏览器支持情况:
可以看到,这个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
概览
在控制台打印 navigator.geolocation
,会打印出上图所示内容。
可以看到,此 API
一共存在三个方法:
clearWatch
getCurrentPosition
watchPosition
其中, getCurrentPosition
就是用于获取地理位置信息的接口。
getCurrentPosition
如果继续深入查看每个方法的话,会看到三个方法中,每个方法的 length
参数都是 1
这就意味着这三个方法都是可以传参的,其中 getCurrentPosition
可以接收三个参数(为什么可以接收 3
个参数而 length
却是 1
,这个我也不清楚,知道的方便告知一下?)
navigator.geolocation.getCurrentPosition(successCallback, errorCallback, options)
successCallback
为方法成功的回调,即成功获取位置信息,为必选参数
errorCallback
为方法失败的回调,即获取位置信息失败,为可选参数
options
为可选参数,是一个对象,支持三个可选的属性:enableHighAccuracy timeout maximumAge
enableHighAccuracy
表示是否获取高精度的地理位置,Boolean
类型,默认为false
,所以如果不指定此参数,一般获取的都是低精度的位置信息,如果开启,则将使得获取位置信息的响应等待时间较长,并且消耗更多的流量与电量。timeout
表示等待响应的最大时间,单位为ms
,默认是0
,表示无穷时间,一直尝试获取位置信息,直到获取成功或者失败。maximumAge
表示应用程序的缓存时间,单位是ms
,默认是0
,表示无缓存,每次请求都会立即去获取一个新的位置信息。
successCallback
和 errorCallback
回调函数都存在一个参数,前者的参数表示取得的位置信息对象 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
返回的数据如下:
因为这里获取到的经纬度还是不够精确,所以定位位置的覆盖面依旧是一片大概的区域,而不是精确的某个点,但在实际应用中,这已经足够了。
另外,如果真是需要应用在实际网站中,最好找个靠谱点的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
返回字段示例如下:
由此,就可以得到访客所在的城市信息,虽然精度有点低,但在某些情况下也已经足够使用了。