对于很多在使用react-native开发应用的小伙伴们肯定都会遇到一个问题,功能越来越复杂,生成的jsbundle文件越来越大,无论是打包在app内发布还是走http请求更新bunlde文件都是噩梦,这个时候我们应该如何来更新呢?来看看我们的方案。
我们可以在打包的时候直接讲基础文件打包到内部, 在请求线上的业务bundle合并后初始化react-native,对于在rn初始化后 如果还有新业务的话 也可以直接加载业务代码b 通过bridge enqueueApplicationScript注入到jscontext,再使用runAppcation 展示模块。 下面我们来看下这里实现的具体细节吧。
finalize会根据参数runMainModule在生成的代码插入执行代码,然后我们就能获得生成的bundle文件了。
通过上面的过程了解后,我们可以在原有的基础上进行扩展,在获取到denpendencies后(onResolutionResponse)通过请求参数,选择过滤基础模块或者仅基础模块,这时就能生成我们需要的文件。
//react-native/packager/react-packager/src/Bundler/index.js onResolutionResponse
if (withoutSource) {
response.dependencies = response.dependencies.filter(module =>
!~module.path.indexOf('react-native')
);
}else if (sourceOnly) {
response.dependencies = moduleSystemDeps.concat(response.dependencies.filter(module =>
~module.path.indexOf('react-native')
));
}
对于这里我们需要在Server中增加对应的参数透传给Bundler, 通过bundle命令的也需要在对应的local-cli/bundle下增加withoutSource、sourceOnly参数传递
实际业务中可以扩展这里的过滤方式,这里相对比较简单
通过上面的文件拆分生成之后,我们可以通过自定义ReactView的方式, 通过RCTBridgeDelegate扩展loadSourceForBridge的方法自定义bundle的加载方式
//// ReactView.h
#import <UIKit/UIKit.h>
@interfaceReactView : UIView
- (instancetype)initWithFrame:(CGRect)frame module:(NSString*)module;
@end
// ReactView.m
#import "ReactView.h
"#import "RCTRootView.h
"#import "ReactNativePackageManager.h"
@interface ReactView()<RCTBridgeDelegate>
@property(nonatomic, strong) NSString *moduleName;
@property(nonatomic, strong) RCTRootView *rootView;
@end
@implementation ReactView
- (instancetype)initWithFrame:(CGRect)frame
module:(NSString*)module
{
self = [super initWithFrame:frame];
if (self){
_moduleName = module;
RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:nil];
_rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:module initialProperties:nil];
}
[self addSubview:_rootView];
_rootView.frame = self.bounds;
return self;
}
#pragma mark -- RCTBridgeDelegate --
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
return [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true&withoutSource=true"];
}
- (void)loadSourceForBridge:(RCTBridge *)bridge withBlock:(RCTSourceLoadBlock)onComplete
{
NSURL *jsCodeLocation;
//这里需要加载本地的基础文件(main.jsbundle)
jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
NSString *filePath = jsCodeLocation.path; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSError *error = nil;
NSData *source = [NSData dataWithContentsOfFile:filePath
options:NSDataReadingMappedIfSafe
error:&error];
//加载线上模块合并初始化react-native
[ReactNativePackageManager load:_moduleName withBlock:^(NSError *error, NSData* data){
NSMutableData *concatData =[[NSMutableData alloc]init];
[concatData appendData:(NSData*)source];
[concatData appendData:(NSData*)data];
onComplete(nil, concatData);
}];
});
}
@end
在上述的代码中,我们会将本地打包好的基础文件读出然后再加载网络上的bundle文件初始化react-native 。
启动react-native app
#import "AppDelegate.h
"#import "ReactView.h"
@implementationAppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary
*)launchOptions
{
//设置初始化模块名称
ReactView *reactView = [[ReactView alloc]
initWithFrame:[UIScreen mainScreen].bounds
module:@"testApp"];
self.window = [[UIWindow alloc] initWithFrame:
[UIScreen mainScreen].bounds];
UIViewController *rootViewController =
[UIViewController new];
rootViewController.view = reactView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
returnYES;
}
@end
4.按需加载jsbundle
对于需要异步加载的模块,我们可以扩展Native Module方式增加异步加载功能,同时修改RCTbridge暴露enqueueApplicationScript接口来将加载后的source运行到javascript core, 同时我们讲模块的加载统一管理起来保证不会重复加载和插入jscore造成额外消耗。
// ReactNativePackageManager.m
#import "ReactNativePackageManager.h"
#import "RCTBridge.h"
#import "RCTUtils.h"
#import "RCTPerformanceLogger.h"
#import "RCTBridgeDelegate.h"
@implementation ReactNativePackageManager
RCT_EXPORT_MODULE();
@synthesize bridge = _bridge;
static NSMutableDictionary *modules;
//实际使用中根据业务设置加载的链接和规则
static NSString *url = @"http://localhost:8081/%@.ios.bundle?platform=ios&dev=false&withoutSource=true";
+(void) load:(NSString *)module withBlock:(RCTSourceLoadBlock)onComplete
{
if (!modules) {
modules =[NSMutableDictionary new];
}
if ([modules objectForKey:module]) {
onComplete(nil, modules[module]);
}else{
NSURL *scriptURL = [NSURL URLWithString:[NSString stringWithFormat:url, module]];
// Load remote script file
NSURLSessionDataTask *task =
[[NSURLSession sharedSession] dataTaskWithURL:scriptURL
completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error) {
// Handle general request errors
if (error) {
onComplete(error, nil);
return;
}
// Parse response as text
NSStringEncoding encoding = NSUTF8StringEncoding;
if (response.textEncodingName != nil) {
CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName); if (cfEncoding != kCFStringEncodingInvalidId) {
encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding);
}
}
// Handle HTTP errors
if ([response isKindOfClass:[NSHTTPURLResponse class]] && ((NSHTTPURLResponse *)response).statusCode != 200) {
NSString *rawText = [[NSString alloc] initWithData:data encoding:encoding];
NSDictionary *userInfo;
NSDictionary *errorDetails = RCTJSONParse(rawText, nil);
if ([errorDetails isKindOfClass:[NSDictionary class]] &&
[errorDetails[@"errors"] isKindOfClass:[NSArray class]]) {
NSMutableArray<NSDictionary *> *fakeStack = [NSMutableArray new];
for (NSDictionary *err in errorDetails[@"errors"]) {
[fakeStack addObject: @{
@"methodName": err[@"description"] ?: @"",
@"file": err[@"filename"] ?: @"",
@"lineNumber": err[@"lineNumber"] ?: @0
}];
}
userInfo = @{
NSLocalizedDescriptionKey: errorDetails[@"message"] ?: @"No message provided",
@"stack": fakeStack,
};
} else {
userInfo = @{NSLocalizedDescriptionKey: rawText};
}
error = [NSError errorWithDomain:@"JSServer"
code:((NSHTTPURLResponse *)response).statusCode
userInfo:userInfo];
onComplete(error, data);
return;
}
[modules setObject:data forKey:module];
onComplete(nil, data);
}];
[task resume];
}
};
-(void) runModule:(NSString *)moduleName
{ NSDictionary *appParameters = @
{
@"rootTag": @1,
@"initialProps": @{},
};
[_bridge enqueueJSCall:@"AppRegistry.runApplication"
args:@[moduleName, appParameters]];
}
RCT_EXPORT_METHOD(loadModule:(NSString *) moduleName)
{
if ([modules objectForKey:moduleName]) {
[self runModule:moduleName];
}else{
[ReactNativePackageManager load:moduleName withBlock:^(NSError *error, NSData* data){
[_bridge enqueueApplicationScript:data url:[_bridge bundleURL] onComplete:^(NSError *scriptLoadError) {
[self runModule:moduleName];
}];
}];
}
}
@end
调用的话相应的要使用NativeModules.ReactNativePackageManager.loadModule('moduleName');
同时通过统一的load方式保证模块不会重复加载,这里在加载失败的情况下还可以考虑更多走到erroView来处理展示。
这样我们就基本完成了拆分工作,保证加载的bundle文件最小化。react-native自身需要加载多模块的话 也可以通过这样的方式调用直接注入到jscontext运行。
实际业务中 js模块还有需要解决多个Component共同依赖通过js module的情况,这里就需要对生成拆分的业务模块有更多要求。