ios 原生 跳轉到不同的RN頁面

參考自github作者:ljunb 文章鏈接:https://github.com/ljunb/rn-relates/issues/2
這里感謝下這位作者!

前言

最近的RN項目中要引入融云sdk實現即時聊天,這樣就需要原生與RN的混編了,現在大部分的技術點的都攻克了(僅ios端),遇到了很多問題,都值得記錄一下,這里就先講一講在ios端跳轉到RN頁面的問題吧,下面開始:

一、初始方案

我的應用中,會話列表頁面是RN頁面,會話頁面則完全是一個原生頁面,直接用的融云的UI。在會話頁面中點擊導航欄右側按鈕想要跳轉到RN頁面去實現,跳轉時還需要傳遞給RN當前用戶id等參數,我最初的方案如下:

新建一個RN應用,在代碼中我們可以看到,其用以下方法去創建加載RN應用:

  NSURL *jsCodeLocation;
  jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
  RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                      moduleName:@"RNChatDemo"
                                               initialProperties:nil
                                                   launchOptions:launchOptions];

可以看到,在初始化RCTRootView實例時,需要傳入moduleName和initialProperties這兩個參數,先說說這2個參數:

  • moduleName

在RN端的入口中,我們用AppRegistry的registerComponent方法來注冊組件,其第一個參數對應的便是ios中的moduleName,所以我們可以在RN入口處注冊多個組件,在ios端我們需要用的哪個RN組件時,便改變moduleName值來加載這個RN組件即可。

//AppRegistry的registerComponent方法
registerComponent(
    appKey: string,
    componentProvider: ComponentProvider,
    section?: boolean,
  )
  
  
//RN端入口index.js
AppRegistry.registerComponent('RNChatDemo', () => App);
AppRegistry.registerComponent('RNPage', () => RNPage);
  • initialProperties
    我們可以通過initialProperties可以向RN組件傳遞初始參數:
NSMutableDictionary *initialProperty = [NSMutableDictionary dictionary];
[initialProperty setObject:self.targetId forKey:@"targetId"];

RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                    moduleName:@"RNPage"
                                             initialProperties:initialProperty
                                                 launchOptions:nil];

RN端直接用this.props接收:

render(){
        return(
            <View style={styles.container}>
                <Text style={{fontSize:20}}>{this.props.targetId}}</Text>
            </View>
        )
    }

所以我最開始的方案是:創建多個ViewController,每個ViewController對應加載一個RN的頁面,加載RN的方法即是上面的rootView的initWithBundleURL方法,通過不同moduleName來加載不同的RN頁面,在viewDidLoad時加載,加載后賦給self.view即可。

- (void)viewDidLoad {
  [super viewDidLoad];
  
   NSURL *jsCodeLocation;
    jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
    RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                        moduleName:@"RNPage"
                                                 initialProperties:nil
                                                     launchOptions:nil];
    self.view = rootView;
}

先看一下效果:
初始方案.gif

這就是最開始的方案,確實時可以實現的,但是可以看到,在調試時,每次由原生跳轉到RN時,都會有bundle加載的進度條,進度條加載完成后才將RN頁面加載出來,估計打包后,每次跳轉會有一個短暫白屏的過程,這樣的話體驗就不好了,所以尋求一個更好的方案,github中看到來作者ljunb的方案,非常受用,這里再次感謝??,下面就開始對這個方案進行優化

二、方案優化

我們首先剖析一下,打開initWithBundleURL方法源碼,我們先看看在頭文件中的官方注釋:

/**
 * - Designated initializer -
 */
- (instancetype)initWithBridge:(RCTBridge *)bridge
                    moduleName:(NSString *)moduleName
             initialProperties:(NSDictionary *)initialProperties NS_DESIGNATED_INITIALIZER;

/**
 * - Convenience initializer -
 * A bridge will be created internally.
 * This initializer is intended to be used when the app has a single RCTRootView,
 * otherwise create an `RCTBridge` and pass it in via `initWithBridge:moduleName:`
 * to all the instances.
 */
- (instancetype)initWithBundleURL:(NSURL *)bundleURL
                       moduleName:(NSString *)moduleName
                initialProperties:(NSDictionary *)initialProperties
                    launchOptions:(NSDictionary *)launchOptions;

注釋中說的很明確了,initWithBundleURL這個方法會先建一個RCTBridge的實例,方法適用于整個app只有一個RCTRootView的情況,在有多個RCTBridge的情況下,我們可以先建立一個全局的bridge,使用initWithBridge這個方法去展示RN,接下來我們具體看下initWithBundleURL的源碼,看看是不是這樣:

- (instancetype)initWithBundleURL:(NSURL *)bundleURL
                       moduleName:(NSString *)moduleName
                initialProperties:(NSDictionary *)initialProperties
                    launchOptions:(NSDictionary *)launchOptions
{
  RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:bundleURL
                                            moduleProvider:nil
                                             launchOptions:launchOptions];

  return [self initWithBridge:bridge moduleName:moduleName initialProperties:initialProperties];
  
}

