一、deeplink是什么?
什么是deeplink, 通俗的讲就是其他app打开你app的任何页面,你需要处理这个一般用url表示的路由请求,React-Native处理deep link主要涉及到三个部分,RN端,iOS原生端,Android原生端, 本博就针对这三个部分分别讲解其实现步骤,就以我最近做的应用外文件分享到APP为主线进行讲解,并对开发中遇到的坑进行相应的说明,希望能帮助到大家少走弯路。
二、实现步骤
React-Native端的处理
RN端需要处理两种情况
- 应用没打开,被其它app启动
import { Linking } from "react-native";
Linking.getInitialURL().then((url) => {
// alert(url);
if (!!url) {
this.handleUrl(url);
}
}).catch(err => console.error('An error occurred', err));
注意,必须关闭debugger调试getInitialURL才会有值,否则获取到的值为null
- 应用已打开,被其它app启动
Linking.addEventListener('url', (e) => this.handleUrl(e.url));
handleUrl就是你处理原生路由跳转的的地方,rn一般使用react-navigation处理路由跳转,就拿我处理文件转发为例
代码如下(示例):
handleUrl = async (url) => {
console.log('Link url: ' + url);
/// url需要解码下获取文件实际路径
url = decodeURIComponent(url);
if (url.startsWith('file://')) {
console.log('march file share ');
try {
const stat = await RNFetchBlob.fs.stat(url.substr(7));
console.log('share file size: ' + stat.size);
if (stat.size === 0) {
Toast("文件为空,不能发送,请重新选择");
} else if (stat.size > FILE_SIZE_LIMIT.OFFLINE_FILE_MAX_SIZE) {
Toast("文件大小超过100M,请重新选择");
} else {
this.handleRoute('ForwardExternalAppFile', { filePath: url, fileSize: stat.size });
}
} catch (e) {
console.log(`handleUrl error: ${e}`);
}
return;
}
}
handleRoute = async (routeName, routeParams) => {
switch (routeName) {
case 'ForwardExternalAppFile': {
if (! await this.hasLogin()) {
Toast.info('抱歉,登录后才可继续使用焦谈分享功能')
return;
}
break;
}
}
_navigator.dispatch(
NavigationActions.navigate({
routeName,
params,
})
);
}
_navigator是什么呢,就是Navigation,rn入口render里通过ref获取暂存起来
<AppNavigator ref={navigatorRef => {
NavigationService.setTopLevelNavigator(navigatorRef);
}} />
到此RN端的处理基本就结束了,app根据业务逻辑进行相应的跳转即可,下面我们来处理原生端。
iOS端处理
ios处理deeplink,如从web页点击跳转到app里,需要在info.plist添加schema
在 URL Types 上添加一个 item
Identifier建议采用公司反转域名的方法保证该名字的唯一性,比如com.comname.appname
URL Schemes理论上随便填什么都可以,比如abiz
验证
在浏览器中输入abiz://,确认后就可以跳转到APP, 配置好了,拉起app时回调UIApplicationDelegate的方法
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
/// ......
return [RCTLinkingManager application:app openURL:url options:options];
}
我们看看 [RCTLinkingManager application:app openURL:url options:options]
的源码
+ (BOOL)application:(UIApplication *)app
openURL:(NSURL *)URL
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
postNotificationWithURL(URL, self);
return YES;
}
static void postNotificationWithURL(NSURL *URL, id sender)
{
NSDictionary<NSString *, id> *payload = @{@"url": URL.absoluteString};
[[NSNotificationCenter defaultCenter] postNotificationName:kOpenURLNotification
object:sender
userInfo:payload];
}
- (void)startObserving
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleOpenURLNotification:)
name:kOpenURLNotification
object:nil];
}
- (void)stopObserving
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)handleOpenURLNotification:(NSNotification *)notification
{
[self sendEventWithName:@"url" body:notification.userInfo];
}
RN端监听Linking的url事件时,startObserving 会调用,移除监听时stopObserving会调用,这样的话就不会再收到url事件了,为了事件一直都能接收到,只能在顶层入口监听事件,这样就和RN建立了连接,原理很简单,原生发送事件‘url’给RN,RN监听url进行路由的派发处理。当应用未打开时拉起app,原生启动入口回传递一些信息
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
NSLog(@"launchOptions:%@", launchOptions);
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"focustalk"
initialProperties:nil];
}
launchOptions包含了相关信息,应用未打开,被其他app启动时launchOptions
会包含UIApplicationLaunchOptionsURLKey
为键值的的url信息, 最后传到RCTBridge里存起来了,RN端调用Linking.getInitialURL()
返回启动url,源码如下
RCT_EXPORT_METHOD(getInitialURL:(RCTPromiseResolveBlock)resolve
reject:(__unused RCTPromiseRejectBlock)reject)
{
NSURL *initialURL = nil;
if (self.bridge.launchOptions[UIApplicationLaunchOptionsURLKey]) {
initialURL = self.bridge.launchOptions[UIApplicationLaunchOptionsURLKey];
} else {
NSDictionary *userActivityDictionary =
self.bridge.launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey];
if ([userActivityDictionary[UIApplicationLaunchOptionsUserActivityTypeKey] isEqual:NSUserActivityTypeBrowsingWeb]) {
initialURL = ((NSUserActivity *)userActivityDictionary[@"UIApplicationLaunchOptionsUserActivityKey"]).webpageURL;
}
}
resolve(RCTNullIfNil(initialURL.absoluteString));
}
至此RN和iOS原生之间如何建立路由通信的纽带讲完了,但我开篇提了以外部app分享文件到app为例讲解,iOS要处理外部app分享的文件需要进行一些设置,那就是info.plist,这个文件类似Android的AndroidManifest.xml,定义了app的一些能力和权限,要想处理应用外分享的文件,需要设置Document Types
info.plist直接右键以Source Code打开编辑即可
添加如下内容
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>item</string>
<key>Handler rank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>public.item</string>
</array>
</dict>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>content </string>
<key>Handler rank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>public.content</string>
</array>
</dict>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>composite-content</string>
<key>Handler rank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>public.composite-content</string>
</array>
</dict>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>data</string>
<key>Handler rank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>public.data</string>
</array>
</dict>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>database</string>
<key>Handler rank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>public.database</string>
</array>
</dict>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>calendar-event</string>
<key>Handler rank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>public.calendar-event</string>
</array>
</dict>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>message </string>
<key>Handler rank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>public.message</string>
</array>
</dict>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>contact </string>
<key>Handler rank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>public.contact</string>
</array>
</dict>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>archive </string>
<key>Handler rank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>public.archive</string>
</array>
</dict>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>disk-image </string>
<key>Handler rank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>public.disk-image</string>
</array>
</dict>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>text</string>
<key>Handler rank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>public.text</string>
</array>
</dict>
</array>
Android端处理
当点击的链接或程序化请求调用网页 URI intent 时,Android 系统会按顺序尝试执行以下每项操作,直到请求成功为止:
1.如果用户指定了可以处理该 URI 的首选应用,就打开此应用。
这个步骤需要app进行DeepLink配置,还需要服务端配合处理,具体怎么配置处理,查看官方文档https://developer.android.google.cn/training/app-links?hl=en
2.打开唯一可以处理该 URI 的应用。
3.允许用户从对话框中选择应用。
App端如何设置来响应对应的请求呢,首先需要Activity添加 intent 过滤器,以外部app分享文件到app为例
<activity
android:name="com.focus.focustalk.MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
android:label="@string/app_name"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
<data android:mimeType="image/*" />
<data android:mimeType="text/*" />
<data android:mimeType="application/*" />
</intent-filter>
</activity>
如果希望在已经启动的Activity接收Intent,需要设置 android:launchMode="singleTask"
这样,如果Activity已启动,就会调用onNewIntent回调,配置完后,通过系统的文件管理器点击分享就能看到我们的app可供选择了,但是不要高兴的太早,能启动app但是RN端怎么都接收不到相应的路由信息,找半天也没找到原因,最后通过看源码才找到原因,源码在IntentModule
里
/**
* Return the URL the activity was started with
*
* @param promise a promise which is resolved with the initial URL
*/
@Override
public void getInitialURL(Promise promise) {
try {
Activity currentActivity = getCurrentActivity();
String initialURL = null;
if (currentActivity != null) {
Intent intent = currentActivity.getIntent();
String action = intent.getAction();
Uri uri = intent.getData();
if (uri != null
&& (Intent.ACTION_VIEW.equals(action)
|| NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action))) {
initialURL = uri.toString();
}
}
promise.resolve(initialURL);
} catch (Exception e) {
promise.reject(
new JSApplicationIllegalArgumentException(
"Could not get the initial URL : " + e.getMessage()));
}
}
源码很简单,action只有是Intent.ACTION_VIEW或NfcAdapter.ACTION_NDEF_DISCOVERED
时才有路由url信息,找到原因就好办了,我们处理分享的Action是Intent.ACTION_SEND
,需要转成ACTION_VIEW
,重新设置intent
Intent newIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("file://" + filePath));
setIntent(newIntent);
@Override
protected void onCreate(@NonNull Bundle savedInstanceState) {
SplashScreen.show(this); // here
preProcessIntent(getIntent());
super.onCreate(savedInstanceState);
TMUtils.setStatusbg(this, R.color.color_1E79E8);
}
@Override
public void onNewIntent(Intent intent) {
preProcessIntent(intent);
super.onNewIntent(getIntent());
}
/**
* LinkManager只能处理ACTION_VIEW, 分享的action是ACTION_SEND, 所以需要转成对应的Intent,RN才能响应对应的事件
**/
protected void preProcessIntent(Intent intent) {
Bundle extras = intent.getExtras();
String action = intent.getAction();
String filePath = null;
// 判断Intent是否是“分享”功能(Share Via)
if (Intent.ACTION_SEND.equals(action)) {
if (extras.containsKey(Intent.EXTRA_STREAM)) {
try {
// 获取资源路径Uri
Uri uri = extras.getParcelable(Intent.EXTRA_STREAM);
filePath = FileUtil.getFileByUri(uri, getContentResolver());
Log.i("ForwardFileActivity", "uri:" + filePath);
} catch (Exception e) {
e.printStackTrace();
}
} else {
// 获取资源路径Uri
filePath = extras.getString("filePath");
}
if (null != filePath) {
Log.d("ACTION_SEND", filePath);
Intent newIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("file://" + filePath));
setIntent(newIntent);
}
}
}
转完后,一切正常,到此我们的原生路由和RN路由的通信机制和实现步骤讲完了