序言
最近对前面的文章ReactNative的Metro的拆包方案进行了补充,有些用户又反馈资源的使用问题,例如Image的资源路径问题(特别是iOS端),特记录该文章进行补充,其实很大程度参考了react-native-multibundler;
承接上一节的文章,首先我们需要导入react-native-smartassets": "^1.0.4"

注意:这是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版本等,这个根据你的实际情况自定义。
网友评论