參考自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;
}
先看一下效果:這就是最開始的方案,確實時可以實現的,但是可以看到,在調試時,每次由原生跳轉到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,看一下效果吧:后面項目完成了我會對ios/RN集成融云,以及ios端與RN的交互作一些總結,有問題歡迎評論探討,謝謝大家。