iOS/Android 浏览器(h5)及微信中唤起本地APP
会遇到的问题:
- 如何解决未安装APP时的做好引导页
- 如何在微信中唤醒APP
- 在iOS9中如何处理universal link被用户误关的情况
- 如何解决Android各种机型、各种第三方浏览器导致的兼容问题等
- 在APP未安装情况下,引导用户下载后打开APP后,如何进入之前唤起时指定的页面或内容,即如何实现场景还原
- 在微信中唤醒APP时,如何进入指定的页面或内容
原理说明
首先需要说明,不管iOS还是Android,浏览器都不可能预知本地是否安装了某个APP的。或者更严谨地说,我们不能通过浏览器来预知本地是否安装。因为就算浏览器可以读取本地应用的安装列表,但是目前也没任何一家浏览器提供查询的API,所以这条路是走不通的。
本质上浏览器是通过URL scheme打开APP,一个APP可以设置一个或多个打开自己的URL scheme。比如,Twitter就注册自己能被「twitter://」打开。
实现方案
1.检测浏览器类型
let browser = {
versions: () => {
let u = navigator.userAgent
// let app = navigator.userAgent
return {
android: u.indexOf('Android') > -1 || u.indexOf('Linux') > -1,
iPhone: u.indexOf('iPhone') > -1 || u.indexOf('Mac') > -1,
weixin: u.toLowerCase().indexOf('micromessenger') > -1,
appVersion: (u.indexOf('iPhone') > -1 || u.indexOf('Mac') > -1)
&& /OS ((\d+_{2,3})\s/.exec(u[0].replace('OS','').replace('os','').replace(/\s+/g,'').replace(/_/g, '.')
}
}
}
let browser = {
versions: () => {
let u = navigator.userAgent
// let app = navigator.userAgent
return {
android: u.indexOf('Android') > -1 || u.indexOf('Linux') > -1,
iPhone: u.indexOf('iPhone') > -1 || u.indexOf('Mac') > -1,
weixin: u.toLowerCase().indexOf('micromessenger') > -1,
appVersion: (u.indexOf('iPhone') > -1 || u.indexOf('Mac') > -1)
&& /OS ((\d+_{2,3})\s/.exec(u[0].replace('OS','').replace('os','').replace(/\s+/g,'').replace(/_/g, '.')
}
}
}
2.打开app函数
openApp (openUrl, appUrl, action, callback) {
// 检查app是否打开
function checkOpen (cb) {
let _clickTime = +(new Date())
let flag = false
function check (elsTime) {
if (elsTime > 3000 || document.hidden || document.webkitHidden) {
flag = true
cb(flag)
} else {
cb(flag)
}
}
// 启动间隔20ms运行的定时器,并检测累计消耗时间是否超过3000ms,超过则结束
let _count = 0
let 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('a')
ifr.href = openUrl
ifr.style.display = 'none'
let event = document.createEvent('MouseEvents')
event.initEvent('click', true, true)
ifr.dispatchEvent(event)
// window.location.href = openUrl
if (callback) {
checkOpen(function (opened) {
callback && callback(opened)
})
}
document.body.appendChild(ifr)
setTimeout(function () {
document.body.removeChild(ifr)
}, 2000)
},
openApp (openUrl, appUrl, action, callback) {
// 检查app是否打开
function checkOpen (cb) {
let _clickTime = +(new Date())
let flag = false
function check (elsTime) {
if (elsTime > 3000 || document.hidden || document.webkitHidden) {
flag = true
cb(flag)
} else {
cb(flag)
}
}
// 启动间隔20ms运行的定时器,并检测累计消耗时间是否超过3000ms,超过则结束
let _count = 0
let 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('a')
ifr.href = openUrl
ifr.style.display = 'none'
let event = document.createEvent('MouseEvents')
event.initEvent('click', true, true)
ifr.dispatchEvent(event)
// window.location.href = openUrl
if (callback) {
checkOpen(function (opened) {
callback && callback(opened)
})
}
document.body.appendChild(ifr)
setTimeout(function () {
document.body.removeChild(ifr)
}, 2000)
},
理想过程是这样:浏览器尝试打开 URL scheme,在1秒计时后,检查当前时间,如果实际时间已过 1200 毫秒,说明唤起APP 成功(唤起 APP 会让浏览器的定时器变慢);如果没超过 1200 毫秒,很可能是没有安装应用,就跳到下载地址。
但原理都是一样,利用setTimeout。但这其实不稳定,因为Android是基于Linux的分时多任务的,setTimeout的基准偏差可能会没那么大。
但如果设置比较小的运行间隔(<30ms),在浏览器或者webview中,应用切换到后台,setInterval
会被很明显的延迟执行,比如设置一个运行间隔20ms,总计运行100次的定时器,如果页面一直处于前台,则100次跑完,总耗时与 100x20=2000ms不会有太大差异,但页面在后台运行时,此时间会明显超过2000ms。可以利用这一点来实现是否成功打开APP检测及回调。
另外,可以通过 document.hidden
或 document.[webkit|moz|ms]Hidden
来判断页面是否被置入后台(即应用被唤起),或visibilitychange
事件,但对于Android 4.4版本一下则不支持。
3.点击按钮的时候
if (this.info.weixin) {
// alert('请在系统浏览器中打开') 在微信中你的操作
} else {
if (this.info.iPhone) { // ios中
this.openApp('你的app', 'xxx', xxx, (flag) => {
if (!flag) {
alert('失败了') //你的操作
}
})
} else if (this.info.android) { //android
this.openApp('你的app', 'xxx', xxx, (flag) => {
if (!flag) {
alert('失败了') // 你的唤起失败操作
}
})
}
}
if (this.info.weixin) {
// alert('请在系统浏览器中打开') 在微信中你的操作
} else {
if (this.info.iPhone) { // ios中
this.openApp('你的app', 'xxx', xxx, (flag) => {
if (!flag) {
alert('失败了') //你的操作
}
})
} else if (this.info.android) { //android
this.openApp('你的app', 'xxx', xxx, (flag) => {
if (!flag) {
alert('失败了') // 你的唤起失败操作
}
})
}
}
在 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下载。
微信中打开
因为微信将唤起本地APP的接口给禁了,所以微信中是不能直接唤起APP的,一般做法是提示用户在浏览器中打开,之后的流程还是我们上面讲的内容。
但是,在iOS9中,这个限制是可以突破的,也就是说可以直接唤起APP。方法就是使用我们上文提到的universal link。
在Android和iOS8及其以下系统中,我们可以利用腾讯的亲儿子:应用宝。简单讲,就是把你的唤起地址配置成你APP的应用宝地址,微信中跳转到这个地址后,如果用户已经安装了APP,则可直接唤起,如果没有安装,则可直接点击下载,