React Native 分包及在iOS端分包加载

React Native 分包及在iOS端分包加载_第1张图片

序言

公司要搞跨平台方案,由于之前移动端页面多数由H5来写,公司javaScript技术栈人员比较多,而且项目之前使用了H5热更新方案,还想继续使用热更新来解决线上问题,所以最终决定使用React Native。


RN端分包(拆包)

众所周知,iOS原生端加载React Native代码主要是通过加载转化后的jsBundle来实现的。有的项目从一开始就是用纯RN开发,就只需要加载一个index.ios.jsbundle。但是我司的项目是在Hybrid的基础上添加React Native模块,还需要热更新, 为了每次热更新包的大小是尽可能小的, 所以需要使用分包加载。

我这里使用的是基于Metro的分包方法,也是市面上主要使用的方法。这个拆包方法主要关注的是Metro工具在“序列化”阶段时调用的 createModuleIdFactory(path)方法和processModuleFilter(module)createModuleIdFactory(path)是传入的模块绝对路径path,并为该模块返回一个唯一的IdprocessModuleFilter(module)则可以实现对模块进行过滤,使业务模块的内容不会被写到common模块里。接下来分具体步骤和代码进行讲解。

1. 先建立一个 common.js 文件,里面引入了所有的公有模块

require('react')
require('react-native')
...

2. Metro 以这个 common.js 为入口文件,打一个 common bundle 包,同时要记录所有的公有模块的 moduleId但是存在一个问题:每次启动 Metro 打包的时候,moduleId 都是从 0 开始自增,这样会导致不同的 JSBundle ID 重复为了避免 id 重复,目前业内主流的做法是把模块的路径当作 moduleId(因为模块的路径基本上是固定且不冲突的),这样就解决了 id 冲突的问题。Metro 暴露了 createModuleIdFactory 这个函数,我们可以在这个函数里覆盖原来的自增逻辑,把公有模块的 moduleId 写入 txt文件

I) 在package.json里配置命令:

"build:common:ios": "rimraf moduleIds_ios.txt && react-native bundle --entry-file common.js --platform ios --config metro.common.config.ios.js --dev false --assets-dest ./bundles/ios --bundle-output ./bundles/ios/common.ios.jsbundle",

II)metro.common.config.ios.js文件

const fs = require('fs');
const pathSep = require('path').sep;


function createModuleId(path) {
  const projectRootPath = __dirname;
  let moduleId = path.substr(projectRootPath.length + 1);

  let regExp = pathSep == '\\' ? new RegExp('\\\\', "gm") : new RegExp(pathSep, "gm");
  moduleId = moduleId.replace(regExp, '__');
  return moduleId;
}

module.exports = {
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: true,
      },
    }),
  },
  serializer: {
    createModuleIdFactory: function () {
      return function (path) {
        const moduleId = createModuleId(path)

        fs.appendFileSync('./moduleIds_ios.txt', `${moduleId}\n`);
        return moduleId;
      };
    },
  },
};

III)生成的moduleIds_ios.txt文件

common.js
node_modules__react__index.js
node_modules__react__cjs__react.production.min.js
node_modules__object-assign__index.js
node_modules__react-native__index.js
node_modules__react-native__Libraries__Components__AccessibilityInfo__AccessibilityInfo.ios.js
......

3. 打包完公有模块,开始打包业务模块。这个步骤的关键在于过滤公有模块的 moduleId(公有模块的Id已经记录在了上一步的moduleIds_ios.txt中),Metro 提供了 processModuleFilter 这个方法,借助它可以实现模块的过滤。这部分的处理主要写在了metro.business.config.ios.js文件中,写在哪个文件中主要取决于最上面package.json命令里指定的文件。

const fs = require('fs');
const pathSep = require('path').sep;

const moduleIds = fs.readFileSync('./moduleIds_ios.txt', 'utf8').toString().split('\n');

function createModuleId(path) {
  const projectRootPath = __dirname;
  let moduleId = path.substr(projectRootPath.length + 1);

  let regExp = pathSep == '\\' ? new RegExp('\\\\', "gm") : new RegExp(pathSep, "gm");
  moduleId = moduleId.replace(regExp, '__');
  return moduleId;
}

module.exports = {
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: true,
      },
    }),
  },
  serializer: {
    createModuleIdFactory: function () {
      return createModuleId;
    },
    processModuleFilter: function (modules) {
      const mouduleId = createModuleId(modules.path);

      if (modules.path == '__prelude__') {
        return false
      }
      if (mouduleId == 'node_modules__metro-runtime__src__polyfills__require.js') {
        return false
      }

      if (moduleIds.indexOf(mouduleId) < 0) {
        return true;
      }
      return false;
    },
    getPolyfills: function() {
      return []
    }
  },
  resolver: {
    sourceExts: ['jsx', 'js', 'ts', 'tsx', 'cjs', 'mjs'],
  },
};

综上,React Native端的分包工作就大致完成了。


