H5唤起App

最近总接到落地页的需求,落地页的职责主要是引流,有以下几种类型
1、引导已经下载App的用户打开App
2、引导未下载App的用户下载App
3、引导未注册的用户注册
4、引导已经注册的用户进入我们的主页或者其他的操作
从数据上可以体现在用户停留在App的时间多了,或者增加了用户量

唤起App主要的媒介是什么呢?

URL Scheme

URL Scheme的组成

1
[scheme:][//authority][path][?query][#fragment]
App 微信 支付宝 淘宝 微博
URL Scheme weixin:// alipay:// taobao:// sinaweibo://
1
2
3
4
5
     行为(应用的某个功能)    
|
scheme://[path][?query]
| |
应用标识 功能需要的参数

URL Scheme 在 ios 9+ 上诸如 safari、UC、QQ浏览器中, iframe 均无法成功唤起 APP,只能通过 window.location 才能成功唤端。

Intent

安卓的原生谷歌浏览器自从 chrome25 版本开始对于唤端功能做了一些变化,URL Scheme 无法再启动Android应用。 例如,通过 iframe 指向 weixin://,即使用户安装了微信也无法打开。所以,APP需要实现谷歌官方提供的 intent: 语法,或者实现让用户通过自定义手势来打开APP。
安卓版本 4.4.4 以上机型的安卓自带浏览器、chrome 浏览器,需要通过 intent 跳转
intents文档

  • Intent 语法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    intent:
    HOST/URI-path // Optional host
    #Intent;
    package=[string];
    action=[string];
    category=[string];
    component=[string];
    scheme=[string];
    end;
    如果用户未安装 APP,则会跳转到系统默认商店。当然,如果你想要指定一个唤起失败的跳转地址,添加下面的字符串在 end; 前就可以了:
    1
    S.browser_fallback_url=[encoded_full_url]
  • 示例
    下面是打开 Zxing 二维码扫描 APP 的 intent。
    1
    2
    3
    4
    5
    6
    intent:
    //scan/
    #Intent;
    package=com.google.zxing.client.android;
    scheme=zxing;
    end;
    1
    <a href="intent://scan/#Intent;scheme=zxing;package=com.google.zxing.client.android;S.browser_fallback_url=http%3A%2F%2Fzxing.org;end"> Take a QR code </a>

传统的 Scheme 链接有以下几个痛点:

  • 在 ios 上会有确认弹窗提示用户是否打开,对于用户来说唤端,多出了一步操作。若用户未安装 APP ,也会有一个提示窗,告知我们 “打不开该网页,因为网址无效”
  • 传统 Scheme 跳转无法得知唤端是否成功,Universal Link 唤端失败可以直接打开此链接对应的页面
  • Scheme 在微信、微博、QQ浏览器、手百中都已经被禁止使用,使用 Universal Link 可以避开它们的屏蔽( 截止到 18年8月21日,微信和QQ浏览器已经禁止了 Universal Link,其他主流APP未发现有禁止 )

有大量的文章会详细的告诉我们如何配置,你也可以去看官方文档,我这里简单的写一个12345。

  1. 拥有一个支持 https 的域名
  2. 开发者中心 ,Identifiers 下 AppIDs 找到自己的 App ID,编辑打开 Associated Domains 服务。
  3. 打开工程配置中的 Associated Domains ,在其中的 Domains 中填入你想支持的域名,必须以 applinks: 为前缀
  4. 配置 apple-app-site-association 文件,文件名必须为 apple-app-site-association不带任何后缀
  5. 上传该文件到你的 HTTPS 服务器的 根目录 或者 .well-known 目录下

这里放一下我们在配置过程中遇到的坑,当然首先你在配置过程中必须得严格按照上面的要求去做,尤其是加粗的地方。

  • 域名问题

Universal Link 支持的域名最多只能支持到二级域名,如果你用到了三级域名,Universal Link 唤端是不会生效的。

  • 跨域问题

IOS 9.2 以后,必须要触发跨域才能支持 Universal Link 唤端。

IOS 那边有这样一个判断,如果你要打开的 Universal Link 和 当前页面是同一域名,ios 尊重用户最可能的意图,直接打开链接所对应的页面。如果不在同一域名下,则在你的 APP 中打开链接,也就是执行具体的唤端操作。

  • Universal Link 是空页面

Universal Link 本质上是个空页面,如果未安装 APP,Universal Link 被当做普通的页面链接,自然会跳到 404 页面,所以我们需要将它绑定到我们的中转页或者下载页。

唤端方式

  • Android中,不同浏览器对唤起APP有严重的兼容性问题,主要处理方案有以下几种:
    1、window.location.href
    2、通过创建 iframe 并为其 src 赋值(主要)
    3、通过 intent
    4、通过制造不可见 a 链接,并触发点击

  • ios中,不同浏览器对唤起APP有严重的兼容性问题,主要处理方案有以下几种:
    1、系统版本在 8 以下时,可以监听页面的 pagehide / visibilitychange 事件。
    2、 window.location.href (主要)
    系统版本大于 8 以后可以 URL scheme 进行跳转。 IOS9 可以使用 universal Link

这里我们结合了两种来处理。

判断唤端是否成功

在浏览器实际上是没有能力判断手机里是否安装了某个App的,所以只能够采取一种投机取巧的方式。APP 如果被唤起的话,页面就会进入后台运行。setInterval 在 ios 中不会停止运行,在 android 中停止运行。

  • ios 通过 document.hidden 和 document.webkitHidden 属性来判断 APP 在 ios 中是否被正常唤起,2000ms 内,页面转入后台运行,document.hidden 会返回 true,代表唤端成功,反之则代表失败。
  • Android 我们的判断条件比预期时间多设置了 500ms,所以如果安卓中 setInterval 内的函数执行 100 次以内所费时间超过 2500ms,则说明 APP 唤起成功,反之则代表失败。
    在JavaScript中判断页面是否进入后台来判断打开成功。Html5提供了下列事件和属性可以利用:
    pagehide : 页面隐藏时触发
    visibilitychange : 页面隐藏没有在当前显示时触发(切换tab也会触发该事件)
    document.hidden : 当页面隐藏时,该值为true,显示时为false
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const initialTime = new Date();
    let counter = 0;
    let waitTime = 0;
    const checkOpen = setInterval(() => {
    count++;
    waitTime = new Date() - initialTime;
    if (waitTime > 2500) {
    clearInterval(checkOpen);
    cb();
    }
    if (counter < 100) return;
    const hide = document.hidden || document.webkitHidden;
    if (!hide) {
    cb(); // 唤端失败的回调函数
    }
    }, 20);

如果唤端失败(APP 未安装),我们总是要做一些处理的,可以是跳转下载页,可以是 ios 下跳转 App Store… 但是Js 并不能提供给我们获取 APP 唤起状态的能力,Android Intent 以及 Universal Link 倒是不用担心,它们俩的自身机制允许它们唤端失败后直接导航至相应的页面,但是 URL Scheme 并不具备这样的能力,所以我们只能通过一些很 hack 的方式来实现 APP 唤起检测功能。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
const browser = {
getBrowser: function() {
var u = navigator.userAgent,
app = navigator.appVersion;
return {
trident: u.indexOf('Trident') > -1, //IE内核
presto: u.indexOf('Presto') > -1, //opera内核
webKit: u.indexOf('AppleWebKit') > -1, //苹果、谷歌内核
gecko: u.indexOf('Gecko') > -1 && u.indexOf('KHTML') == -1, //火狐内核
opera: u.indexOf('Opera') > -1,
chrome: u.indexOf('Chrome') > -1,
firefox: u.indexOf('Firefox') > -1,
safari: u.indexOf('Safari') > -1, //注意chrome浏览器此项也为true,非chrome且此项为true则可确定为Safari
ie: u.indexOf('compatible') > -1 && u.indexOf('MSIE') > -1 && u.indexOf('Gecko') == -1,
mobile: u.search(/AppleWebKit.*Mobile/) > -1, //是否为移动终端
ios: u.search(/\(i[^;]+;( U;)? CPU.+Mac OS X/) > -1, //ios终端
android: u.indexOf('Android') > -1 || u.indexOf('Adr') > -1, //android终端
winPhone: u.search(/Windows Phone/) > -1, //windows phone终端
iPhone: u.indexOf('iPhone') > -1, //是否为iPhon
iPad: u.indexOf('iPad') > -1, //是否iPad
webApp: u.indexOf('Safari') == -1, //是否Safari web应该程序,没有头部与底部
weibo: u.search(/WeiBo/i) > -1, //是否微博
weixin: u.search(/MicroMessenger/i) > -1, //是否微信
qq: u.search(/\sQQ/i) > -1, //是否QQ
mQQ: u.search(/MQQBrowser/) > -1, //是否QQ手机浏览器
uc: u.search(/UCBrowser/) > -1 //是否UC浏览器
};
}(),
getIOSVersion: function getIOSVersion() {
const verion = navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/);
return parseInt(verion[1], 10);
}()
}
/**
* [evokeApp 唤起APP]
* @param {[Object]} config [跳转的URL]
* config.ios.url {[String]} [iOS跳转的URL]
* config.ios.data {[String]} [iOS URL参数]
* config.ios.callback {[function]} //android的回掉如果没有下载app就执行操作
* config.android.url {[String]} [android跳转的URL]
* config.android.data {[String]} [android URL参数]
* config.android.callback {[function]} //android的回掉如果没有下载app就执行操作
*/
const evokeApp = (config) =>{
//对微信,微博,qq做处理弹窗
if (browser.versions.weibo || browser.versions.weixin || browser.versions.qq) {
return;
}
//
let browserVersions = browser.versions;
let evokeAppURL = '';
let cb;
if (browserVersions.ios) {
evokeAppURL = config.ios.data ? 'authority://' + config.ios.url + '?' + config.ios.dataArr.join('&') : 'authority://' + config.ios.url;
cb = config.ios.callback;
} else if (browserVersions.android) {
evokeAppURL = config.android.data ? 'authority://' + config.android.url + '?' + config.android.dataArr.join('&') : 'authority://' + config.android.url;
cb = config.android.callback;
};
const initialTime = new Date();
let counter = 0;
let waitTime = 0;
const checkOpen = setInterval(() => {
count++;
waitTime = new Date() - initialTime;
if (waitTime > 2500) {
clearInterval(checkOpen);
cb && cb();
}
if (counter < 100) return;
const hide = document.hidden || document.webkitHidden;
if (!hide) {
cb && cb(); // 唤端失败的回调函数
}
}, 20);
}

注意

1、h5在微信中无法唤醒App,需要“用浏览器打开”
微信对所有的分享连接做了scheme屏蔽,也就是说分享连接中所有对于scheme的调用都被微信封掉了。

  1. 在询问是否打开APP的时候,如果选择了“取消”,则再唤起APP的时候会不起作用。目前并没有什么解决方案,在chrome Android,UC Android上会复现问题。需再次刷新页面才行。

  2. 在ios手机中,用location.href唤起app,本地如果没装app,会弹窗提示safari浏览器打不开该网页,网址无效,后面在用location.href来下载安装包的话也会显示同样的错误,即使你的下载链接是有效的。解决办法:IOS9及以上使用 Universal Links

感谢作者
参考博客
实现文章

问题跨域

前端开发经常面临跨域问题,恩Universal Link也有跨域问题,但不一样的是,Universal Link,必须要求跨域,如果不跨域,就不行,就失效,就不工作。(iOS 9.2之后的改动,苹果就这么规定这么设计的)

这也是上面拿知乎举例子的时候重点强调的一个问题,知乎为什么使用oia.zhihu.com做Universal Link?

  • 假如当前网页的域名是 A
  • 当前网页发起跳转的域名是 B
  • 必须要求 B 和 A 是不同域名,才会触发Universal Link
  • 如果B 和 A 是相同域名,只会继续在当前WebView里面进行跳转,哪怕你的Universal Link一切正常,根本不会打开App

是不是不太好理解,那直接拿知乎举例子

https://www.zhihu.com/question/22914651

知乎的一般网页URL都是www.zhihu.com域名,你在微信朋友圈看到了知乎的问题分享,如果copy url 你就能看到这样的链接
微信里其实是屏蔽Schema的,但是你依然能看到大大的一个按钮App内打开,这确实就是通过Universal Link来实现的,但如果知乎把Universal Link 配在了www.zhihu.com域名,那么即便已经安装了App,Universal Link也是不会生效的。

一般的公司都会有自己的主域名,比如知乎的www.zhihu.com,在各处分享传播的时候,也都是直接分享基于主域名的url,但为了解决苹果强制要求跨域才生效的问题,Universal Link就不能配置在主域名下,于是知乎才会准备一个oia.zhihu.com域名,专为Universal Link使用,不会跟任何主动传播分享的域名撞车,从而在任何活动WAP页面里,都能顺利让Universal Link生效。

简单一句话

只有当前webview的url域名,与跳转目标url域名不一致时,Universal Link 才生效
Universal Link不是必须通过a标签的href来跳转,我遇到一个问题就是我的页面没有识别Universal Link,因为异步渲染页面,导致没有解析出来Universal Link,加了一个隐藏的a标签,href配置上Universal Link,页面里面所有需要跳转Universal Link都会被识别