美文网首页iOS
外部app唤醒app踩坑记录

外部app唤醒app踩坑记录

作者: 伍超波 | 来源:发表于2019-07-23 09:41 被阅读0次

    时间:2018-10-18

    URL Scheme: zhihu://
    Universal Link: https://oia.zhihu.com/****
    安卓端intent协议

    前记

    最近接到一个需求,需要在投放到外部app的页面中支持唤醒app的功能,未安装app就跳到APP Store或者安卓端调用下载app应用。
    这道这个需求的时候,安利了好久。网络上对于前端控制唤醒的app的方式也有很详尽的解释。本文就简单介绍一下URL Scheme唤醒方式和universal Link方式及其优缺点。重点描述我在开发过程中的踩坑点。
    好了,说了这么多,开始切入正题吧。

    在这个万物互联的时代

    虽然不能让app之间相互通信,但是可以让app之间相互唤起,并且通过传递参数的形式简介单向通信,前提是你已经安装了该应用。要是没有安装的话就跳转到下载

    唤醒方式

    1. URL Scheme

    1. 每一个ios中的APP都在安装的时候可以自定义URL Scheme,开头类似zhihu://,至于怎么配置,就请各位百度一下吧,本文只讨论前端如何使用URL Scheme。
    2. 配置的注意点:
      定义的时候不要和其他app冲突而且也不要和原生app冲突,因为在app安装的时候,系统就注册了你的URL Scheme要是和原生app冲突的话,原生的优先级比其他的高,会调起原生的而不是你的。安卓同理,不过是协议不同而已,这个需要我们ios和安卓的小伙伴们提供。
    3. 使用URL Scheme中遇到的坑,有些前任踩过也分享过了。
      • Scheme无法判断是否安装了APP
        这种弊端的出现是因为浏览器是没有能力判断是否安装了App的,当Scheme没有唤醒的时候,用户会继续留存在该网页不会做任何的操作。
        所以,看到这,我们伟大的程序员哥哥就想出了一个方法
      // 唤醒失败样例函数
      function onWakeupFail() {
        if (ua.isIOS()) {
            location.href = 'https://itunes.apple.com/us/app/idxxxxxxx?mt=8'
        } else {
            location.href='https://xxx.xxx.xxx/xxx/xxx.apk';//直接apk下载link
        }
      }
      function openByScheme(wakeupUrl, onBeforeWakeup, onWakeupFail) {
            var ifm = document.createElement('iframe');
      
            ifm.setAttribute('src', wakeupUrl);
            ifm.setAttribute('style', 'display:none');
            document.body.appendChild(ifm);
            onBeforeWakeup && onBeforeWakeup();
      
            var currentTime = Date.now();
      
            setTimeout(function() {
                var nowTime = Date.now();
                if (nowTime - currentTime < 1050) {
                    onWakeupFail && onWakeupFail();
                }
            }, 1000);
      }
      
      这时,
      1. 发起Scheme跳转
      2. 如果按照了App,会成功打开。如果未安装App,会打开失败,没任何效果
      3. 延迟1000ms执行的意义
        • 如果没有安装,Scheme打开失败,等待1000ms之后,自动去下载
        • 如果安装了App,App会打开,当前页面会被暂停,后面的延迟代码就会被阻断,不会执行。但是,这里有个问题就是,当再次返回到这个页面时,那段被阻断的延迟代码就会执行了,安卓会跳出下载apk包的提示,IOS会再度跳到Appstore。是不是体验很差,唤醒成功之后,再次进来竟然让我去下载?不过这个是目前最佳的解决办法了,忍一下让用户多个点击取消的操作,应该估计可能还能勉强接受。不过嘛,这个也是有解决办法的,例如根据页面的显示和隐藏,当监听到离开该页面,即跳去其他APP的时候,就清除掉setTimeout函数,这样返回来的时候就不会再次执行了:
        /**
         * 监听页面的显隐来判断是否唤起成功
         * 当唤起成功的时候,会离开该页面,此时去掉deeplink唤起的setTimeout
         * 防止唤起之后,又返回浏览器继续执行setTimeout,引导用户下载
         */
         function attachDocumentHide() {
           // eslint-disable-next-line
           let hiddenProperty = 'hidden' in document ? 'hidden' : ('webkitHidden' in document ? 'webkitHidden' : ('mozHidden' in document ? 'mozHidden' : null));
             let visibilityChangeEvent = hiddenProperty.replace(
                 /hidden/i,
                 'visibilitychange'
             );
             let onVisibilityChange = function() {
                 if (document[hiddenProperty]) {
                     console.log('页面隐藏了');
                     deepLinkTimeout && clearTimeout(deepLinkTimeout);
                 }
             };
             document.addEventListener(visibilityChangeEvent, onVisibilityChange);
         }
        
      • Scheme被很多的App禁止了,例如微信和百度浏览器,QQ浏览器。因为他们出于留存用户的考量,不希望用户看到分享的内容的时候就跳出App到其他应用了,所以就拦截了所有的Scheme,此时就无法通过Scheme唤醒App。
        解决办法也很简单,就是嗅探一下浏览器类型,如果浏览器上方有个'...'图标的话,指示用户跳到系统/外部浏览器打开
      • 使用了URL Scheme系统会唤起一个弹窗“是否打开***”。这样跳转不太流畅,所以建议在ios9以上的系统中使用 universalLink的方式唤醒,直接唤醒到App里面具体的页面

    2. Universal Link

    这里先说明一下为什么苹果在WWDC2015推出Universal Link。因为Universal Link将一个正常的url访问方式赋予了唤醒的功能,前提是你在App应用中配置apple-app-association。同时解决了上文提到的Scheme的前两个弊端。所以,建议广大开发们迎接新技术,在IOS9以上使用Universal Link,而且在ios9以上系统,已经不支持URL Scheme的方式了。安卓的话就使用URL Scheme吧,这项技术是IOS特有的。

    1. 踩坑记(前辈们)
      • 跨域
        Universal Link,必须要求跨域,如果不跨域,就不行,就失效,就不工作。(iOS 9.2之后的改动,苹果就这么规定这么设计的)
        例如:
        假如当前网页的域名是 A
        当前网页发起跳转的域名是 B
        必须要求 B 和 A 是不同域名,才会触发Universal Link
        如果B 和 A 是相同域名,只会继续在当前WebView里面进行跳转,哪怕你的Universal Link一切正常,根本不会打开App
        所以,一般使用Universal Link唤醒App的公司都有一个域来专门做universal link唤醒域。
     + 当链接跳转的页面在WAP不存在和APP存在时
     例如:WAP和APP功能差异非常大,除了公共的功能外,其他的功能WAP是WAP的,APP是APP的,形态和场景都有明显差异。他只需要跳转到APP,他没有合法的```WAP Url```可以让浏览器在没有安装App的情况下继续跳转。我们选择的Universal Link的域名其实是一个没有实际页面的域名,也就是说```https://xxx.xxx.xxx/view/*```这个url,如果没安装APP因此触发WebView继续跳转原地址,会直接404。
     所以,可以通过重定向来设置,在url的hash参数中加入refer=redirectUrl,后端获取到这个参数就重定向到那个页面。
    

    我的踩坑记录

    毕竟每家厂商考虑的东西不一样,有些能支持universalLink的就使用universalLink,否则就使用deeplink
    目前ios端中:

    1. UC、微博、头条、Safari都支持universalLink或deeplink的唤醒方式
    2. QQ浏览器竟然Universal Link无效,而URL Scheme有效。额,这就有点尴尬了。(测试机ios12 QQ浏览器版本8.8.2.3990)。
    3. 微信端,微信的话必须得调用他自己的wx-sdk工具才能唤醒外部引用,安卓和微信端都是。
      安卓端的话:
      由于同一使用URL Scheme的情况,所以不用考虑QQ浏览器的情况了,但是遇到个问题是某些浏览器URL Scheme唤醒不成功的时候弹出下载apk包的提示,但是,但是,此时用户已经安装了App了。这特么怎么办?只能说某些国产浏览器真的厉害了

    代码走一波

    1. 非微信端

    /*
     * APP唤醒模块
     */
    var testAgent =  function(agentRegEx) {
        return function() {
            return agentRegEx.test((window.navigator && navigator.userAgent) || '')
        }
    }
    var ua = {
        isSafari: testAgent(/webkit\W(?!.*chrome).*safari\W/i),
        isIOS: testAgent(/(ipad|iphone|ipod)/i),
        isWechat: detect(/micromessenger/i),
        isUC: testAgent(/uc browser|ucbrowser|ucweb/i)
    }
    
    var wakeupApp = {
        /**
         * 唤醒APP,无法知道是否唤醒成功
         * @param {String} {wakeupUrl} 唤醒参数 url唤起的链接
         * @param {Function} onBeforeWakeup 唤醒前执行
         * @param {Function} onWakeupFail deeplink唤醒失败后执行
         */
        wakeup: function(wakeupUrl, onBeforeWakeup, onWakeupFail) {
            var iPhoneVersion = navigator.userAgent.match(
                /OS ([\d]+)_\d[_\d]* like Mac OS X/i
            );
            var wakeupUrl = wakeupUrl || '';
            if (
                ua.isIOS() &&
                (ua.isSafari() || ua.isUC()) &&
                iPhoneVersion &&
                iPhoneVersion[1] >= 9
            ) {
                this._openByUniversalLink(
                    wakeupUrl,
                    onBeforeWakeup,
                    onWakeupFail
                );
            } else {
                this._openByIframe(wakeupUrl, onBeforeWakeup, onWakeupFail);
            }                   
        },
        /**
         * universal_link唤起
         * @param wakeupUrl
         * @param onBeforeWakeup
         * @param onWakeupFail
         */
        _openByUniversalLink: function(wakeupUrl, onBeforeWakeup, onWakeupFail) {
              // 此处不用调用onWakeupFail函数了,因为Universal Link跳转后端会做一个重定向,不会访问到404的页面
            onBeforeWakeup && onBeforeWakeup();
            wakeupUrl = wakeupUrl.replace(
                'zhihu://',
                '//oia.zhihu.com/'
            );
            location.href = wakeupUrl;
        },
        /**
         * iframe唤起
         * @param wakeupUrl
         * @param onBeforeWakeup
         * @param onWakeupFail
         * @private
         */
        _openByIframe: function(wakeupUrl, onBeforeWakeup, onWakeupFail) {
            var ifm = document.createElement('iframe');
    
            ifm.setAttribute('src', wakeupUrl);
            ifm.setAttribute('style', 'display:none');
            document.body.appendChild(ifm);
            onBeforeWakeup && onBeforeWakeup();
    
            var currentTime = Date.now();
    
            setTimeout(function() {
                var nowTime = Date.now();
                if (nowTime - currentTime < 1050) {
                    onWakeupFail && onWakeupFail();
                }
            }, 1000);
        }
    };
    

    2. 微信

    微信的话,可能为了用户体验或者提高自身存在感,自己搞了套唤醒的js。要是想在微信唤醒外部app的话得使用wx-sdk。
    其次,跟微信打交道的话,我想离不开WeixinJSBridge这个微信浏览器里挂载到window里的对象,以及wx-sdk的sdk工具。两者功能基本一致,只是使用上有点区别。而wx-sdk主要是在外部App中使用的,WeixinJSBridge是针对微信中的h5页面的。
    关于如何使用wx-sdk.js 请各位小伙伴查看官方文档:
    https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115
    关于WeixinJSBridge,目前还没找到好的文档

    1. 引入js

    http://res.wx.qq.com/open/js/jweixin-1.4.0.js

    1. 注入配置权限
    wx.config({
        debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
        appId: '', // 必填,公众号的唯一标识
        timestamp: , // 必填,生成签名的时间戳
        nonceStr: '', // 必填,生成签名的随机串
        signature: '',// 必填,签名
        jsApiList: [] // 必填,需要使用的JS接口列表
    });
    

    注意这里的appid需要你去官网注册,而signature需要后台根据签名算法动态生成的

    1. 使用WeixinJSBridge.invokelaunchApplication需要注意的是微信版本大于6.5.16才能支持WeixinJSBridge
    function invokeLaunchApp(opts) {
         const invoke = () => {
             const conf = {
                 appID: '填写申请的appid',
                 schemeUrl: opts.schemeUrl
                 // // 自定义 scheme URL 中的 path 部分,for iOS
                 // parameter: opts.parameter,
                 // // 格式可以自定义,第三方 APP 自主处理,可以是 path 或 json,for Android
                 // extInfo: opts.extInfo
             };
    
             /* eslint-disable */
             // WeixinJSBridge 是微信环境下(真实环境或微信开发者工具下)window 下的对象
             WeixinJSBridge &&
                 WeixinJSBridge.invoke &&
                 WeixinJSBridge.invoke('launchApplication', conf, opts.onLaunch);
             /* eslint-enable */
         };
         setTimeout(invoke, 0);
     }
     function wakeup(opts) {
         if (window.WeixinJSBridge) {
           invokeLaunchApp(opts);
       } else {
           document.addEventListener('WeixinJSBridgeReady', () => {
               this.invokeLaunchApp(opts);
           });
       }
     }
     // 调用
     wechatWakeupApp.wakeup({
         schemeUrl: url,
         onLaunch: handleLaunch
     });
     const handleLaunch = res => {
         switch (res && res.err_msg) {
             case 'launchApplication:ok':
                 break;
             case 'launchApplication:fail':
                 // '打开失败,请检查是否已安装APP'
                 break;
             case 'launchApplication:fail_check fail':
                 // '调用 app 权限校验失败'
                 break;
             default:
                 // 显示res.err_msg
                 break;
         }
     };
    
    1. 完工

    2018-11-29 补充:
    在一次版本迭代的过程中,使用京东商城作为我们的模板对象,研究京东是如何唤醒的,过程中发现IOS12以后再Safari唤醒的时候,当系统感知到universallink的时候,会自动调起系统的选择框,而IOS12一下不会存在该选择框:


    image.png

    如图所示,发现个坑,当点击取消的时候,IOS12下不会继续请求universal link这个链接,即可以留着当前页了。但是IOS12的时候就会继续访问这个universal link链接,要是这个链接没有匹配的页面,就会有报错的风险。所以,针对这个,一些使用302重定向方式唤醒APP的技术,需要考虑适配IOS12这种情况。
    而京东商城触发该唤醒逻辑是使用模拟点击的方式:
    代码来源京东线上:

    setTimeout(function() {
            var e = document.createElement("a");
            // 此处的a,最好是URL Scheme的连接,如果是Universal Link的话,点击取消的时候会(IOS12下)会继续访问universal Link的连接。
            // 例如: a = 'jindongshop://goHome'。只是个虚拟例子,不能成功访问的。
            e.setAttribute("href", a);
            e.style.display = "none";
            document.body.appendChild(e);
            var t = document.createEvent("HTMLEvents");
            t.initEvent("click", !1, !1);
            e.dispatchEvent(t)
        }, 0)
    

    而如果没有安装京东APP的话,则会有以下提示框,之后,就会跳转到下载页面。很不友好耶。。


    微信图片_20181129145950.jpg

    兼容性总结:

    Android系统:Chrome for Android无法通过iframe方式来调用scheme,而通过a链接的方式可以成功调用,而针对Chrome内核的浏览器如360浏览器,对于iframe和a链接的方式都能支持,所以对Chrome内核的浏览器采用a链接的方式来调用scheme;对于其他浏览器,如UC,QQ浏览器则采用iframe方式调用scheme。

    iOS系统:Safari浏览器不支持 iframe可直接做页面跳转;对于UC、Chrome、QQ只能通过a链接方式调用scheme。

    2019-07-09

    安卓UC浏览器使用scheme方式换不起APP

    原因:安卓会在scheme前自动加上http:// ?

    最后感谢一下网站作者:(写的很详细也很好,果断mark)
    1、Universal Link 前端部署踩坑记:Universal Link 前端部署采坑记
    2、包含intend唤起的解释:如何唤起APP

    相关文章

      网友评论

        本文标题:外部app唤醒app踩坑记录

        本文链接:https://www.haomeiwen.com/subject/wftszftx.html