iOS端分包加载

1. iOS端首先需要加载公共包

-(void) prepareReactNativeCommon{
  NSDictionary *launchOptions = [[NSDictionary alloc] init];
  self.bridge = [[RCTBridge alloc] initWithDelegate:self
                                      launchOptions:launchOptions];
}

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{   
    return  [NSURL URLWithString:[[self getDocument] stringByAppendingPathComponent:@"bundles/ios/common.ios.jsbundle"]];
}

2. 加载完公共包就需要加载业务包,加载业务包需要使用executeSourceCode方法,但是这是RCTBridge的一个私有方法,需要建立一个RCTBridge的分类,只有.h文件即可,通过Runtime机制会最终找到内部的executeSourceCode方法实现。

#import 


@interface RCTBridge (ALCategory) // 暴露RCTBridge的私有接口

- (void)executeSourceCode:(NSData *)sourceCode sync:(BOOL)sync;

@end
-(RCTBridge *) loadBusinessBridgeWithBundleName:(NSString*) fileName{
    
    NSString * busJsCodeLocation = [[self getDocument] stringByAppendingPathComponent:fileName];
    NSError *error = nil;

    NSData * sourceData = [NSData dataWithContentsOfFile:busJsCodeLocation options:NSDataReadingMappedIfSafe error:&error];
    NSLog(@"%@", error);

    [self.bridge.batchedBridge  executeSourceCode:sourceData sync:NO];
    
    return self.bridge;
}

- (NSString *)getDocument {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *path = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"app"];
    return path;
}

完整的加载公共包和业务包的ALAsyncLoadManager类的代码:

#import "ALAsyncLoadManager.h"
#import "RCTBridge.h"
#import 

static ALAsyncLoadManager *instance;

@implementation ALAsyncLoadManager

+ (ALAsyncLoadManager *) getInstance{
  @synchronized(self) {
    if (!instance) {
      instance = [[self alloc] init];
    }
  }
  return instance;
}

-(void) prepareReactNativeCommon{
  NSDictionary *launchOptions = [[NSDictionary alloc] init];
  self.bridge = [[RCTBridge alloc] initWithDelegate:self
                                      launchOptions:launchOptions];
}

-(RCTBridge *) loadBusinessBridgeWithBundleName:(NSString*) fileName{
    
    NSString * busJsCodeLocation = [[self getDocument] stringByAppendingPathComponent:fileName];
    NSError *error = nil;

    NSData * sourceData = [NSData dataWithContentsOfFile:busJsCodeLocation options:NSDataReadingMappedIfSafe error:&error];
    NSLog(@"%@", error);

    [self.bridge.batchedBridge  executeSourceCode:sourceData sync:NO];
    
    return self.bridge;
}

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{   
    return  [NSURL URLWithString:[[self getDocument] stringByAppendingPathComponent:@"bundles/ios/common.ios.jsbundle"]];
}

- (NSString *)getDocument {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *path = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"app"];
    return path;
}

3. 还有一个比较重要的点,由于公共包和业务包是分开加载的,需要加载完公共包再加载业务包,还要求速度要尽可能快,网上有些说一启动App就用初始化Bridge加载公共包,我认为这是一种很懒的处理方式,会很耗性能,我这里采用的是监听加载完公共包的通知,一旦收到加载完公共包的通知,就开始加载业务包。

@interface ALRNContainerController ()
@property (strong, nonatomic) RCTRootView *rctContainerView;
@end

@implementation ALRNContainerController

- (void)viewDidLoad {

[[NSNotificationCenter defaultCenter] addObserver:self
                                      selector:@selector(loadRctView)                                                                
                                      name:RCTJavaScriptDidLoadNotification         
                                      object:nil];
}

- (void)loadRctView {
    [self.view addSubview:self.rctContainerView];
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (RCTRootView *)rctContainerView {
            
            RCTBridge* bridge = [[MMAsyncLoadManager getInstance] buildBridgeWithDiffBundleName:[NSString stringWithFormat:@"bundles/ios/%@.ios.jsbundle",_moduleName]];
            
            _rctContainerView = [[RCTRootView alloc] initWithBridge:bridge moduleName:_moduleName initialProperties:@{
                @"user" : @"用户信息",
                @"params" : @"参数信息"
            }];
        
        _rctContainerView.frame = UIScreen.mainScreen.bounds;
        return _rctContainerView;
}

- (void)dealloc
{
    //清理bridge,减少性能消耗
    [ALAsyncLoadManager getInstance].bridge = nil;
    [self.rctContainerView removeFromSuperview];
    self.rctContainerView = nil;
}

综上,iOS端的分包加载工作就大致完成了。


以上就是React Native端和原生端分包加载的所有流程,接下来还有热更新的实现和项目中使用到的组件库的实现, 感觉有用的话接给个Star吧!

你可能感兴趣的:(移动开发iOS,移动开发,前端学习,react,native,ios,react.js)