h5浏览器,唤起app

代码:

 /*
         * function getDeviceBrowser
         * @return {Object} device设备系统,browser浏览器类型
         */
        function _getDeviceBrowser () {
            var ua = window.navigator.userAgent;
            //device & browser
            /*
             * isIos 是否为ios
             * isAndroid 是否为android
             * isMobile 是否为mobile
             */
            var device = {
                isIos: false,
                isAndroid: false,
                isMobile: false,
                //isIpad: false,
                //isIphone: false,
                iosVersion: ""
            }
            /*
             * isWX 是否为微信
             * isQQ 是否为QQ
             * isUC 是否为UC
             */
            var browser = {
                isWX: false,
                isQQ: false,
                isUC: false,
                isWeiBo: false,
                isSafari: false,
                isChrome: false
            }

            //设备系统判断
            device.isIos = (/iPhone os/i.test(ua));
            device.isAndroid = (/android/i.test(ua));
            device.isMobile = !!window.navigator.userAgent.match(/AppleWebKit.*Mobile.*/);

            //ios版本判断
            //iphone:/iphone os ([0-9]+)_/
            //ipad:/cpu os ([0-9]+)_/
            if (ua.toLowerCase().indexOf("like mac os x") > 0) {
                var regStr_saf = /os [\d._]*/gi ;
                var verinfo = ua.toLowerCase().match(regStr_saf) ;
                device.iosVersion = (verinfo+"").replace(/[^0-9|_.]/ig,"").replace(/_/ig,".");
            }

            //浏览器
            browser.isWX = (/micromessenger/i.test(ua));
            browser.isQQ = (/mqqbrowser|qq/i.test(ua));
            browser.isUC = (/ucbrowser/i.test(ua));
            browser.isWeiBo = (/weibo/i.test(ua));
            browser.isSafari = (/safari/i.test(ua) && !/mqqbrowser/i.test(ua));
            browser.isChrome = (/chrome/i.test(ua) && !/mqqbrowser/i.test(ua));

            return {
                device: device,
                browser: browser,
            }

        }


/**
 * Created by sxj on 16/12/28.
 */

