React-Native deeplink处理

一、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路由的通信机制和实现步骤讲完了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值