序言

公司要搞跨平台方案,由于之前移动端页面多数由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,并为该模块返回一个唯一的Id。processModuleFilter(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

@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

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

相关阅读

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: