在移动端浏览器H5页面中,点击按钮打开本地应用主要通过 scheme 协议。本文主要介绍如何在浏览器H5页面中通过 scheme 协议打开本地应用。
scheme协议定义
scheme 是一种页面之间跳转的协议,不仅可以用于app之间进行跳转,还可以用于 H5 页面跳转到app页面。
无论Android还是IOS,都可以通过在H5页面中打开 scheme 协议的地址,从而打开本地app。
scheme 协议定义和 http 协议类似,都是标准的 URI 结构。
[scheme:][//host:port][path][?query][#fragment]
- scheme : 协议名称 - 必须
- host : 协议地址 - 必须
- port : 协议的端口,可以不填
- path : 协议路径,可用 / 连接多个
- query : 携带的参数可用 & 连接多个
- fragment : 锚点
下面看一个例子:
wexin://tencent.com:8080/dl/news/open?data=902323¶ms=test
- weixin : 协议名称
- tencent.com : 域名
- 8080 : 端口
- /dl/news/open : 页面的路径
- data, params : 传递的参数
URI中的参数如果包含特殊字符,需要预先进行url编码,否则的话URI可能不能打开。
在 Android 中声明实现 scheme
要使得在浏览器或者别的应用中通过打开 scheme 协议来唤起应用,需要对该应用进行相关的配置。
首先需要在Android工程的 Manifest文件,给想要接收跳转的Activity添加 intent-filter 节点的配置拦截器规则
<activity
<!--定义响应该scheme协议的 activity 的名称 -->
android:name=".DeepLinkActivity"
<!--需要添加下面的intent-filter配置-->
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<!--scheme 允许在浏览器中打开-->
<category android:name="android.intent.category.BROWSABLE"/>
<!--scheme 相关信息配置-->
<data android:scheme="uuopen"
android:host="uusama.com"/>
</intent-filter>
</activity>
上面的 data 节点中可以包含下面的信息来对相应的scheme进行过滤,一般需要配置 scheme 和 host。
<data
android:scheme=""
android:host=""
android:port=""
android:path=""
android:mimeType=""
android:pathPattern=""
android:pathPrefix=""
android:ssp=""
android:sspPattern=""
android:sspPrefix=""/>
然后在相应的 activity 可以获取 uri 中参数。
public class DeepLinkActivity extends AppCompatActivity {
private static final String TAG = "DeepLinkActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent = getIntent();
Log.e(TAG, "scheme:" + intent.getScheme());
Uri uri = intent.getData();
Log.e(TAG, "scheme: " + uri.getScheme()); // 获取 scheme 名称
Log.e(TAG, "host: " + uri.getHost()); // 获取 scheme 的host
Log.e(TAG, "path: " + uri.getPath()); // 获取 scheme 的路径
Log.e(TAG, "queryString: "+ uri.getQuery()); // 获取 scheme 的参数,?后面的部分
Log.e(TAG, "queryParameter: " + uri.getQueryParameter("param")); // 获取 scheme 中的 param 参数
}
}
其中的 intent 实例有下面的方法可以获取相应的 scheme 信息:
- getScheme() :获取Uri中的scheme名称:[scheme:]
- getSchemeSpecificPart() :获取Uri中的scheme-specific-part:部分:[//host:port][path]
- getFragment() :获取Uri中的Fragment部分:[#fragment]
- getAuthority() :获取Uri中Authority部分:[//host:port]
- getPath() :获取Uri中path部分:[path]
- getQuery() :获取Uri中的query部分:[?query]
- getHost() :获取Authority中的Host字符串
- getPost() :获取Authority中的Port字符串
- List< String> getPathSegments() :依次提取出Path的各个部分的字符串,以字符串数组的形式输出
- getQueryParameter(String key) :获取query部分中 key 对应的参数值
在浏览器中打开 scheme
在浏览器中打开 scheme 就像打开一个不同的http地址一样。可以在一个 a 标签中打开。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Scheme</title>
</head>
<body>
<a href="wexin://" id="open">打开应用</a>
</body>
</html>
点击上面的H5页面中的链接将会尝试唤醒微信,在一些浏览器中,可能会弹出一个提示框,询问用户是否允许打开应用。
如果打开的 scheme 在本地没有对应的 app,则点击连接不会反应。
当然还可以使用 JavaScript 代码打开,只需要添加相应的事件触发和处理即可。
在JavaScript代码中打开连接有以下几种方式:
- 新建一个隐藏的 iframe ,地址指向需要打开的url
- 使用 window.location 或者 window.location.href 刷新当前页面
- 新建一个隐藏的 a 标签,地址指向打开的url,并触发打开链接事件
- 动态创建一个script脚本,在这个脚本中新建一个a标签并打开
// 打开url的方式
var urlOpen = {
'iframe' : function(url) {
var iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
},
'location' : function(url) {
window.location = url;
},
'href' : function(url) {
var a = document.createElement('a');
a.style.display = 'none';
a.href = url;
document.body.appendChild(a);
a.click();
},
'script' : function(url) {
var script = document.createElement('script');
script.setAttribute('type', 'test/javascript');
script.innerHTML = '(function(){' +
'var a = document.createElement("a");' +
'a.style.display = "none";' +
'a.href = "' + url.replace(/"/g, '\"') + '";' +
'document.body.appendChild(a);' +
'a.click();' +
'})()';
document.body.appendChild(script);
},
'open' : function(url) {
window.open(url);
}
};
浏览器判断是否安装应用
很多时候用户在浏览器中打开 scheme 链接的时候,用户不一定安装了应用,这个时候打开会失效,我们希望打开这个动作应该下载应用。这个时候需要判断用户是否安装应用。
其实判断用户是否安装某个应用的方法,就是直接打开这个应用的 scheme,查看是否打开成功。但是这就是问题之所在。
我们无法在浏览器中准确地知道打开 scheme 是否成功,浏览器或者系统没有给我么这样的回调。我们只能用迂回的方法去判断。
比如在JavaScript中判断页面是否进入后台来判断打开成功。有下面这些事件和属性可以利用:
- pagehide : 页面隐藏时触发
- visibilitychange : 页面隐藏没有在当前显示时触发(切换tab也会触发该事件)
- document.hidden : 当页面隐藏时,该值为true,显示时为false
上面这些事件或者属性并不是所有浏览器都支持。下面是一个给出为 id 为 open 的按钮添加打开scheme或者下载事件的例子。
var downloader,
scheme = 'weixin://', // 需要打开的 scheme 地址
download='index'; // 如果打开scheme失效的app下载地址
// 给 id 为 open 的按钮添加点击事件处理函数
document.getElementById('open').onclick = function () {
window.location.href = scheme; // 尝试打开 scheme
// 设置3秒的定时下载任务,3秒之后下载app
downloader = setTimeout(function(){
window.location.href = download;
}, 3000);
};
document.addEventListener('visibilitychange webkitvisibilitychange', function () {
// 如果页面隐藏,推测打开scheme成功,清除下载任务
if (document.hidden || document.webkitHidden) {
clearTimeout(downloader);
}
});
window.addEventListener('pagehide', function() {
clearTimeout(downloader);
});
}
对于通过判断打开 scheme 的耗时来确实是否打开应用的做法是很容易失效的,因为无法判断打开成功以后,页面的JS是否还在执行,而且打开应用的耗时也是不可控的。
总之,没有完美的解决方案在H5页面中判断本地是否安装了某个应用,不过使用监听当前页面是否隐藏的方法能够很大程度的作为判断依据。
也有的应用不管用户是否安装应用,用户点击链接的时候,同时打开 scheme 和拉起下载页面,这种方式牺牲了很大的用户体验。
局限性
这种通过 scheme 打开本地应用的方式并不是所有浏览器都支持,尤其是在微信浏览器中是不支持使用 scheme 打开应用的,除非微信官方添加了白名单。QQ浏览器中倒是支持。
而且一些浏览器会询问用户是否打开,而另外一些则直接打开应用。
一般的做法是,判断当前浏览器是否为微信,如果是微信的话,则弹出一个遮罩层,提示用户使用其他浏览器打开。
还有就是在微信浏览器中使用应用宝的微下载,将当前页面重定向到应用宝的下载页面,不过这种方式的转化率很低。
有一个好消息是,在IOS9.0以上的系统中,可以使用 universal links 打开本地应用,不过Android不支持。
另外一个备选方案是,在微信浏览器中,使用iframe的方式打开一个包体地址(.apk结尾的url)进行下载时,会拉起一个选择框,让你选择打开的应用。不过这种方式对于某些域名无效,对于一些特殊的下载文件无效,如不能下载.rar的文件。而且对于已经安装了应用的用户来说,用户体验也不好。
使用这种技术的同时,考虑到其不确定性,应该做好备选方案。充分考虑到该页面的用户群体是否主要为新用户,以及访问的浏览器分布,从而制定相应的对用户来说比较友好的引导和备选方案。