外部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

  • 每一个ios中的APP都在安装的时候可以自定义URL Scheme,开头类似 zhihu:// ,至于怎么配置,就请各位百度一下吧,本文只讨论前端如何使用URL Scheme。
  • 配置的注意点:
    定义的时候不要和其他app冲突而且也不要和原生app冲突,因为在app安装的时候,系统就注册了你的URL Scheme要是和原生app冲突的话,原生的优先级比其他的高,会调起原生的而不是你的。安卓同理,不过是协议不同而已,这个需要我们ios和安卓的小伙伴们提供。
  • 使用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', '');
          document.body.appendChild(ifm);
          onBeforeWakeup && onBeforeWakeup();
          var currentTime = Date.now();
          setTimeout(function() {
              var nowTime = Date.now();
              if (nowTime - currentTime < 1050) {
                  onWakeupFail && onWakeupFail();
          }, 1000);
    
  • 发起Scheme跳转
  • 如果按照了App,会成功打开。如果未安装App,会打开失败,没任何效果
  • 延迟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特有的。

  • 踩坑记(前辈们) 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端中:

    UC、微博、头条、Safari都支持universalLink或deeplink的唤醒方式
  • QQ浏览器竟然Universal Link无效,而URL Scheme有效。额,这就有点尴尬了。(测试机ios12 QQ浏览器版本8.8.2.3990)。
  • 微信端,微信的话必须得调用他自己的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 || ''; 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', ''); 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,目前还没找到好的文档

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

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

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

  • 使用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;
    

    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的话,则会有以下提示框,之后,就会跳转到下载页面。很不友好耶。。

    兼容性总结:

    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