angular.module('h5_angularjs');
app.factory("$evokeapp", ['$window', 'FW.utility',
    function ($w, utility) {
        var db = utility.getDeviceBrowser();
        var _URL = {
            "ios": {
                openUrl: "yiqianbao://",
                downloadUrl: "https://itunes.apple.com/app/id745097904"
            },
            "android": {
                openUrl: "eqianbao://",
                downloadUrl: "//d.1qianbao.com/youqian/app/1qb_88888.apk"
            },
            baseUrl: "//d.1qianbao.com/youqian/"
        }

        //alert("isIos: " + db.device.isIos);

        //检查app是否打开
        function openApp(openUrl, callback) {
            function checkOpen(cb){
                var _clickTime = +(new Date());
                function check(elsTime) {
                    if ( elsTime > 3000 || document.hidden || document.webkitHidden) {
                        cb(1);
                    } else {
                        cb(0);
                    }
                }
                //启动间隔20ms运行的定时器,并检测累计消耗时间是否超过3000ms,超过则结束
                var _count = 0, intHandle;
                intHandle = setInterval(function(){
                    _count++;
                    var elsTime = +(new Date()) - _clickTime;
                    if (_count>=100 || elsTime > 3000 ) {
                        clearInterval(intHandle);
                        check(elsTime);
                    }
                }, 20);
            }

            //在iframe 中打开APP
            var ifr = document.createElement('iframe');
            ifr.src = openUrl;
            ifr.style.display = 'none';
            if (callback) {
                checkOpen(function(opened){
                    callback && callback(opened);
                });
            }

            document.body.appendChild(ifr);
            setTimeout(function() {
                document.body.removeChild(ifr);
            }, 2000);
        }

        //唤起函数
        function evokeUp() {
            if (db.browser.isWX) {
                //微信走应用宝流程
                window.location.href = 'http://a.app.qq.com/o/simple.jsp?pkgname=com.paic.zhifu.wallet.activity';
            } else {
                //
                if (db.device.isIos) {
                    //ios9以下
                    alert(db.device.iosVersion);
                    if (+db.device.iosVersion >= 9) {

                    } else {
                        openApp(_URL.ios.openUrl, function(isOpen) {
                            //alert(isOpen);
                            //唤起之后的回调
                            !isOpen && (window.location.href = _URL.ios.downloadUrl);
                        });
                    }
                } else if (db.device.isAndroid) {
                    openApp(_URL.android.openUrl, function(isOpen) {
                        //alert(isOpen);
                        //唤起之后的回调
                        !isOpen && (window.location.href = _URL.android.downloadUrl);
                    });
                } else {
                    //走base 流程
                    window.location.href = _URL.baseUrl;
                }

            }
        }

        // 部分浏览器只支持 vendor-prefixed
        // 根据浏览器支持情况设置隐藏属性和可见状态改变事件
        //var hidden, state, visibilityChange;
        //if (typeof document.hidden !== "undefined") {
        //    hidden = "hidden";
        //    visibilityChange = "visibilitychange";
        //    state = "visibilityState";
        //} else if (typeof document.mozHidden !== "undefined") {
        //    hidden = "mozHidden";
        //    visibilityChange = "mozvisibilitychange";
        //    state = "mozVisibilityState";
        //} else if (typeof document.msHidden !== "undefined") {
        //    hidden = "msHidden";
        //    visibilityChange = "msvisibilitychange";
        //    state = "msVisibilityState";
        //} else if (typeof document.webkitHidden !== "undefined") {
        //    hidden = "webkitHidden";
        //    visibilityChange = "webkitvisibilitychange";
        //    state = "webkitVisibilityState";
        //}
        // 添加一个时间来实时改变页面的标题
        //document.addEventListener(visibilityChange, function(e) {
        //    // Start or stop processing depending on state
        //    if (document.webkitHidden) {
        //       alert("离开当前窗口啦");
        //    } else {
        //        document.title = "选中当前窗口啦";
        //    }
        //
        //
        //}, false);


        return {
            evoke: function(url) {
                evokeUp();
            }
        }
    }
]);


网易新闻实现代码

window.NRUM = window.NRUM || {};
window.NRUM.config = {
    key:'27e86c0843344caca7ba9ea652d7948d',
    clientStart: +new Date()
};
(function() {
    var n = document.getElementsByTagName('script')[0],
        s = document.createElement('script');

    s.type = 'text/javascript';
    s.async = true;
    s.src = '//nos.netease.com/apmsdk/napm-web-min-1.1.3.js';
    n.parentNode.insertBefore(s, n);
})();


