參考自github作者:ljunb 文章鏈接:https://github.com/ljunb/rn-relates/issues/2
這里感謝下這位作者!
前言
最近的RN項(xiàng)目中要引入融云sdk實(shí)現(xiàn)即時(shí)聊天鲁纠,這樣就需要原生與RN的混編了魄衅,現(xiàn)在大部分的技術(shù)點(diǎn)的都攻克了(僅ios端),遇到了很多問(wèn)題苍在,都值得記錄一下绝页,這里就先講一講在ios端跳轉(zhuǎn)到RN頁(yè)面的問(wèn)題吧,下面開(kāi)始:
一寂恬、初始方案
我的應(yīng)用中续誉,會(huì)話列表頁(yè)面是RN頁(yè)面,會(huì)話頁(yè)面則完全是一個(gè)原生頁(yè)面掠剑,直接用的融云的UI屈芜。在會(huì)話頁(yè)面中點(diǎn)擊導(dǎo)航欄右側(cè)按鈕想要跳轉(zhuǎn)到RN頁(yè)面去實(shí)現(xiàn),跳轉(zhuǎn)時(shí)還需要傳遞給RN當(dāng)前用戶id等參數(shù)朴译,我最初的方案如下:
新建一個(gè)RN應(yīng)用井佑,在代碼中我們可以看到,其用以下方法去創(chuàng)建加載RN應(yīng)用:
NSURL *jsCodeLocation;
jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"RNChatDemo"
initialProperties:nil
launchOptions:launchOptions];
可以看到眠寿,在初始化RCTRootView實(shí)例時(shí)躬翁,需要傳入moduleName和initialProperties這兩個(gè)參數(shù),先說(shuō)說(shuō)這2個(gè)參數(shù):
- moduleName
在RN端的入口中盯拱,我們用AppRegistry的registerComponent方法來(lái)注冊(cè)組件盒发,其第一個(gè)參數(shù)對(duì)應(yīng)的便是ios中的moduleName,所以我們可以在RN入口處注冊(cè)多個(gè)組件狡逢,在ios端我們需要用的哪個(gè)RN組件時(shí)宁舰,便改變moduleName值來(lái)加載這個(gè)RN組件即可。
//AppRegistry的registerComponent方法
registerComponent(
appKey: string,
componentProvider: ComponentProvider,
section?: boolean,
)
//RN端入口index.js
AppRegistry.registerComponent('RNChatDemo', () => App);
AppRegistry.registerComponent('RNPage', () => RNPage);
- initialProperties
我們可以通過(guò)initialProperties可以向RN組件傳遞初始參數(shù):
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>
)
}
所以我最開(kāi)始的方案是:創(chuàng)建多個(gè)ViewController,每個(gè)ViewController對(duì)應(yīng)加載一個(gè)RN的頁(yè)面奢浑,加載RN的方法即是上面的rootView的initWithBundleURL方法蛮艰,通過(guò)不同moduleName來(lái)加載不同的RN頁(yè)面,在viewDidLoad時(shí)加載雀彼,加載后賦給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;
}
先看一下效果:這就是最開(kāi)始的方案即寡,確實(shí)時(shí)可以實(shí)現(xiàn)的,但是可以看到袜刷,在調(diào)試時(shí)聪富,每次由原生跳轉(zhuǎn)到RN時(shí),都會(huì)有bundle加載的進(jìn)度條著蟹,進(jìn)度條加載完成后才將RN頁(yè)面加載出來(lái)墩蔓,估計(jì)打包后,每次跳轉(zhuǎn)會(huì)有一個(gè)短暫白屏的過(guò)程草则,這樣的話體驗(yàn)就不好了钢拧,所以尋求一個(gè)更好的方案,github中看到來(lái)作者ljunb的方案炕横,非常受用源内,這里再次感謝??,下面就開(kāi)始對(duì)這個(gè)方案進(jìn)行優(yōu)化
二份殿、方案優(yōu)化
我們首先剖析一下膜钓,打開(kāi)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;
注釋中說(shuō)的很明確了卿嘲,initWithBundleURL這個(gè)方法會(huì)先建一個(gè)RCTBridge的實(shí)例颂斜,方法適用于整個(gè)app只有一個(gè)RCTRootView的情況,在有多個(gè)RCTBridge的情況下拾枣,我們可以先建立一個(gè)全局的bridge沃疮,使用initWithBridge這個(gè)方法去展示RN,接下來(lái)我們具體看下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方法,如果我們像初始方案一樣姨蝴,每次都執(zhí)行initWithBundleURL方法俊啼,那么每次都會(huì)初始化一個(gè)RCTBridge實(shí)例,會(huì)占用更多的時(shí)間和資源開(kāi)銷左医,同時(shí)每次在用到RN頁(yè)面的時(shí)候才去加載bundle資源授帕,會(huì)有一段白屏?xí)r間,因此給出的優(yōu)化方案是:
- 建立一個(gè)NSObject類浮梢,讓其實(shí)現(xiàn)RCTBridgeDelegate協(xié)議
- 這個(gè)類添加一個(gè)bridge屬性作為一個(gè)全局的bridge跛十,每一次新建RN頁(yè)面使用這個(gè)bridge
- 類中實(shí)現(xiàn)預(yù)加載方法,在適當(dāng)?shù)臅r(shí)候可以預(yù)加載RCTRootView
- 類中實(shí)現(xiàn)RCTRootView的管理秕硝,將預(yù)加載的RCTRootView保存起來(lái)偶器,在用到的時(shí)候直接提取
這個(gè)類的具體的實(shí)現(xiàn)如下:
//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;
/*
* 根據(jù)viewName預(yù)加載bundle文件
* param:
* viewName RN界面名稱
* initialProperty: 初始化參數(shù)
*/
- (void)preLoadRootViewWithName:(NSString *)viewName;
- (void)preLoadRootViewWithName:(NSString *)viewName initialProperty:(NSDictionary *)initialProperty;
/*
* 根據(jù)viewName獲取rootView
* param:
* viewName RN界面名稱
*
* return: 返回匹配的rootView
*/
- (RCTRootView *)rootViewWithName:(NSString *)viewName;
@end
具體的.m文件實(shí)現(xiàn):
//ReactRootViewManager.m
#import "ReactRootViewManager.h"
@interface ReactRootViewManager ()
// 以 viewName-rootView 的形式保存需預(yù)加載的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創(chuàng)建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
在我的項(xiàng)目中,我在需要預(yù)加載的VC的viewDidLoad中實(shí)現(xiàn)預(yù)加載:
//配置initialProperties
NSMutableDictionary *initialProperties = [NSMutableDictionary dictionary];
[initialProperties setObject: [RCTRongCloud _convertConversationType:self.conversationType] forKey:@"type"];
[initialProperties setObject:self.targetId forKey:@"targetId"];
//RN頁(yè)面預(yù)加載
NSString *pageName = @"RNPage";
[[ReactRootViewManager manager] preLoadRootViewWithName:pageName initialProperty:initialProperties];
在RN頁(yè)面所在的ViewController中,在viewDidLoad里將預(yù)加載的rootView賦給self.view :
//RNPageViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
self.view=[[ReactRootViewManager manager] rootViewWithName:@"RNPage"];
}
最后,在由ios跳轉(zhuǎn)到RN的方法里屏轰,直接push上面的ViewController :
RNPageViewController *RNPageVC = [[RNPageViewController alloc] init];
[self.navigationController pushViewController:RNPageVC animated:YES];
ok,看一下效果吧:后面項(xiàng)目完成了我會(huì)對(duì)ios/RN集成融云憋飞,以及ios端與RN的交互作一些總結(jié)霎苗,有問(wèn)題歡迎評(píng)論探討,謝謝大家榛做。