React Native サブパッケージと iOS 側でのサブパッケージの読み込み

前文

同社はクロスプラットフォーム ソリューションを開発したいと考えており、以前はほとんどのモバイル ページが H5 によって作成されていたため、同社には多数の javaScript テクノロジ スタック担当者がおり、プロジェクトの前に H5 ホット アップデート ソリューションが使用されていました。オンラインの問題を解決するために引き続きホット アップデートを使用する必要があるため、最終的には React Native を使用することにしました。


RN エンド サブパッケージ (開梱)

ご存知のように、iOS ネイティブ側は、主に変換された jsBundle をロードすることによって React Native コードをロードします。一部のプロジェクトは最初から純粋な RN で開発されており、index.ios.jsbundle をロードするだけで済みます。ただし、Hybrid に基づいて React Native モジュールを追加するプロジェクトであり、ホット アップデートも必要です.各ホット アップデート パッケージのサイズをできるだけ小さく保つために、サブパッケージのロードを使用する必要があります.

ここで使用するのは、市場で使用される主要な方法でもある Metro ベースの下請け方法です。このアンパック方法は、主に「シリアライゼーション」段階で Metro ツールによって呼び出される createModuleIdFactory(path)メソッドとメソッドに関連していますprocessModuleFilter(module)createModuleIdFactory(path)渡されたモジュールの絶対パスでありpath、そのモジュールに固有のものを返しますIdprocessModuleFilter(module)次に、ビジネス モジュールの内容が共通モジュールに書き込まれないように、モジュールをフィルター処理できます。次に、具体的な手順とコードについて説明します。

1. まず、すべてのパブリック モジュールをインポートする common.js ファイルを作成します。

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

2. Metro はこの common.js をエントリ ファイルとして使用して共通バンドル パッケージを作成し、同時にすべての公開モジュールの 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のprivateメソッドであり、RCTBridgeの分類を作成する必要があります. .h ファイルで十分です. ランタイム メカニズムを通じて、内部の 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. もう一つ重要なポイントがあります. 公開パッケージと業務パッケージは別々にロードされるため, 公開パッケージがロードされた後に業務パッケージをロードする必要があり, 速度はできるだけ速くする必要があります. インターネット上の一部の人々は言う.アプリが開始されると、初期化ブリッジのパブリック パッケージが読み込まれますが、これは非常に怠惰な処理方法であり、多くのパフォーマンスを消費すると思います. ここで使用するのは、パブリック パッケージが持っている通知を監視することです.通知が受信されると、ビジネス パッケージのロードが開始されます。

@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側とnative側のサブパッケージロードの全過程で、あとはホットアップデートの実装と、プロジェクトで使うコンポーネントライブラリの実装です。 !

おすすめ

転載: blog.csdn.net/weixin_42433480/article/details/129913092