ReactNative拆包后的资源文件处理

2024-08-15  本文已影响0人  那年那月那花儿

序言

最近对前面的文章ReactNative的Metro的拆包方案进行了补充,有些用户又反馈资源的使用问题,例如Image的资源路径问题(特别是iOS端),特记录该文章进行补充,其实很大程度参考了react-native-multibundler

承接上一节的文章,首先我们需要导入react-native-smartassets": "^1.0.4"

图片.png
注意:这是iOS主工程添加资源包assets的方式,注意勾选方式,避免找不到资源
这里我对react-native-smartassets源码进行部分注释,方便使用的时候可以了解
//  react-native-smartassets/index.js
import { NativeModules, Platform } from 'react-native';
import AssetSourceResolver from "react-native/Libraries/Image/AssetSourceResolver";
const { Smartassets } = NativeModules; // 获取原生模块 `Smartassets`
let iOSRelateMainBundlePath = ''; // 用于存储 iOS 主包路径的相对路径
let bundlePath = null; // 存储 JS Bundle 的路径
let _sourceCodeScriptURL = null; // 存储当前脚本的 URL

// 获取当前运行脚本的 URL 地址
function getSourceCodeScriptURL() {
  if (_sourceCodeScriptURL) {
    return _sourceCodeScriptURL;
  }
  let sourceCode = global.nativeExtensions && global.nativeExtensions.SourceCode;
  if (!sourceCode) {
    sourceCode = NativeModules && NativeModules.SourceCode;
  }
  _sourceCodeScriptURL = sourceCode.scriptURL;
  return _sourceCodeScriptURL;
}

const defaultMainBundlePath = Smartassets.DefaultMainBundlePath; // 默认的主包路径,来自于原生模块
var _ = require('lodash'); // 引入 lodash 库
var SmartAssets = {
  // 初始化 SmartAssets,只会被调用一次
  initSmartAssets() {
    var initialize = _.once(this.initSmartAssetsInner);
    initialize();
  },

  // 内部初始化方法,配置资源加载逻辑
  initSmartAssetsInner() {
    let drawablePathInfos = []; // 存储 drawable 文件路径信息

    // 确保 `Smartassets` 和 `travelDrawable` 方法存在,遍历 drawable 目录并收集路径信息
    Smartassets.travelDrawable(getSourceCodeScriptURL(), (retArray) => {
      drawablePathInfos = drawablePathInfos.concat(retArray);
    });

    // 重写 `defaultAsset` 方法,用于自定义资源加载逻辑
    AssetSourceResolver.prototype.defaultAsset = _.wrap(AssetSourceResolver.prototype.defaultAsset, function (func, ...args) {
      // 如果资源是从服务器加载的,直接返回服务器 URL
      if (this.isLoadedFromServer()) {
        return this.assetServerURL();
      }

      if (Platform.OS === 'android') { // Android 平台的处理逻辑
        // 如果资源是从文件系统加载的或者 `bundlePath` 不为 `null`
        if (this.isLoadedFromFileSystem() || bundlePath != null) {
          if (bundlePath != null) {
            this.jsbundleUrl = bundlePath; // 设置 JS Bundle 的 URL
          }
          let resolvedAssetSource = this.drawableFolderInBundle(); // 获取资源的完整路径
          let resPath = resolvedAssetSource.uri;

          // 如果资源已经存在于 drawable 文件夹中,直接返回
          if (drawablePathInfos.includes(resPath)) {
            return resolvedAssetSource;
          }

          // 检查资源文件是否存在,存在则返回路径,否则返回资源标识符
          let isFileExist = Smartassets.isFileExist(resPath);
          if (isFileExist === true) {
            return resolvedAssetSource;
          } else {
            return this.resourceIdentifierWithoutScale();
          }
        } else {
          return this.resourceIdentifierWithoutScale(); // 从资源标识符加载
        }
      } else { // iOS 平台的处理逻辑
        if (bundlePath != null) {
          /**
           * 一般用于热更新下的文件夹, 即下载的 bundle 文件, 设置 bundle 的实际位置, 例如 documents/bundle,
           * 在 index 中监听 bundleLoad 的路径, 在原生 Native 中常用于到达某个模块后加载 ReactView,
           * 发送 emit 事件, 和 RN 交互, 重置资源路径, 以便读取正确的资源路径
           */
          this.jsbundleUrl = bundlePath;
        }

        let iOSAsset = this.scaledAssetURLNearBundle(); // 获取与 JS Bundle 路径相关的资源路径
        let isFileExist = Smartassets.isFileExist(iOSAsset.uri);
        if (isFileExist) {
          return iOSAsset; // 如果文件存在,直接返回资源路径
        } else {
          // 如果文件不存在,尝试替换为原始的 JS Bundle 路径
          let oriJsBundleUrl = 'file://' + defaultMainBundlePath + '/' + iOSRelateMainBundlePath;
          iOSAsset.uri = iOSAsset.uri.replace(this.jsbundleUrl, oriJsBundleUrl);
          return iOSAsset;
        }
      }
    });
  },

  // 设置 JS Bundle 的路径
  setBundlePath(bundlePathNew) {
    bundlePath = bundlePathNew;
  },

  // 设置 iOS 主包的相对路径
  setiOSRelateMainBundlePath(relatePath) {
    iOSRelateMainBundlePath = relatePath;
  }
};

