前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >React-Native 分包实践

React-Native 分包实践

作者头像
QQ音乐技术团队
发布2018-01-30 16:51:05
3.5K0
发布2018-01-30 16:51:05
举报
文章被收录于专栏:QQ音乐技术团队的专栏

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

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

  1. jsbundle文件如何生成

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

2.拆分jsbundle

通过上面的过程了解后,我们可以在原有的基础上进行扩展,在获取到denpendencies后(onResolutionResponse)通过请求参数,选择过滤基础模块或者仅基础模块,这时就能生成我们需要的文件。

代码语言:javascript
复制
//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的加载方式

代码语言:js
复制
////  ReactView.h
#import <UIKit/UIKit.h>
@interfaceReactView : UIView
- (instancetype)initWithFrame:(CGRect)frame module:(NSString*)module;
 @end
代码语言:javascript
复制
//  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 。

代码语言:js
复制
启动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造成额外消耗。

代码语言:javascript
复制
//  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的情况,这里就需要对生成拆分的业务模块有更多要求。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2016-04-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 QQ音乐技术团队 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 2.拆分jsbundle
  • 3.react-native加载文件
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档