移动端webview定位--爬坑经验

6,545 阅读10分钟

写在前面

最近的一个业务需求,要求在进入页面之后,获取用户的地理位置,然后根据地理位置展示相关的内容。

说起来似乎是一个非常简单的需求,但是。。如果你的公司有lbs服务的话,并且不考虑适配,那么它真的非常非常简单。引入一个sdk或者访问一个后台接口,就可以拿到相应的地理位置了。当然,国内有完整lbs服务链路的公司也就是屈指可数。

另外就是适配,地理位置服务的适配并不是适配机型,而是适配平台,不同的平台提供的服务也是不一致的,下面来说说几种定位方式及其优劣,并且整理一个通用的方案吧(或许根本不存在通用的方案)。

lbs

先说说lbs,不说那么多没用的了,基于位置的服务,听名字大家应该都知道到底是个什么东西。

大部分成熟的互联网平台都或多或少地使用了lbs,来根据用户的当前地理位置来进行推荐或者搜索。

说这个就是为了说明lbs很常用,也是必须要了解到的一个缩写。

location

环境

首先说一下我们这个业务需要适配的平台:

  1. 站内,也就是团队的app内部;
  2. 微信,最大的站外分享平台;
  3. 其他的移动端站外平台,包括但不限于QQ、微博、各种浏览器;
  4. 甚至有些用户可能会在电脑端微信打开。

环境很恶劣是不是?如果你的团队有着自己的app,并且希望自己的内容可以被人分享到站外浏览,那么上面的四条,你基本都需要考虑了。

如果你做的是微信小程序,那么恭喜你,下面基本上不用看了,因为微信给了小程序很好的开发环境,不需要考虑这么多东西。

方法

移动端GPS定位

移动端GPS定位这个方法仅限于你们有着自己的app,作为一个前端开发一般是不需要了解native是如何给你各种JSBridge的。但是稍微了解一点也蛮好的。

说起nativeweb通信,不得不说的就是JSBridge,native通过JSBridge提供各种必须但是web拿不到的方法,比如原生的定位,麦克风,摄像头等设备的使用。

举个栗子

function setupWebViewJavascriptBridge(callback) {
    if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
    if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
    window.WVJBCallbacks = [callback];
    var WVJBIframe = document.createElement('iframe');
    WVJBIframe.style.display = 'none';
    WVJBIframe.src = 'https://__bridge_loaded__';
    document.documentElement.appendChild(WVJBIframe);
    setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
setupWebViewJavascriptBridge(function(bridge) {
    /* Initialize your app here */
    var button = document.querySelector(".JSToNativeButton");
    button.addEventListener('click', function(event) {
        bridge.callHandler('JsToNative', "This is a message from javascript to native");
    })
    bridge.registerHandler('NativeToJs', function(data, responseCallback) {
        alert(data);
    })
    // bridge.callHandler('ObjC Echo', {'key':'value'}, function responseCallback(responseData) {
    //     console.log("JS received response:", responseData)
    // })
})

这是一个很简单的web和native的例子,上面是web端的代码。可以看出来,通信的方法就是我们最常使用的事件监听机制。上面的代码看起来有点绕,但是应该还是可以看懂的,就不多解释了。因为似乎有点跑题了。既然跑题了就索性说完吧Orz。下面的是相关的一点OC代码片段。

[self.bridge registerHandler:@"JsToNative" handler:^(id data, WVJBResponseCallback responseCallback) {
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"message from JS" message:data preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *actionCancel = [UIAlertAction actionWithTitle:@"close" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        
    }];
    [alertController addAction:actionCancel];
    [self presentViewController:alertController animated:YES completion:nil];
}];

简而言之,JSBridge可以实现两端的通信,native可以注册一个window上面的对象,这个对象提供给web注册事件的方法(上面代码中的bridge.registerHandler),这个注册事件在native可以触发,并且传入参数,让native能够通知到web端。又在native上可以注册事件,然后可以在web中调用这个事件函数,传入参数,让web能够调用native的功能。

跑题跑的够远的,但是之所以跑题,是因为这个方案确实没啥可说的。native给你约定一个协议,然后你去调用那个协议,传个函数进去等着回调就好了。。基本上对于我们前端没有很大的难度和工作量。

什么? 你们的native没有提供这个协议? 下个迭代给他们排的满满的。

提需求的来啦

这个方法好嘛?

当然好,非常好。定位准,提示友好,除了会弹出权限(当然这也是必须的),基本上是app内的最优解。当然如果你们有着大量的用户定位数据,并且能够保证这些数据实时有效的话,那当我没说。即使有接口,静默定位会不会引起用户反感,还需要你的交互和策划来确定。

这个小demo在我的github上面有工程,哇,才发现写了半年论文,github都好久没更新了。。

H5定位

新的标准给了我们很多统一口径的机会。不得不说,H5定位的兼容性还是不错的,目前我们团队的移动端兼容性支持到了android 4.4.2以上,这个版本以上的手机,基本上都支持了H5定位的功能。就在我准备将其当做第一解决方案的时候,我却发现了一个能让交互疯掉的问题。

首先,H5定位会要求第三方平台,也就是app的地理位置权限,其次,H5定位会弹出一个webview的地理位置权限弹窗。

这两个权限只要有一个被取消,那么H5定位就只能够根据IP来进行定位了。

两个弹框还不是重点,最重要的是H5定位,webview的弹框上面会显示:www.xxx.com想要获取您的地理位置。,这样会让用户非常迷惑,用户可能不知道你的域名,只知道你的软件叫做什么。