可以看到,源碼中便是使用的initWithBridge方法,如果我們像初始方案一樣,每次都執行initWithBundleURL方法,那么每次都會初始化一個RCTBridge實例,會占用更多的時間和資源開銷,同時每次在用到RN頁面的時候才去加載bundle資源,會有一段白屏時間,因此給出的優化方案是:

  • 建立一個NSObject類,讓其實現RCTBridgeDelegate協議
  • 這個類添加一個bridge屬性作為一個全局的bridge,每一次新建RN頁面使用這個bridge
  • 類中實現預加載方法,在適當的時候可以預加載RCTRootView
  • 類中實現RCTRootView的管理,將預加載的RCTRootView保存起來,在用到的時候直接提取

這個類的具體的實現如下:

//ReactRootViewManager.h

#import <React/RCTRootView.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTBridge.h>

@interface ReactRootViewManager : NSObject<RCTBridgeDelegate>

/* 全局唯一的bridge */
@property (nonatomic, strong, readonly) RCTBridge * bridge;

/*
 * 獲取單例
 */
+ (instancetype)manager;

/*
 * 根據viewName預加載bundle文件
 * param:
 *     viewName RN界面名稱
 *     initialProperty: 初始化參數
 */
- (void)preLoadRootViewWithName:(NSString *)viewName;
- (void)preLoadRootViewWithName:(NSString *)viewName initialProperty:(NSDictionary *)initialProperty;

/*
 * 根據viewName獲取rootView
 * param:
 *     viewName RN界面名稱
 *
 * return: 返回匹配的rootView
 */
- (RCTRootView *)rootViewWithName:(NSString *)viewName;

@end

具體的.m文件實現:

//ReactRootViewManager.m
#import "ReactRootViewManager.h"

@interface ReactRootViewManager ()
// 以 viewName-rootView 的形式保存需預加載的RN界面
@property (nonatomic, strong) NSMutableDictionary<NSString *, RCTRootView*> * rootViewMap;
@end

@implementation ReactRootViewManager

- (void)dealloc {
  _rootViewMap = nil;
  [_bridge invalidate];
}

+ (instancetype)manager {
  static ReactRootViewManager * _rootViewManager = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    _rootViewManager = [[ReactRootViewManager alloc] init];
  });
  return _rootViewManager;
}

- (instancetype)init {
  if (self = [super init]) {
    _rootViewMap = [NSMutableDictionary dictionaryWithCapacity:0];
    _bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:nil];
  }
  return self;
}

- (void)preLoadRootViewWithName:(NSString *)viewName {
  [self preLoadRootViewWithName:viewName initialProperty:nil];
}

- (void)preLoadRootViewWithName:(NSString *)viewName initialProperty:(NSDictionary *)initialProperty {
  if (!viewName && [_rootViewMap objectForKey:viewName]) {
    return;
  }
  // 由bridge創建rootView
  RCTRootView * rnView = [[RCTRootView alloc] initWithBridge:self.bridge
                                                  moduleName:viewName
                                           initialProperties:initialProperty];
  [_rootViewMap setObject:rnView forKey:viewName];
}

- (RCTRootView *)rootViewWithName:(NSString *)viewName {
  if (!viewName) {
    return nil;
  }
  return [self.rootViewMap objectForKey:viewName];
}

#pragma mark - RCTBridgeDelegate
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
  return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
}

@end

在我的項目中,我在需要預加載的VC的viewDidLoad中實現預加載:

//配置initialProperties
  NSMutableDictionary *initialProperties = [NSMutableDictionary dictionary];
  [initialProperties setObject: [RCTRongCloud _convertConversationType:self.conversationType] forKey:@"type"];
  [initialProperties setObject:self.targetId forKey:@"targetId"];
  
  //RN頁面預加載
  NSString *pageName = @"RNPage";
  [[ReactRootViewManager manager] preLoadRootViewWithName:pageName initialProperty:initialProperties];

在RN頁面所在的ViewController中,在viewDidLoad里將預加載的rootView賦給self.view :

//RNPageViewController.m
- (void)viewDidLoad {
  [super viewDidLoad];
  self.view=[[ReactRootViewManager manager] rootViewWithName:@"RNPage"];
}

最后,在由ios跳轉到RN的方法里,直接push上面的ViewController :

RNPageViewController *RNPageVC = [[RNPageViewController alloc] init];
[self.navigationController pushViewController:RNPageVC animated:YES];

ok,看一下效果吧:
優化方案.gif

后面項目完成了我會對ios/RN集成融云,以及ios端與RN的交互作一些總結,有問題歡迎評論探討,謝謝大家。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 雙親日 今天,父親節。這也許是我和父親能夠共同度過的最后一個父親節。40多年在一起的歲月深深烙在我的骨子里,無法割...
    繁星如海閱讀 302評論 1 2