;
(function(window,doc){

    // http://apm.netease.com/manual?api=web
    NRUM.mark && NRUM.mark('pageload', true)

    var list = []
    var config = null

    // jsonp
    function jsonp(a, b, c) {
        var d;
        d = document.createElement('script');
        d.src = a;
        c && (d.charset = c);
        d.onload = function() {
            this.onload = this.onerror = null;
            this.parentNode.removeChild(this);
            b && b(!0);
        };
        d.onerror = function() {
            this.onload = this.onerror = null;
            this.parentNode.removeChild(this);
            b && b(!1);
        };
        document.head.appendChild(d);
    };


    function localParam(search,hash){
        search = search || window.location.search;
        hash = hash || window.location.hash;
        var fn = function(str,reg){
            if(str){
                var data = {};
                str.replace(reg,function( $0, $1, $2, $3 ){
                    data[ $1 ] = $3;
                });
                return data;
            }
        }
        return {search: fn(search,new RegExp( "([^?=&]+)(=([^&]*))?", "g" ))||{},hash: fn(hash,new RegExp( "([^#=&]+)(=([^&]*))?", "g" ))||{}};
    }

    jsonp('http://active.163.com/service/form/v1/5847/view/1047.jsonp')

    window.search = localParam().search
    window._callback = function(data) {
        window._callback = null
        list = data.list
        if(search.s && !!search.s.match(/^wap/i)) {
            config = list.filter(function(item){
                return item.type === 'wap'
            })[0]
            return
        }
        config = list.filter(function(item){
            return item.type === search.s
        })[0]
    }

    var isAndroid = !!navigator.userAgent.match(/android/ig),
        isIos = !!navigator.userAgent.match(/iphone|ipod/ig),
        isIpad = !!navigator.userAgent.match(/ipad/ig),
        isIos9 = !!navigator.userAgent.match(/OS 9/ig),
        isYx = !!navigator.userAgent.match(/MailMaster_Android/i),
        isNewsapp = !!navigator.userAgent.match(/newsapp/i),
        isWeixin = (/MicroMessenger/ig).test(navigator.userAgent),
        isYixin = (/yixin/ig).test(navigator.userAgent),
        isQQ = (/qq/ig).test(navigator.userAgent),
        params = localParam().search,
        url = 'newsapp://',
        iframe = document.getElementById('iframe');

    var isIDevicePhone = (/iphone|ipod/gi).test(navigator.platform);
    var isIDeviceIpad = !isIDevicePhone && (/ipad/gi).test(navigator.platform);
    var isIDevice = isIDevicePhone || isIDeviceIpad;
    var isandroid2_x = !isIDevice && (/android\s?2\./gi).test(navigator.userAgent);
    var isIEMobile = !isIDevice && !isAndroid && (/MSIE/gi).test(navigator.userAgent);
    var android_url = (!isandroid2_x) ? "http://3g.163.com/links/4304" : "http://3g.163.com/links/6264";
    var ios_url = "http://3g.163.com/links/3615";
    var wphone_url = "http://3g.163.com/links/3614";
    var channel = params.s || 'newsapp'

    // 判断在不同环境下app的url
    if(params.docid){
        if(params['boardid'] && params['title']){
            url = url + 'comment/' + params.boardid + '/' + params.docid + '/' + params.title
        }else{
            url = url + 'doc/' + params.docid
        }
    }else if(params.sid){
        url = url + 'topic/' + params.sid
    }else if(params.pid){
        var pid = params.pid.split('_')
        url = url + 'photo/' + pid[0] + '/' + pid[1]
    }else if(params.vid){
        url = url + 'video/' + params.vid
    }else if(params.liveRoomid){
        url = url + 'live/' + params.liveRoomid
    }else if(params.url){
        url = url + 'web/' + decodeURIComponent(params.url)
    }else if(params.expertid){
        url = url + 'expert/' + params.expertid
    }else if(params.subjectid){
        url = url + 'subject/' + params.subjectid
    }else if(params.readerid){
        url = url + 'reader/' + params.readerid
    }else{
        url += 'startup'
    }
    if(url.indexOf('?') >= 0){
        url += '&s=' + (params.s || 'sps')
    }else{
        url += '?s=' + (params.s || 'sps')
    }

    // ios && 易信  用iframe 打开
    if((isIos||isIpad) && navigator.userAgent.match(/yixin/i)) {
        document.getElementById('iframe').src = url;
    }

    var height = document.documentElement.clientHeight;

    // 通常情况下先尝试使用iframe打开
    document.getElementById('iframe').src = url;

    // 移动端浏览器中,将下载页面显示出来
    if(!isWeixin && !isQQ && !isYixin && !isYx){
        document.querySelector('.main-body').style.display = 'block'
        if(isIos9){
            document.querySelector('.main-body').classList.add('showtip')
        }

        setTimeout(function(){
            document.body.scrollTop = 0
        },200)
    }else{
        document.getElementById('guide').style.display = 'block'
    }

    // Forward To Redirect Url
    // Add by zhanzhixiang 12/28/2015
    if (params.redirect) {
        var redirectUrl = decodeURIComponent(params.redirect);
        if ( typeof(URL) === 'function' && new URL(redirectUrl).hostname.search("163.com") !== -1) {
            window.location.href = redirectUrl;
        } else if (redirectUrl.search("163.com") !== -1){
            window.location.href = redirectUrl;
        };
    }

    // Forward To Redirect Url End
    if ((isWeixin || isQQ) && isAndroid) {
        window.location.href = 'http://a.app.qq.com/o/simple.jsp?pkgname=com.netease.newsreader.activity&ckey=CK1331205846719&android_schema=' + url.match(/(.*)\?/)[1]
    }

    if(isIos||isIpad){
        document.getElementById("guide").classList.add('iosguideopen')
    }else if (isAndroid){
        document.getElementById("guide").classList.add('androidguideopen')
    }else{
        // window.location.href = 'http://www.163.com/newsapp'
    }

    document.getElementById('link').addEventListener('click', function(){

        // 统计
        neteaseTracker && neteaseTracker(false,'http://sps.163.com/func/?func=downloadapp&modelid='+modelid+'&spst='+spst+'&spsf&spss=' + channel,'', 'sps' )

        if (config) {
            android_url = config.android
        }
        if (config && config.iOS) {
            ios_url = config.iOS
        }
        if(isWeixin || isQQ){
            return
        }
        var msg = isIDeviceIpad ? "检测到您正在使用iPad, 是否直接前往AppStore下载?" : "检测到您正在使用iPhone, 是否直接前往AppStore下载?";
        if (isIDevice){
            window.location = ios_url;
            return;
        }else if(isAndroid){
            // uc浏览器用iframe唤醒
            if(navigator.userAgent.match(/ucbrowser|yixin|MailMaster/i)){
                document.getElementById('iframe').src = url;
            } else {
                window.location.href = url;
            }
            setTimeout(function(){
                if(document.webkitHidden) {
                    return
                }
                if (confirm("检测到您正在使用Android 手机,是否直接下载程序安装包?")) {
                    neteaseTracker && neteaseTracker(false,'http://sps.163.com/func/?func=downloadapp_pass&modelid='+modelid+'&spst='+spst+'&spsf&spss=' + channel,'', 'sps' )
                    window.location.href = android_url;
                } else {
                    neteaseTracker && neteaseTracker(false,'http://sps.163.com/func/?func=downloadapp_cancel&modelid='+modelid+'&spst='+spst+'&spsf&spss=' + channel,'', 'sps' )
                }
            },200)
            return;
        }else if(isIEMobile){
            window.location = wphone_url;
            return;
        }else{
            window.open('http://www.163.com/special/00774IQ6/newsapp_download.html');
            return;
        }
    }, false)

    setTimeout(function(){
        if(isIDevice && params.notdownload != 1 && !isNewsapp && !isIos9){
            document.getElementById('link').click()
        }
    }, 1000)

})(window,document);





