Carga de subpaquetes y subpaquetes nativos de React en el lado de iOS

preámbulo

La empresa quiere desarrollar una solución multiplataforma. Dado que la mayoría de las páginas móviles fueron escritas antes por H5, la empresa tiene una gran cantidad de personal de pila de tecnología javaScript, y la solución de actualización en caliente H5 se usó antes del proyecto, y quiere para continuar usando la actualización en caliente para resolver problemas en línea, por lo que al final decidió usar React Native.


Subpaquete final RN (desempaque)

Como todos sabemos, el lado nativo de iOS carga el código React Native principalmente cargando el jsBundle convertido. Algunos proyectos se desarrollan con RN puro desde el principio y solo necesitan cargar un index.ios.jsbundle. Sin embargo, nuestro proyecto es agregar módulos React Native sobre la base de Hybrid, y también se requiere una actualización en caliente Para mantener el tamaño de cada paquete de actualización en caliente lo más pequeño posible, es necesario utilizar la carga de subpaquetes.

Lo que uso aquí es el método de subcontratación basado en Metro, que también es el principal método utilizado en el mercado. createModuleIdFactory(path)Este método de desempaquetado se ocupa principalmente de los métodos y métodos llamados por las herramientas Metro durante la fase de "serialización"  processModuleFilter(module). createModuleIdFactory(path)es la ruta absoluta del módulo pasado pathy devuelve una única para ese módulo Id. processModuleFilter(module)Luego, el módulo se puede filtrar para que el contenido del módulo comercial no se escriba en el módulo común. A continuación, se explicarán los pasos y códigos específicos.

1. Primero cree un archivo common.js, que importa todos los módulos públicos

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

2. Metro usa este common.js como archivo de entrada para crear un paquete de paquete común y, al mismo tiempo, registra el moduleId de todos los módulos públicos , pero hay un problema: cada vez que inicia el paquete de Metro, el moduleId es automáticamente incrementado desde 0. Esto conducirá a la duplicación de diferentes ID de JSBundle Para evitar la duplicación de ID, la práctica principal actual en la industria es usar la ruta del módulo como moduleId (porque la ruta del módulo es básicamente fijo y no entra en conflicto), resolviendo así el problema de los conflictos de id. Metro expone la función createModuleIdFactory, podemos anular la lógica de incremento automático original en esta función y escribir el moduleId del módulo público en el archivo txt

I) Configurar comandos en 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) El archivo 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) Archivo moduleIds_ios.txt generado

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. Después de empaquetar los módulos públicos, comience a empaquetar los módulos comerciales. La clave de este paso es filtrar el moduleId del módulo público (el Id del módulo público se registró en moduleIds_ios.txt en el paso anterior). Metro proporciona el método processModuleFilter, que se puede usar para filtrar módulos. El procesamiento de esta parte se escribe principalmente en el archivo metro.business.config.ios.js, en qué archivo escribir depende principalmente del archivo especificado en el comando top 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'],
  },
};

En resumen, el trabajo de subcontratación en el lado de React Native está prácticamente completado.


Carga de subpaquetes en el lado de iOS

1. El lado de iOS primero necesita cargar el paquete público

-(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. Después de cargar el paquete público, debe cargar el paquete comercial. Para cargar el paquete comercial, debe usar el método executeSourceCode, pero este es un método privado de RCTBridge, y debe crear una clasificación de RCTBridge. Solo el archivo .h es suficiente.A través del mecanismo Runtime, la implementación interna del método 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;
}

El código de la clase ALAsyncLoadManager completa que carga paquetes públicos y paquetes comerciales:

#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. Hay otro punto importante. Dado que el paquete público y el paquete comercial se cargan por separado, el paquete comercial debe cargarse después de cargar el paquete público, y la velocidad debe ser lo más rápida posible. Algunas personas en Internet dicen que cuando se inicia la aplicación, se carga con el paquete público del puente de inicialización, creo que esta es una forma de procesamiento muy perezosa, que consumirá mucho rendimiento. Lo que uso aquí es monitorear la notificación que tiene el paquete público Una vez recibida la notificación, el paquete comercial comenzará a cargarse.

@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;
}

En resumen, el trabajo de carga de subpaquetes en el lado de iOS se completó aproximadamente.


Lo anterior es todo el proceso de carga de subpaquetes en el lado React Native y el lado nativo, y luego está la implementación de la actualización en caliente y la implementación de la biblioteca de componentes utilizada en el proyecto.Si lo encuentra útil, dele una estrella !

Supongo que te gusta

Origin blog.csdn.net/weixin_42433480/article/details/129913092
Recomendado
Clasificación