外部app唤醒app踩坑记录

时间: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

推荐阅读更多精彩内容