在移动互联网,链接是比较重要的传播媒质,但很多时候我们又希望用户能够回到APP中,这就要求APP可以通过浏览器或在微信中被方便地唤起。

这是一个既直观又很好的用户体验,但在实现过程中会遇到各种问题:

  1. 如何解决未安装APP时的做好引导页
  2. 如何在微信中唤醒APP
  3. 在iOS9中如何处理universal link被用户误关的情况
  4. 如何解决Android各种机型、各种第三方浏览器导致的兼容问题等
  5. 在APP未安装情况下,引导用户下载后打开APP后,如何进入之前唤起时指定的页面或内容,即如何实现场景还原
  6. 在微信中唤醒APP时,如何进入指定的页面或内容

下面是我一些个人的经验分享。

浏览器中打开

iOS/Android APP配置

这块内容其实比较简单,在网上都有很多资料可供查阅,就不再赘述。

原理说明

首先需要说明,不管iOS还是Android,浏览器都不可能预知本地是否安装了某个APP的。或者更严谨地说,我们不能通过浏览器来预知本地是否安装。因为就算浏览器可以读取本地应用的安装列表,但是目前也没任何一家浏览器提供查询的API,所以这条路是走不通的。

本质上浏览器是通过URL scheme打开APP,一个APP可以设置一个或多个打开自己的URL scheme。比如,Twitter就注册自己能被「twitter://」打开。

其实,如果是做APP间相互跳转是比较简单的。iOS就可以使用 UIApplication 的 canOpenUrl 方法来检测URL scheme 是否能打开对应的APP。比如,如果「twitter://」检测能被打开,也就说明本地安装了 Twitter 。再用 UIApplication 的 openURL 方法,就能打开Twitter了。Android 中的做法类似。

