赞
踩
调研 h5 唤起 App 方案
目的:引导已下载用户打开 APP,引导未下载用户下载 APP。
IOS:绝大多数用户使用 IOS 10 +
Android:跨度比较大,4以上均有一定的人使用,各个浏览器对 app links处理方式不同
如上图,我们一般业务逻辑是有两个页面,一个详情页,一个下载页。上图逻辑中描述的就是详情页“打开app”按钮的逻辑。
其中涉及到的技术:
接下来一一说明以上技术
只能在浏览器中使用,微信环境触发不了。
下图是 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协议等,自定义协议可以使用自定义的字符串,当我们启动第三方的应用时候,多是使用自定义协议。
安卓的原生谷歌浏览器自从 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>
支持之后,在 safari 中输入 rf://
,你就会看到
这里每次在 Safari 浏览器测试要打开一个新的页面
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,没有多余的弹窗之类的。这个目前在 h5 打开 app 的这个场景没有用到。
Android App Links是一种特殊的Deep Links,它使Android系统能够直接通过网站地址打开应用程序对应的内容页面,而不需要用户选择使用哪个应用来处理网站地址。
Deep Link 即我们通常说的url scheme跳转
这个主要的场景就是你自己的网站,自己的app。
<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:
android.intent.action.VIEW
android.intent.category.BROWSABLE
和android.intent.category.DEFAULT
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,别的不用动
keytool -list -v -keystore my-release-key.keystore
这个字段支持多个指纹信息,可以用来支持不同的应用版本,如开发版本和发布版本。
当你用这个脚本生成的没有 sha256 的时候,将你的 java 版本升级一下,我当时是 java8 不行,升级到 11 就可以了。
注意:
测试
https://developers.google.com/digital-asset-links/tools/generator
场景
大多是短信链接直接唤起app的场景
在短信/记事本/微信 等等环境中,点击链接可以直接唤起 app,没有弹框等多余的提示。
Universal Link 是苹果在 WWDC2015 上为 iOS9 引入的新功能,通过传统的 HTTP 链接即可打开 APP。如果用户未安装 APP,则会跳转到该链接所对应的页面。
为什么要用Universal Link?
之前只能用URL Scheme 的时候,如果在微信里打开h5,需要引导用户点击右上角用 safari 上,现在直接点击一个按钮就可以跳转到对应的app。
官方文档
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/*"
]
},
]
}
}
如果微信打开的 h5 想要跳转 app 时
这里解读以下接入的规则:
比如你有一个域名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,以下任意一种都可以
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,也就是上面所说的开放标签。跳转链接还是这个。(这个微信官方文档也没有说过,完全是自己试出来的)
以上都是 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>
按照 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];
接受到之后就可以解析参数并跳到对应的页面了。
import { Linking } from 'react-native';
// 热启动接受
Linking.addEventListener('url', (data) => {
handleLinking(data.url);
});
// 冷启动接受
const initialUrl = await Linking.getInitialURL();
if (initialUrl) {
setTimeout(() => {
handleLinking(initialUrl);
}, 3000);
}
按照 https://github.com/little-snow-fox/react-native-wechat-lib 中的配置好了之后,可以用这个接收参数
import { DeviceEventEmitter } from 'react-native';
DeviceEventEmitter.addListener('WeChat_Resp', (resp) => {
handleExtMsg(resp.extMsg);
});
同 ios
https://github.com/suanmei/callapp-lib
https://github.com/jawidx/web-launch-app
https://www.zhihu.com/question/270839820
示例
app links
Universal Link配置
https://github.com/little-snow-fox/react-native-wechat-lib
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。