参考自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的交互作一些总结,有问题欢迎评论探讨,谢谢大家。