实现方案

因为iOS9和之前的iOS系统有区别,所以这里我们也要区别对待。

iOS7/iOS8

iOS中默认通过Safari打开URL scheme,方法一般如下两种:

  1. 直跳方式:点击链接、修改 window.location 等。
  2. iframe 方式:在 body 上添加 iframe,设置src属性为跳转的URL scheme。

第一种情况:

<a href="schemeUrl">唤醒你的APP</a>

或者

window.location.href = schemeUrl;

但在第一种情况,如果APP唤醒失败,或者APP未安装的话,很多时候都会跳到错误页,这很影响用户体验,而我们的要求可能是跳转到其他页面或者下载APP。

后一种方法不会引起页面可见的变化(例如页面内容变成一个新页面),不会导致浏览器历史记录的变化,大致实现如下:

<a href="APP下载地址">下载或打开APP</a>
<script>
$('a').click(function() {
    var ifr = document.createElement('iframe');
    ifr.src = '自定义 URL scheme';
    ifr.style.display = 'none';
    document.body.appendChild(ifr);
    setTimeout(function(){
        document.body.removeChild(ifr);
    }, 3000);
});
</script>

过程是这样:点击 a 标签时,首先会尝试打开URL scheme,如果成功,就唤起APP;如果失败,则跳转到 href 属性,即下载页。

Android

但这个方案在很多安卓机型上有问题,为保证可用,改用第一种方案:

