前言:h5/web实现“唤起app,如果已下载就直接打开app,如果不能下载,就直接跳转下载”的功能,通过反复查资料,我知道目前有2钟实现方式:
1. 是通过把h5cordova打包成app调用cordova的方法。
2. 但是绝大多数做法是通过h5打开app链接地址,如果能打开则直接会跳转,如果不能,则设置一个延迟定时器setTimeout(如延迟2秒),然后直接进行下载window.location.href=downUrl。但是因为ios和android以及不同浏览器不同应用内置的浏览器打开,会出现的一些被限制兼容等问题。所以更为精细的做法是更加ios和android以及浏览器,版本,常用打开应用的内置浏览器.....等条件,一般存在三种打开app的方式:
注意:方法二的一个弊端,在打开app链接时,如果因为网络延迟响应超过2秒也会被识别为没有安装app,直接进行下载链接。或者如果用超过2秒且页面没有关闭同时满足才下载,但是在ios9以上的safari浏览器上仍然会存在的问题是,safari浏览器通过window.location.href跳转链接时,会默认有弹窗提示(链接有效时,提示是否需要打开app的弹窗。链接无效时,仍然会提示,该网址无效的弹窗)。如果链接有效,这个默认弹窗在2秒之后在操作是否需要打开app时,此时已经超过2秒,代码仍然会视为页面超过2秒仍然没有关闭,进而会直接指向下载app链接的代码。
方式一:location打开,最常见的打开方式。但是一些浏览器会限制这种方式打开。
window.location.href=openUrl
方式二:a标签点击打开
const tagA = document.createElement('a');
tagA.setAttribute('href', openUrl);
tagA.style.display = 'none';
document.body.appendChild(tagA);
tagA.click();
方式三:iframe 标签点击打开
const iframe = document.createElement('iframe');
iframe.frameborder = '0';
iframe.src = openUrl;
iframe.style.cssText = 'display:none;border:0;width:0;height:0;';
document.body.appendChild(iframe);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>callapp-lib源码解析</title>
</head>
<body>
<div id='btn''>点击打开/下载app</div>
<script>
let iosOpenUrl='';
let androidOpenUrl='';
let iosDownUrl='';
let androidDownUrl="";
let intent=null; //安卓原生谷歌浏览器必须传递 Intent 协议地址,才能唤起 APP
const ua = window.navigator.userAgent || '';
const isAndroid = /android/i.test(ua);
const isIos = /iphone|ipad|ipod/i.test(ua);
const isWechat = /micromessenger\/([\d.]+)/i.test(ua);
const isWeibo = /(weibo).*weibo__([\d.]+)/i.test(ua);
const isQQ = /qq\/([\d.]+)/i.test(ua);
const isQQBrowser = /(qqbrowser)\/([\d.]+)/i.test(ua);
const isQzone = /qzone\/.*_qz_([\d.]+)/i.test(ua);
const isOriginalChrome = /chrome\/[\d.]+ Mobile Safari\/[\d.]+/i.test(ua) && isAndroid;// 安卓 chrome 浏览器,很多 app 都是在 chrome 的 ua 上进行扩展的,即安卓的应用app很多都是内置chrome浏览器
// chrome for ios 和 safari 的区别仅仅是将 Version/<VersionNum> 替换成了 CriOS/<ChromeRevision>
// ios 上很多 app 都包含 safari 标识,但它们都是以自己的 app 标识开头,而不是 Mozilla
const isSafari = /safari\/([\d.]+)$/i.test(ua) && isIos && ua.indexOf('Crios') < 0 && ua.indexOf('Mozilla') === 0;
const wechatVersion = navigator.appVersion.match(/micromessenger\/(\d+\.\d+\.\d+)/i)[1]; //获取 微信 版本号
let iosVersion = navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/); //获取 ios 大版本号
iosVersion=parseInt(iosVersion[1], 10);
if(isIos){ //iOS设备
// 近期ios版本qq禁止了scheme和universalLink唤起app,安卓不受影响 - 18年12月23日
// ios qq浏览器禁止了scheme和universalLink - 2019年5月1日
// ios 微信自 7.0.5 版本放开了 Universal Link 的限制
if ((isWechat && wechatVersion < '7.0.5') || isQQ || isQQBrowser) {//微信且微信的版本小于7.0.5,或者是qq打开,或者是qq浏览器打卡
window.top.location.href = iosOpenUrl;
} else if (iosVersion < 9) { //ios9版本以下
const iframe = document.createElement('iframe');
iframe.frameborder = '0';
iframe.src = iosOpenUrl;
iframe.style.cssText = 'display:none;border:0;width:0;height:0;';
document.body.appendChild(iframe);
} else {
window.top.location.href = iosOpenUrl;
}
}else { //android设备
if(isWechat){ //android的微信
window.top.location.href = androidOpenUrl;
}else if(isOriginalChrome){ //android的原生浏览器
if (typeof intent !== 'undefined') { //安卓原生谷歌浏览器必须传递 Intent 协议地址,才能唤起 APP
window.top.location.href = androidOpenUrl;
} else { // scheme 在 andriod chrome 25+ 版本上必须手势触发
const tagA = document.createElement('a');
tagA.setAttribute('href', androidOpenUrl);
tagA.style.display = 'none';
document.body.appendChild(tagA);
tagA.click();
}
}else{ //android设备其他应用
const iframe = document.createElement('iframe');
iframe.frameborder = '0';
iframe.src = androidOpenUrl;
iframe.style.cssText = 'display:none;border:0;width:0;height:0;';
document.body.appendChild(iframe);
}
}
document.getElementById('btn').addEventListener("click",()=>{
checkOpen(()=>{
if(isIos){
window.top.location.href = iosDownUrl;
}else{
window.top.location.href = androidDownUrl;
}
},2000);
})
function checkOpen(failCallback, timeout=2000) {
const visibilityChangeProperty = getVisibilityChangeProperty();
const timer = setTimeout(() => {
const hidden = isPageHidden(); //判断页面是否隐藏(进入后台)
if (!hidden) { //没有进入后端,说明唤起失败,唤起失败,就执行失败的函数
failCallback();
}
}, timeout);
if (visibilityChangeProperty) {
document.addEventListener(visibilityChangeProperty, () => {
clearTimeout(timer);
});
return;
}
window.addEventListener('pagehide', () => { //页面关闭时 清除定时器
clearTimeout(timer);
});
}
/**
* 获取判断页面 显示|隐藏 状态改变的属性,webkitvisibilitychange/mozvisibilitychange/msvisibilitychange/ovisibilitychange/visibilitychange文档的可见性改变时触发
*/
function getVisibilityChangeProperty() {
const prefix = getPagePropertyPrefix();
if (prefix === false) return false;
return `${prefix}visibilitychange`;
}
/**
* 获取页面隐藏属性的前缀
* 如果页面支持 hidden 属性,返回 '' 就行
* 如果不支持,各个浏览器对 hidden 属性,有自己的实现,不同浏览器不同前缀,遍历看支持哪个
*/
function getPagePropertyPrefix() {
const prefixes = ['webkit', 'moz', 'ms', 'o'];
let correctPrefix;
if ('hidden' in document) return '';
prefixes.forEach((prefix) => {
if (`${prefix}Hidden` in document) {
correctPrefix = prefix;
}
});
return correctPrefix || false; //返回结果是'webkit', 'moz', 'ms', 'o' ,false
}
/**
* 判断页面是否隐藏(进入后台)
*/
function isPageHidden() {
const prefix = getPagePropertyPrefix();
if (prefix === false) return false;
const hiddenProperty = prefix ? `${prefix}Hidden` : 'hidden';
return document[hiddenProperty]; //返回结果是document.hidden,document.mozHidden,document.msHidden, document.webkitHidden,document.oHidden,是判断页面是否隐藏(进入后台)
}
</script>
</body>
</html>