除了这两个问题,H5定位无论是兼容性,还是对于前端定位的统一性来说,都是一个非常之好的选择。

const noop = () => {};
getH5Location(options = {}) {
    const cb = options.cb || noop;
    const errCb = options.errCb || noop;
    let isLocation = false;
    if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(
            res => {
                isLocation = true;
                cb(res.coords.longitude, res.coords.latitude);
            },
            () => {
                isLocation = true;
                errCb();
            }
        );
    }
    // 定位兜底
    setTimeout(() => {
        if (!isLocation) {
            errCb();
        }
    }, 5000);
},

这是一个简单的H5定位的封装,为了防止某些设备不支持H5定位,加了个兜底的方法。兜底触发的延迟可以根据需求自己设置。


这里要注意H5定位的一个小坑,让我爬了很久。众所周知,H5定位在比较新的浏览器中,都会要求https协议才能够进行定位,当你将协议切换成了https之后,你会发现所有的http资源都被H5定位阻塞了。但是如果你注释掉H5定位的代码,就会发现http资源仅仅会显示一个warning,但是可以正常显示。

如果你的静态资源有https版本的话,还是推荐你使用//via.placeholder.com/333x333这种无协议方式引入,这样可以自动切换资源协议,防止资源被阻塞。


微信API

在微信平台,因为H5的定位的种种小问题,虽然不影响使用,但是效果总是差那么一丢丢。如果你的团队能够申请到微信的SDK,那么就是极好的了。

简单的调用,前提是你引入了微信的js-sdk,这不是一个很困难的事情。微信给的文档已经非常详细,这里就不赘述了。

wx.getLocation({
    type: 'wgs84', // 默认为wgs84的gps坐标,如果要返回直接给openLocation用的火星坐标,可传入'gcj02'
    success: function (res) {
        var latitude = res.latitude; // 纬度,浮点数,范围为90 ~ -90
        var longitude = res.longitude ; // 经度,浮点数,范围为180 ~ -180。
        var speed = res.speed; // 速度,以米/每秒计
        var accuracy = res.accuracy; // 位置精度
    }
});

在微信平台,使用微信的接口无疑是最好的选择。它完善,简单,经过了无数人的使用,很NICE,而且微信平台是你必须适配的平台,因为微信平台是你的大部分落地页的第一着陆点。

使用方法微信公众平台文档中已经写得非常详细了。

爸爸

第三方SDK(百度地图,高德地图等)

这是最不靠谱的一种方法,我花了一整天时间,把百度地图,高德地图,腾讯地图就引入了,并且进行了尝试。颇有一种病急乱投医的风格。

后来从根本上思考了这个问题。他们是根据什么定位的。在没有GPS权限的情况下,他们难道不是通过IP来进行定位的吗??

果然,尝试了多次之后才发现,这些第三方SDK中,有些会通过H5来定位,因为弹出了api.map.xxx.com想要获取您的位置,这样我何不使用H5定位呢。后台爸爸们已经提供了一个IP定位的接口,如果还是通过第三方SDK进行IP定位,那不是浪费了后台爸爸们的辛勤劳动呢。

第三方地图很好,但是他们的定位服务并不一定好,如果你仅仅需要定位,那么还是不要考虑这个方法了,因为第三方地图SDK的主要功能是得到可视化的地图。

我居然花了半天时间测试完之后才想明白这个问题。。

如果你想要可视化的地图服务,请选择第三方地图SDK,如果仅仅是为了定位,那么其他方法都是好于这个方法的。

IP定位

依赖后端爸爸给的接口,可以直接通过接口拿到保存在后端数据库内的定位信息。或者通过用户的IP,来进行地理位置的获取。

看起来很美好。如果你比较追求定位的准确度的话,还是放弃这个方法吧。IP定位在4G网络中的效果非常之差。

4G网络环境下,IP定位一般是根据运营商的归属地或者基站来进行定位的。如果你住在北京四环,很有可能把你定位到石家庄。。

但是也没办法,在拿不到权限的情况下,IP定位可以作为兜底的方案,还是比较现实的。

终极方案

哈哈,其实所谓的终极方案就是把上面的多个方案进行适配。

  • 首先,app内部让客户端开发们给你搭一个JSBridge,来让你好好地调用一下native的定位方法。

  • 其次,作为第一分享平台的微信,当然要特殊照顾了,微信通过微信的js-sdk来进行定位。

  • 再次,对于其他所有移动端,客户端平台,可以一概而论了。全部采用H5定位,暴力又好用。

  • 最后,任意一个定位失效了之后,乖乖IP定位吧。

当然,上面所说的这么复杂的适配方案是让你得到更好的用户体验而设计的。如果你觉得麻烦或者某些条件不允许(客户端排期满了?不存在的)。可以根据上述的优缺点,进行替换,适合自己的方案才是最好的方案。

最后,别忘了封装一下你的定位函数,让后边的人能够更方便的复用。你一定也不希望下次再需要定位的时候,再回来看这篇干干的文章吧~

export const getLocation = (options = {
    cb: () => {},
    errCb: () => {},
}) => {
    const isInApp = Utils.getEnv().isInApp();
    const isInWechat = Utils.isInWechat();
    if (isInApp) {
        // 站内定位
        return this.getAppLocation(options);
    }
    if (isInWechat) {
        // 微信定位
        return this.getWechatLocation(options);
    }
    // H5定位
    return this.getH5Location(options);
},