$('a').click(function() {
    location.href = '自定义 URL scheme';
    t = Date.now();
    setTimeout(function(){
        if (Date.now() - t < 1200) {
            location.href = 'Android 下载地址';
        }
    }, 1000);
    return false;
}

理想过程是这样:浏览器尝试打开 URL scheme,在1秒计时后,检查当前时间,如果实际时间已过 1200 毫秒,说明唤起APP 成功(唤起 APP 会让浏览器的定时器变慢);如果没超过 1200 毫秒,很可能是没有安装应用,就跳到下载地址。

或者换种方式:

var ifr = document.createElement('iframe');
ifr.src = 'com.baidu.tieba://';
ifr.style.display = 'none';
document.body.appendChild(ifr);
var openTime = +new Date();
window.setTimeout(function(){
    document.body.removeChild(ifr);
    if( (+new Date()) - openTime > 2500 ){
        window.location = 'http://exam.com/xxxx.apk';
    }
},2000)

但原理都是一样,利用setTimeout。但这其实不稳定,因为Android是基于Linux的分时多任务的,setTimeout的基准偏差可能会没那么大。

但如果设置比较小的运行间隔(<30ms),在浏览器或者webview中,应用切换到后台,setInterval会被很明显的延迟执行,比如设置一个运行间隔20ms,总计运行100次的定时器,如果页面一直处于前台,则100次跑完,总耗时与 100x20=2000ms不会有太大差异,但页面在后台运行时,此时间会明显超过2000ms。可以利用这一点来实现是否成功打开APP检测及回调。

function openApp(openUrl, appUrl, action, callback) {
    //检查app是否打开
    function checkOpen(cb){
        var _clickTime = +(new Date());
        function check(elsTime) {
            if ( elsTime > 3000 || document.hidden || document.webkitHidden) {
                cb(1);
            } else {
                cb(0);
            }
        }
        //启动间隔20ms运行的定时器,并检测累计消耗时间是否超过3000ms,超过则结束
        var _count = 0, intHandle;
        intHandle = setInterval(function(){
            _count++;        
            var elsTime = +(new Date()) - _clickTime;
            if (_count>=100 || elsTime > 3000 ) {
                clearInterval(intHandle);
                check(elsTime);
            }
        }, 20);
    }
    
    //在iframe 中打开APP
    var ifr = document.createElement('iframe');
    ifr.src = openUrl;
    ifr.style.display = 'none';
    if (callback) {
        checkOpen(function(opened){
            callback && callback(opened);
        });
    }
    
    document.body.appendChild(ifr);      
    setTimeout(function() {
        document.body.removeChild(ifr);
    }, 2000);  
}

另外,可以通过 document.hidden 或 document.[webkit|moz|ms]Hidden 来判断页面是否被置入后台(即应用被唤起),或visibilitychange事件,但对于Android 4.4版本一下则不支持。

iOS9

在 iOS 9 上,iframe 方案变得不可用。
按不能使用之前Android的代码,因为在打开自定义 URL scheme 时,会弹出对话框,询问是否用 xx 应用来打开。往往用户还没来得及点击打开,定时器又触发了,导致跳到 App Store。

可以在尝试打开URL scheme 后,再加一个页面跳转,这样对话框会被覆盖,再刷新页面,就能无需确认唤起APP:

$('a').click(function() {
    location.href = '自定义 URL scheme';
    location.href = '下载页';
    location.reload();
}

这里,下载页延时 2 秒跳转到 App Store。

APP已安装这是没问题的,但如果APP未安装,跳 App Store 的请求会失败。
这时可以使用两个定时器:

$('a').click(function() {
    location.href = '自定义 URL scheme';
    setTimeout(function() {
        location.href = '下载页';
    }, 250);
    setTimeout(function() {
        location.reload();
    }, 1000);
}

不过在iOS9中其实是支持universal link的,就是一个http域名形式,在微信中都可以唤起APP。如果未安装的话,可以直接引导用户去APP store下载。

可以参考这篇文章

http://www.magicwindow.cn/doc/#universal-link-info

没有完美的解决方案

主要是在安卓上,总归会有各种兼容问题,知乎的解决办法是,提供两个按钮,一个下载,一个打开APP,让用户自己选。

微信中打开

因为微信将唤起本地APP的接口给禁了,所以微信中是不能直接唤起APP的,一般做法是提示用户在浏览器中打开,之后的流程还是我们上面讲的内容。

但是,在iOS9中,这个限制是可以突破的,也就是说可以直接唤起APP。方法就是使用我们上文提到的universal link。

在Android和iOS8及其以下系统中,我们可以利用腾讯的亲儿子:应用宝。简单讲,就是把你的唤起地址配置成你APP的应用宝地址,微信中跳转到这个地址后,如果用户已经安装了APP,则可直接唤起,如果没有安装,则可直接点击下载,如下图示:

但这里有坑需要注意。

对于使用universal link来说,如下图所示用户在微信中打开APP之后,可能不小心点击右上角的链接(比方说点几分享,却不小心点击了"mlinks.cc"),导致跳到外部浏览器中,如下图所示:

这时候再在微信中就打不开APP了,因为universal link已被关闭,这是iOS9的机制,没法改变,这时候用户再在微信中打开,就得需要一个中间页来引导用户在外部浏览器中打开APP,如下图所示:

另外,在微信中唤醒APP默认只能到达首页,即不能到达指定页面或内容,如果想要做,则需要额外的处理。

拿来主义

从以上内容可以总结出:要做一个兼容性很好的方案,就需要考虑各种情况,在不同的情况适配不同的方案,比方说用户是在手机浏览器打开还是微信中打开,或者是在pc中打开,universal link是否被关闭等,这就使代码实现变得复杂,且容易出错,且还有安卓平台机型众多、浏览器众多等导致的兼容问题。

如果觉得实现难度或者成本太高,你可以考虑使用魔窗的mLink。只要你加了魔窗的sdk,就可以通过类似“https://s.mlinks.cc/AA01”的链接,在任何环境下打开你的APP(如果在pc机上打开,浏览器中将会出现APP下载地址的二维码),上面提到的问题都不复存在,并且魔窗已经兼容超过600台以上安卓机型的第三方主流浏览器。而且关键的是,不管是在手机浏览器中,还是在微信中打开,你可以指定唤起APP后直达APP中的某个页面或内容(某个促销商品等),就算用户没安装APP,点击下载安装之后,再打开,还是跳转到指定的页面,这就是场景还原,或者叫做Deffered Deep Linking。



©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页