React-Native 分包实践

时间:2022-04-25
本文章向大家介绍React-Native 分包实践,主要内容包括2.拆分jsbundle、3.react-native加载文件、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

对于很多在使用react-native开发应用的小伙伴们肯定都会遇到一个问题,功能越来越复杂,生成的jsbundle文件越来越大,无论是打包在app内发布还是走http请求更新bunlde文件都是噩梦,这个时候我们应该如何来更新呢?来看看我们的方案。

我们可以在打包的时候直接讲基础文件打包到内部, 在请求线上的业务bundle合并后初始化react-native,对于在rn初始化后 如果还有新业务的话 也可以直接加载业务代码b 通过bridge enqueueApplicationScript注入到jscontext,再使用runAppcation 展示模块。 下面我们来看下这里实现的具体细节吧。

  1. jsbundle文件如何生成

finalize会根据参数runMainModule在生成的代码插入执行代码,然后我们就能获得生成的bundle文件了。

2.拆分jsbundle

通过上面的过程了解后,我们可以在原有的基础上进行扩展,在获取到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参数传递

实际业务中可以扩展这里的过滤方式,这里相对比较简单

3.react-native加载文件

通过上面的文件拆分生成之后,我们可以通过自定义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的情况,这里就需要对生成拆分的业务模块有更多要求。