export { SmartAssets };

然后我们在入口index.js配置监听:

// index.js
/**
 * @format
 */

import {
  AppRegistry,
  Platform,
  NativeEventEmitter,
  NativeModules,
} from 'react-native';
import App from './App';
import {name as appName} from './app.json';
import {SmartAssets} from 'react-native-smartassets';
SmartAssets.initSmartAssets();
if (Platform.OS != 'android') {
  const {BundleloadEventEmiter} = NativeModules;

  if (BundleloadEventEmiter) {
    const bundleLoadEmitter = new NativeEventEmitter(BundleloadEventEmiter);

    const subscription = bundleLoadEmitter.addListener(
      'BundleLoad',
      bundleInfo => {
        console.log('BundleLoad==' + bundleInfo.path);
        SmartAssets.setBundlePath(bundleInfo.path);
      },
    );
  } else {
    console.error('BundleloadEventEmiter is null or undefined');
  }
}

AppRegistry.registerComponent(appName, () => App);

在这里React工程的配置基本结束,进入iOS工程中(Android端自己参考上述git),基于上一节文章中,需要引入原生和SmartAssets的交互代码,创建RNSmartassets.(h, m)如下:

#if __has_include("RCTBridgeModule.h")
#import "RCTBridgeModule.h"
#else
#import <React/RCTBridgeModule.h>
#endif

@interface RNSmartassets : NSObject <RCTBridgeModule>

@end

#import "RNSmartassets.h"

@implementation RNSmartassets

- (dispatch_queue_t)methodQueue
{
    return dispatch_get_main_queue();
}
RCT_EXPORT_MODULE(Smartassets)

RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(isFileExist:(NSString *)filePath){
    if([filePath hasPrefix:@"file://"]){
      filePath = [filePath substringFromIndex:6];
    }
  NSLog(@"filePath:%@, bundlePath:%@", filePath, [[NSBundle mainBundle] bundlePath]);
    NSFileManager *fileManager = [NSFileManager defaultManager];
    BOOL fileExists = [fileManager fileExistsAtPath:filePath];
    return @(fileExists);
}

RCT_EXPORT_METHOD(travelDrawable:(NSString *)bundlePath callBack:(RCTResponseSenderBlock)callback){
    bundlePath = [bundlePath substringFromIndex:6];
    NSString *assetPath = [bundlePath stringByDeletingLastPathComponent];
  NSLog(@"bundlePath:%@", assetPath);
    NSString *imgPath;
    NSFileManager *fm;
    NSDirectoryEnumerator *dirEnum;
    fm = [NSFileManager defaultManager];
    dirEnum = [fm enumeratorAtPath:assetPath];
  
    NSMutableArray *imgArrays = [[NSMutableArray alloc]init];
    while ((imgPath = [dirEnum nextObject]) != nil){
      NSLog(@"imgPath:%@", imgPath);
        [imgArrays addObject:[assetPath stringByAppendingPathComponent:imgPath]];
    }
  NSLog(@"imgArr:%@", imgArrays);
    callback(imgArrays);
}

- (NSDictionary *)constantsToExport
{
    return @{
             @"DefaultMainBundlePath": [[NSBundle mainBundle] bundlePath]
             };
}

@end

然后注册交互的Emiter事件表BundleloadEventEmiter.(h, m)

#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>

@interface BundleloadEventEmiter : RCTEventEmitter<RCTBridgeModule>

@end

#import <Foundation/Foundation.h>
#import "BundleloadEventEmiter.h"

@implementation BundleloadEventEmiter

{
  bool hasListeners;
}

