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

序言

公司要搞跨平台方案,由于之前移动端页面多数由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 <Foundation/Foundation.h>


@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 <React/RCTBridge+Private.h>

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吧!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
React Native提供了一个名为`react-native-video`的第三方库,它可以在iOS和Android平台上实现视频播放功能。 下面是在iOS平台上使用`react-native-video`库实现视频播放的基本步骤: 1. 安装`react-native-video`库 可以通过npm或yarn安装: ``` npm install react-native-video ``` 或者 ``` yarn add react-native-video ``` 2. 在Xcode中配置`react-native-video`库 打开你的React Native项目的ios文件夹,找到.xcodeproj文件并打开,在Xcode中添加`react-native-video`库。 3. 导入`react-native-video`库 在需要使用视频播放的组件中导入`react-native-video`库: ``` import Video from 'react-native-video'; ``` 4. 添加视频播放器 在需要播放视频的组件中添加`Video`组件: ``` <Video source={{ uri: "https://example.com/video.mp4" }} // 视频的URL地址 ref={(ref) => { this.player = ref }} // 视频播放器的引用 style={styles.backgroundVideo} // 视频播放器的样式 controls={true} // 是否显示播放器控制条 /> ``` 5. 控制视频播放 你可以通过控制`Video`组件的状态来控制视频的播放,例如: ``` this.player.seek(0); // 将当前播放位置设置为0 this.player.pause(); // 暂停视频播放 this.player.play(); // 开始播放视频 ``` 以上是基本的视频播放功能实现,你还可以通过`react-native-video`库提供的其他API来增加更多的功能,例如: - 设置视频的缩放模式 - 实现视频的全屏播放 - 获取视频的播放时长和当前播放位置等 希望这些信息对你有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值