文章目录
- 背景
- 现状
- 结论
- URL Scheme(通用)
- 效果
- 说明
- 场景
- Intent语法(安卓原生谷歌浏览器)
- 如何让ios APP 支持自定义URL Scheme?
- 如何让Android APP 支持自定义URL Scheme?
- App Links(安卓6.0+)
- 效果
- 说明
- 如何让Android APP 支持App Links?
- Universal Link(IOS 9+)
- 效果
- 说明
- 如何让ios APP 支持 Universal Link?
- 微信的开放标签
- 效果
- 遇到的坑
- 微信/浏览器中唤起 app store
- 效果
- 说明
- h5端调起app
- h5传参给app,app端如何接收
- ios
- 微信开放标签参数
- URL scheme参数
- android
- 微信开放标签参数
- URL scheme参数
- 参考文档
背景
调研 h5 唤起 App 方案
目的:引导已下载用户打开 APP,引导未下载用户下载 APP。
现状
IOS:绝大多数用户使用 IOS 10 +
Android:跨度比较大,4以上均有一定的人使用,各个浏览器对 app links处理方式不同
结论
如上图,我们一般业务逻辑是有两个页面,一个详情页,一个下载页。上图逻辑中描述的就是详情页“打开app”按钮的逻辑。
其中涉及到的技术:
- URL Scheme(android/ios都支持)
- Universal Link(ios9 及以上支持的)
- App Links(android 6+ 及以上支持)【这个可以不用,因为这个只在短信打开app可以用到】
- 微信开放标签SDK(android/ios都支持)
- 微信/浏览器中唤起 app store
接下来一一说明以上技术
URL Scheme(通用)
效果
只能在浏览器中使用,微信环境触发不了。
下图是 ios Safari 截图,android 也是差不多的一个弹窗。
说明
有点像 web 中我们通过域名定位到一个网站,app 同样是通过类似的这个东西(URL Scheme)来定位到 app
# authority包括host和port两部分
[scheme:][//authority][path][?query][#fragment]
常用APP的 URL Scheme
weixin:// | alipay:// | taobao:// | sinaweibo:// | mqq:// | zhihu:// | sms:// |
其中scheme既可以是Android中常见的协议,也可以是我们自定义的协议。Android中常见的协议包括content协议、http协议、file协议等,自定义协议可以使用自定义的字符串,当我们启动第三方的应用时候,多是使用自定义协议。
场景
- 服务器下发跳转路径,客户端根据服务器下发跳转路径跳转相应的页面
- H5页面点击锚点,根据锚点具体跳转路径APP端跳转具体的页面
- APP端收到服务器端下发的PUSH通知栏消息,根据消息的点击跳转路径跳转相关页面
- APP根据URL跳转到另外一个APP指定页面
Intent语法(安卓原生谷歌浏览器)
安卓的原生谷歌浏览器自从 chrome25 版本开始对于唤端功能做了一些变化,URL Scheme 无法再启动Android应用。 例如,通过 iframe 指向 weixin://,即使用户安装了微信也无法打开。所以,APP需要实现谷歌官方提供的 intent: 语法
如果用户未安装 APP,则会跳转到系统默认商店。当然,如果你想要指定一个唤起失败的跳转地址,添加下面的字符串在 end; 前就可以了:
intent:
//scan/
#Intent;
package=com.google.zxing.client.android;
scheme=zxing;
end;
<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>
如何让ios APP 支持自定义URL Scheme?
支持之后,在 safari 中输入 rf://
,你就会看到
这里每次在 Safari 浏览器测试要打开一个新的页面
如何让Android APP 支持自定义URL Scheme?
AndroidManifest
<activity android:name=".MainActivity">
<intent-filter> <!--正常启动-->
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter> <!--URL Scheme启动-->
<!--必有项-->
<action android:name="android.intent.action.VIEW"/>
<!--如果希望该应用可以通过浏览器的连接启动,则添加该项-->
<category android:name="android.intent.category.BROWSABLE"/>
<!--表示该页面可以被隐式调用,必须加上该项-->
<category android:name="android.intent.category.DEFAULT"/>
<!--协议部分-->
<data android:scheme="urlscheme"
android:host="auth_activity">
</intent-filter>
<intent-filter>
<action android:name="emms.intent.action.check_authorization"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="emms.intent.category.authorization"/>
</intent-filter>
</activity>
intent-filter的标签在指定path的值时,可以在里面使用通配符*,起到部分匹配的效果。
上面的设置中可以看到,MainActivity包含多个设置,第一个是正常的启动,也就是在应用列表中启动;第二个是通过URL Scheme方式启动,其本身也是隐式Intent调用的一种,不同在于添加了属性,定义了其接受URL Scheme协议格式为urlschemel://auth_activity
Activity
Intent intent = getIntent();
String scheme = intent.getScheme();
String dataString = intent.getDataString();
Uri uri = intent.getData();
if (uri != null) {
//完整的url信息
String url = uri.toString();
//scheme部分
String schemes = uri.getScheme();
//host部分
String host = uri.getHost();
//port部分
int port = uri.getPort();
//访问路径
String path = uri.getPath();
//编码路径
String path1 = uri.getEncodedPath();
//query部分
String queryString = uri.getQuery();
//获取参数值
String systemInfo = uri.getQueryParameter("tool_id");
}
接受参数
App Links(安卓6.0+)
效果
比如在短信、记事本等等中有一个链接,点击直接唤起 app,没有多余的弹窗之类的。这个目前在 h5 打开 app 的这个场景没有用到。
说明
Android App Links是一种特殊的Deep Links,它使Android系统能够直接通过网站地址打开应用程序对应的内容页面,而不需要用户选择使用哪个应用来处理网站地址。
Deep Link 即我们通常说的url scheme跳转
这个主要的场景就是你自己的网站,自己的app。
如何让Android APP 支持App Links?
<activity ...>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" android:host="www.example.com" />
<data android:scheme="https" />
<data android:scheme="https" android:host="*.example.com" />
</intent-filter>
</activity>
当 android:autoVerify="true"
出现在你任意一个 intent filter 里,在Android 6.0 及以上的系统上安装应用的时候,会触发系统对APP里和URL有关的每一个域名的验证。验证过程设计以下步骤:
1、系统会检查所有包含以下特征的 intent filter:
- Action 为
android.intent.action.VIEW
- Category 为
android.intent.category.BROWSABLE
和android.intent.category.DEFAULT
- Data scheme 为 http 或 https
2、对于在上述intent filter里找到的每一个唯一的域名,Android系统会到对应的域名下查找数字资产文件,地址是:https://域名/.well-known/assetlinks.json
比如知乎的网站:https://www.zhihu.com/.well-known/assetlinks.json
如何生成数字资产文件?
这是知乎的文件举例:
[
{
"relation": [
"delegate_permission/common.handle_all_urls"
],
"target": {
"namespace": "android_app",
"package_name": "com.zhihu.android",
"sha256_cert_fingerprints": [
"BD:84:50:55:7C:B3:96:5C:05:1F:16:11:D4:28:6A:5F:02:9B:90:9C:AE:3D:E7:57:EC:15:2D:05:63:C3:F7:FA"
]
}
}
]
需要改的就是 package_name 和 sha256_cert_fingerprints,别的不用动
- package_name:在build.gradle里定义的application ID
- sha256_cert_fingerprints:应用签名的SHA256指纹信息。你可以用下面的命令,通过Java keytool来生成指纹信息:
keytool -list -v -keystore my-release-key.keystore
这个字段支持多个指纹信息,可以用来支持不同的应用版本,如开发版本和发布版本。
当你用这个脚本生成的没有 sha256 的时候,将你的 java 版本升级一下,我当时是 java8 不行,升级到 11 就可以了。
注意:
- assetlinks.json文件的content-type必须为application/json
- 不管你的应用的intent filter是否定义https作为data的scheme,assetlinks.json必须能通过HTTPS链接访问
- assetlinks.json必须能不经过任何重定向被访问到(即没有301、302跳转),同时可以被爬虫访问到(你的robot.txt必须允许抓取/.well-known/assetlinks.json)
- 如果你的应用支持多种域名,你需要把assetlinks.json发布在这几个域名的服务器上。
- 不要在你的AndroidManifest文件里发布无法公开访问的开发/测试URL(比如那些只能通过VPN进行访问的URL)。一种可行的做法是配置构建类型,来为开发构建生成不同的清单。
测试
https://developers.google.com/digital-asset-links/tools/generator场景
大多是短信链接直接唤起app的场景
Universal Link(IOS 9+)
效果
在短信/记事本/微信 等等环境中,点击链接可以直接唤起 app,没有弹框等多余的提示。
说明
Universal Link 是苹果在 WWDC2015 上为 iOS9 引入的新功能,通过传统的 HTTP 链接即可打开 APP。如果用户未安装 APP,则会跳转到该链接所对应的页面。
为什么要用Universal Link?
之前只能用URL Scheme 的时候,如果在微信里打开h5,需要引导用户点击右上角用 safari 上,现在直接点击一个按钮就可以跳转到对应的app。
如何让ios APP 支持 Universal Link?
官方文档 1、在 开发者中心 ,Certificates, Identifiers & Profiles --> Identifiers 下 AppIDs 找到自己的 App ID,编辑打开 Associated Domains 服务。
2、打开工程配置中的 Associated Domains ,在其中的 Domains 中填入你想支持的域名,必须以 applinks:
为前缀
3、配置 apple-app-site-association 文件,文件名必须为 apple-app-site-association ,不带任何后缀。上传该文件到你的 HTTPS 服务器的根目录或者 .well-known 目录下。
例如知乎链接:https://www.zhihu.com/apple-app-site-association
{
"applinks": {
"apps": [],
"details": [
{
"appID": "9JA89QQLNQ.com.apple.wwdc",
"paths": [ "/wwdc/news/", "/videos/wwdc/2015/*"]
},
{
"appID": "ABCD1234.com.apple.wwdc",
"paths": [ "*" ]
},
"8J52SRPW6X.com.zhihu.ios": {
"paths": [
"/universal-links-callback/*",
"/qq_conn/100490701/*",
"NOT /question/*/log",
"/question/*",
"/people/*",
"/topic/*",
"/p/*"
]
},
]
}
}
- appID:组成方式是 teamId.yourapp’s bundle identifier。如上面的 9JA89QQLNQ就是teamId。登陆开发者中心,在Account -> Membership里面可以找到Team ID。
- paths:设定你的app支持的路径列表,只有这些指定的路径的链接,才能被app所处理。星号的写法代表了可识 别域名下所有链接。
微信的开放标签
如果微信打开的 h5 想要跳转 app 时
- h5 端需要用微信的开放标签 wx-open-launch-app
- App 必须接入微信 OpenSDK。
这里解读以下接入的规则:
- 多个公众号可以绑定同一个应用。
- 公众号可以有多个域名白名单。
- 公众号对应绑定的应用只能有一个域名。
比如你有一个域名e.test.com,一个公众号,一个开放平台的应用,那么你需要:
1、公众号后台绑定域名白名单 e.test.com
2、开放平台找到公众号的关联设置,选择该公众号,并绑定域名 e.test.com。
所以这三个是一一对应的,如果你有另一个域名想要接入唤起同一个应用,那么你需要找一个新的公众号和新的域名,按上面的步骤绑定。
这里注意一点:其实在 ios 中,只要你接入了 Universal Link,那么在微信环境下你是可以直接唤起 app 的而不需要接入微信sdk触发这个弹框,但是如果你想和 android 保持一致的用户体验和代码逻辑,那么就干脆都接入吧。
效果
ios、android 效果相同。
遇到的坑
1、按照微信的文档,各方面都接好之后,发现只是偶尔能唤起这个弹框,大多数的时候是唤不起的。最终发现是该页面导致的:
也就是说,你的测试链接不要放在微信的聊天记录点开,会先到这个页面,微信可能会做一些处理,导致我们的 sdk 被影响。我在这个帖子有详细的回答。
https://developers.weixin.qq.com/community/develop/doc/0004c06e12479887f00d42be85b800 所以需要你把这个链接用微信分享,或者生成二维码,总之不要触发这个页面,就可以正常调起 h5 唤起 APP 的弹框。
2、Android冷启动无法唤起
添加下面文件中的注释部分
WXEntryActivity.java
package com.cmvalue.wisederma.wxapi;
import android.app.Activity;
import android.os.Bundle;
import com.theweflex.react.WeChatModule;
import com.tencent.mm.opensdk.constants.ConstantsAPI;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.tencent.mm.opensdk.modelmsg.ShowMessageFromWX;
import com.tencent.mm.opensdk.openapi.IWXAPI;
import com.tencent.mm.opensdk.openapi.WXAPIFactory;
import com.tencent.mm.opensdk.modelbase.BaseReq;
import com.tencent.mm.opensdk.modelbase.BaseResp;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import android.util.Log;
import android.content.Intent;
import com.tencent.mm.opensdk.openapi.IWXAPIEventHandler;
import java.lang.ref.WeakReference;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import org.json.JSONObject;
import org.json.JSONException;
import com.facebook.react.ReactPackage;
import java.util.List;
import com.facebook.react.PackageList;
import com.theweflex.react.WeChatPackage;
import com.cmvalue.wisederma.MainActivity;
import java.lang.Thread;
import android.os.Handler;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.bridge.ReactContext;
import com.cmvalue.wisederma.generated.BasePackageList;
import com.facebook.react.common.build.ReactBuildConfig;
public class WXEntryActivity extends ReactActivity implements IWXAPIEventHandler {
private IWXAPI api;
private static final String APP_ID = "xxxxx";
private static String TAG = "MicroMsg.WXEntryActivity";
public void trigger () {
api.handleIntent(getIntent(), this);
}
public void loopCall () {
Handler handler = new Handler();
handler.postDelayed(new Runnable(){
public void run() {
if(WeChatModule.getModules().isEmpty()){
Log.e("WXEntryActivity", "11111");
loopCall();
} else {
Log.e("WXEntryActivity", "222222");
trigger();
}
}
}, 3000);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
api = WXAPIFactory.createWXAPI(this, APP_ID, false);
WeChatModule.handleIntent(getIntent());
// 关键:如果没有,说明冷启动,手动启动,否则就正常启动
// 这个getModules函数是手动改的npm包,加了一个
if(WeChatModule.getModules().isEmpty()){
Intent intent = new Intent(WXEntryActivity.this, MainActivity.class);
startActivity(intent);
loopCall();
}else {
trigger();
}
finish();
}
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
api.handleIntent(intent, this);
}
@Override
public void onReq(BaseReq baseReq) {
WritableMap map = Arguments.createMap();
map.putString("openId", baseReq.openId);
map.putString("transaction", baseReq.transaction);
if (baseReq.getType() == ConstantsAPI.COMMAND_SHOWMESSAGE_FROM_WX) {
ShowMessageFromWX.Req req = (ShowMessageFromWX.Req) baseReq;
// 对应JsApi navigateBackApplication中的extraData字段数据
map.putString("type", "SendMessageToWX.Resp");
map.putString("lang", req.lang);
map.putString("extMsg", req.message.messageExt);
}
ReactInstanceManager mReactInstanceManager = this.getReactNativeHost().getReactInstanceManager();
ReactContext currentReactContext = mReactInstanceManager.getCurrentReactContext();
currentReactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("WeChat_Resp", map);
finish();
}
@Override
public void onResp(BaseResp resp) {
Log.e("WXEntryActivity", "测试测试测试测试测试");
}
}
react-native-wechat-lib 这个npm 包里找到这个文件 WeChatModule.java,添加如下代码:
// add
public static ArrayList<WeChatModule> getModules() {
return modules;
}
这个 getModules 也就是上面第一个文件要调用的。
微信/浏览器中唤起 app store
效果
说明
浏览器中唤起 app store,以下任意一种都可以
window.top.location.href = 'https://itunes.apple.com/cn/app/id432274380'
window.location.href = 'https://itunes.apple.com/cn/app/id432274380'
window.top.location.href = 'itms-apps://itunes.apple.com/app/id432274380'
window.location.href = 'itms-apps://itunes.apple.com/app/id432274380'
微信中唤起 appstore,需要接入微信的 sdk,也就是上面所说的开放标签。跳转链接还是这个。(这个微信官方文档也没有说过,完全是自己试出来的)
h5端调起app
以上都是 app 端要做的事情,那么 h5 端要做什么调起 app 呢?
有好几种,比如下面的 a 标签,iframe 之类的,npm 上 callapp-lib 包兼容了这些,我们可以直接用。
// a标签调起举例
<a href="intent://scan/#Intent;scheme=zxing;package=com.google.zxing.client.android;end"">扫一扫</a>
// 或
window.location.href = 'sinaweibo://qrcode';
callapp-lib 包调起 app
<template>
<div @click="goDownload">
<img class="bgImg" src="~/assets/img/bg.png" alt="" />
<img v-if="userAgent.isWechat" class="chickImg" src="~/assets/img/chick.png" alt="" />
</div>
</template>
<script>
import CallApp from 'callapp-lib'
export default {
computed: {
userAgent() {
return this.$store.getters.userAgent
}
},
mounted() {
if (!this.userAgent.isWechat) {
this.openApp(this.$route.query)
}
},
methods: {
openApp(url) {
const options = {
scheme: {
protocol: 'kccatalog'
},
intent: {
package: '',
scheme: ''
},
appstore: '填写appstore的下载地址',
yingyongbao: '填写应用宝的下载地址',
fallback: '填写唤端失败后跳转的地址。'
}
const callLib = new CallApp(options)
callLib.open({
param: url.param,
path: url.path
})
},
goDownload() {
window.location.href = '没有自动唤端的话,证明手机里面没有app, 点击页面上任意一个地方直接跳应用宝下载链接, 微信不会拦截支付宝的链接'
}
}
}
</script>
h5传参给app,app端如何接收
ios
微信开放标签参数
按照 https://github.com/little-snow-fox/react-native-wechat-lib 中的配置好了之后,可以用这个接收参数
import { DeviceEventEmitter } from 'react-native';
DeviceEventEmitter.addListener('WeChat_Req', (req) => {
handleExtMsg(req.extMsg);
});
这时你会发现 ios 冷启动无法唤起,需要在 ios 的生命周期 didFinishLaunchingWithOptions 函数里注册微信:
// 这个是RN向ios注入全局变量用的
#import "ReactNativeConfig.h"
//向微信注册
NSString *APP_ID = [ReactNativeConfig envFor:@"APP_ID"];
NSString *UNIVERSAL_LINK = [ReactNativeConfig envFor:@"UNIVERSAL_LINK"];
[WXApi registerApp:APP_ID universalLink:UNIVERSAL_LINK];
URL scheme参数
接受到之后就可以解析参数并跳到对应的页面了。
import { Linking } from 'react-native';
// 热启动接受
Linking.addEventListener('url', (data) => {
handleLinking(data.url);
});
// 冷启动接受
const initialUrl = await Linking.getInitialURL();
if (initialUrl) {
setTimeout(() => {
handleLinking(initialUrl);
}, 3000);
}
android
微信开放标签参数
按照 https://github.com/little-snow-fox/react-native-wechat-lib 中的配置好了之后,可以用这个接收参数
import { DeviceEventEmitter } from 'react-native';
DeviceEventEmitter.addListener('WeChat_Resp', (resp) => {
handleExtMsg(resp.extMsg);
});
URL scheme参数
同 ios
参考文档