- (instancetype)init
{
  self = [super init];
  if (self) {
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(bundleLoaded:)
                                                 name:@"BundleLoad"
                                               object:nil];
  }
  return self;
}

RCT_EXPORT_MODULE();

// Will be called when this module's first listener is added.
-(void)startObserving
{
  hasListeners = YES;
  // Set up any upstream listeners or background tasks as necessary
}

// Will be called when this module's last listener is removed, or on dealloc.
-(void)stopObserving
{
  hasListeners = NO;
  // Remove upstream listeners, stop unnecessary background tasks
}

- (NSArray<NSString *> *)supportedEvents
{
  return @[@"BundleLoad"];
}

- (void)bundleLoaded:(NSNotification *)notification
{
  NSString *bundlePath = notification.userInfo[@"path"];
  if (hasListeners) { // Only send events if anyone is listening
    [self sendEventWithName:@"BundleLoad" body:@{@"path": bundlePath}];
  }
}

@end

下面我们模拟进入【热更新】后下载的模块,在这里我们假设下载的文件放在document中(如果使用xcode模拟器,手动拖进document中,模拟下载的),文件夹名字是sub(sub.jsbundle, assets), 上一节我们是直接放在根目录下面,导入的方式没有考虑资源文件(因为当初考虑是基础包,不包含图片),这一节对AppDelegate代码进行更改

#import "AppDelegate.h"
#import "RCTBridge+CustomerBridge.h"
#import <React/RCTBundleURLProvider.h>
#import <React/RCTAssert.h>
#import "MainViewController.h"
#import <React/RCTBridge+Private.h>
#import <React/RCTBridge.h>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  self.moduleName = @"MetroBundlersDemo";
  
   dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
     [self loadJSBundle:@"sub" sync:NO];
     MainViewController *vc = [MainViewController new];
     vc.bridge = self.bridge;
     [self.window.rootViewController presentViewController:vc animated:true completion:nil];
   });
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

- (void)loadJSBundle:(NSString *)bundleName sync:(BOOL)sync {
  NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
  NSString *documentsDirectory = [paths objectAtIndex:0];

  // 拼接sub.jsbundle文件路径
  NSString *filePath = [documentsDirectory stringByAppendingPathComponent:@"sub/sub.jsbundle"];
  NSLog(@"filePath:%@", filePath);
  // 检查文件是否存在
  if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
      // 读取文件内容
      NSData *fileData = [NSData dataWithContentsOfFile:filePath];
      if (fileData) {
        NSURL *fileURL = [NSURL fileURLWithPath:filePath];
        [self.bridge.batchedBridge executeSourceCode:fileData withSourceURL:fileURL sync:sync];
      } else {
          NSLog(@"Failed to load file data");
      }
  } else {
      NSLog(@"sub.jsbundle file not found");
  }
//  NSURL *bundleURL = [[NSBundle mainBundle] URLForResource:bundleName withExtension:@"jsbundle"];
//  NSData *bundleData = [NSData dataWithContentsOfURL:bundleURL];
//  if (bundleData) {
//    [self.bridge.batchedBridge executeSourceCode:bundleData withSourceURL:[NSURL new] sync:sync];
//  } else {
//    NSLog(@"解析错误");
//  }
}

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
  return [self bundleURL];
}

- (NSURL *)bundleURL
{
//#if DEBUG
// return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
//#else
  return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
//#endif
}

然后在加载对应模块之前,发送通知给ReactNative,改变bundleJs, 示例代码在MainViewController中

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
  NSString *filePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"sub"];
  [[NSNotificationCenter defaultCenter] postNotificationName:@"BundleLoad" object:nil userInfo:@{@"path":[@"file://" stringByAppendingString:[filePath stringByAppendingString:@"/"]]}];
  RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:_bridge moduleName:@"App1" initialProperties:@{}];
  rootView.frame = self.view.bounds;
  [self.view addSubview:rootView];
}

代码已经给的很详细啦,就不再给本篇文章的demo啦

后记,部分代码重构

最后给出reactnative端和iOS端的完整的本地+热更新的处理demo,没有版本控制(这个涉及到业务,就不班门弄斧啦),sub.zip可以放到你自己的服务器,更改服务器地址就可以运行(npm pod更新自行处理)啦。实际开发注意下载的文件地址、存储地址和解压地址,以及bundle的名字和模块名字,bundle版本等,这个根据你的实际情况自定义。

上一篇 下一篇

猜你喜欢

热点阅读