ReactNative(0.74.5)拆分bundle包

注意

由于Metro默认打包配置,在bundle头添加了如下globalVariables的全局配置,因此在打第一个包的时候注意执行拼接,下面已封装为nodeJS代码,自行执行
主包, 主包,切记!不需要每个包都包含

"bundle:ios-main": "react-native bundle --entry-file index.js --platform ios --dev false --bundle-output dist/ios/main.jsbundle --assets-dest dist/ios --config metro.index.config.js && node insertGlobals.js",
// insertGlobals.js
const fs = require('fs');
const path = require('path');

// 定义要插入的全局变量
const globalVariables =
  'var __BUNDLE_START_TIME__=this.nativePerformanceNow?nativePerformanceNow():Date.now(),__DEV__=false,process=this.process||{},__METRO_GLOBAL_PREFIX__=\'\';process.env=process.env||{};process.env.NODE_ENV=process.env.NODE_ENV||"production";';

// 定义生成的主包文件路径
const bundleFilePath = path.resolve(__dirname, 'dist/ios/main.jsbundle');

// 读取 bundle 文件内容
fs.readFile(bundleFilePath, 'utf8', (err, data) => {
  if (err) {
    console.error('Error reading the bundle file:', err);
    return;
  }

  // 插入全局变量到 bundle 文件的开头
  const modifiedData = globalVariables + data;

  // 写入修改后的内容到 bundle 文件
  fs.writeFile(bundleFilePath, modifiedData, 'utf8', err => {
    if (err) {
      console.error('Error writing the modified bundle file:', err);
    } else {
      console.log('Global variables inserted successfully.');
    }
  });
});

序言

在开始本章之前,如果你尝试过其它的分包方案,得到如下错误:

png: missing-asset-registry-path could not be found within the project or in these directories:
  node_modules
  ../../../node_modules

该问题大概率由于你使用当前的metro版本,而分包使用的metro.config.js的代码是老版本的,简单讲进行代码适配就行啦!

ReactNative分包

踩在巨人的肩膀,我们得知Metro工具在序列化阶段其实调用的是createModuleIdFactory和processModuleFilter。

createModuleIdFactory 是一个在 React Native 的 Metro Bundler 配置中用于生成模块 ID 的函数。这个函数的目的是为每个模块生成一个唯一的标识符,以便在打包过程中使用。

processModuleFilter 是在 React Native 的 Metro Bundler 配置中使用的函数之一。它用于过滤模块,在打包过程中决定哪些模块应该被包含,哪些应该被排除。

具体的拆包逻辑就孕育而生:


naotu.jpg

第一阶段

  1. 建立index1.js业务模块,demo是以index.js为入口第一个主包,index1.js入口的第二个子包(因为懒省事直接使用了默认的iOS工程,正常混合开发是原生页面加载common+业务1+业务2,demo中是index 模态 index1, 原理是一样的,这点不过多阐述)
// index1.js
import {AppRegistry} from 'react-native';
import App1 from './App1';

AppRegistry.registerComponent('App1', () => App1);

//App1.tsx
import {StyleSheet, Text, View} from 'react-native';
import React from 'react';

const App1 = () => {
  return (
    <View>
      <Text>我是App1</Text>
    </View>
  );
};

export default App1;
  1. 设置打包的配置文件,不使用默认的(metro.config.js是全量的打包),这里重新新建metro.index.config.js, 后续其实都用这一个就行,只需要注意打包的顺序
// metro.index.config.js
'use strict';

const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
const fs = require('fs');
const pathSep = require('path').sep;

var commonModules = null;

/**
 * 检查模块路径是否已经在清单文件中
 *
 * @param {string} path - 要检查的模块路径
 * @returns {boolean} - 如果模块在清单中,返回 true,否则返回 false
 */
function isInManifest(path) {
  const manifestFile = './dist/common_manifest.txt';

  // 如果清单文件尚未加载到内存中,则加载它
  if (commonModules === null && fs.existsSync(manifestFile)) {
    const lines = String(fs.readFileSync(manifestFile))
      .split('\n')
      .filter(line => line.length > 0);
    commonModules = new Set(lines);
  } else if (commonModules === null) {
    commonModules = new Set();
  }

  return commonModules.has(path);
}

/**
 * 将模块路径添加到清单文件中
 *
 * @param {string} path - 要添加的模块路径
 */
function manifest(path) {
  if (path.length) {
    const manifestFile = './dist/common_manifest.txt';

    if (!fs.existsSync(manifestFile)) {
      // 如果清单文件不存在,则创建它
      fs.writeFileSync(manifestFile, path);
    } else {
      // 如果清单文件已存在,则在文件末尾追加路径
      fs.appendFileSync(manifestFile, '\n' + path);
    }
  }
}

/**
 * 过滤要处理的模块
 *
 * @param {object} module - 要过滤的模块
 * @returns {boolean} - 如果模块需要处理,返回 true,否则返回 false
 */
function processModuleFilter(module) {
  if (module.path.indexOf('__prelude__') >= 0) {
    return false;
  }
  if (isInManifest(module.path)) {
    return false;
  }
  manifest(module.path);
  return true;
}

/**
 * 创建唯一模块 ID 的工厂函数
 *
 * @returns {function} - 用于生成模块 ID 的函数
 */
function createModuleIdFactory() {
  return path => {
    let name = '';
    if (path.startsWith(__dirname)) {
      name = path.substr(__dirname.length + 1);
    }
    let regExp =
      pathSep == '\\' ? new RegExp('\\\\', 'gm') : new RegExp(pathSep, 'gm');

    return name.replace(regExp, '_');
  };
}

// Metro 配置
const config = {
  serializer: {
    createModuleIdFactory,
    processModuleFilter,
  },
};

// 合并默认配置并导出
module.exports = mergeConfig(getDefaultConfig(__dirname), config);

  1. 配置打包命令,注意上面我们说的主包bundle要注入部分metro的默认配置,所以主包用到了insertGlobals.js
//package.json, 这里以iOS为例(安卓命令自行添加), ios-full是全量包,下面是主包和副包,当然如果有common.js主包应该是以common.js为入口,下面只是为了配合后续的demo实例
"scripts": {
    "android": "react-native run-android",
    "ios": "react-native run-ios",
    "lint": "eslint .",
    "start": "react-native start",
    "bundle:ios-full": "react-native bundle --entry-file index.js --platform ios --dev false --bundle-output dist/ios/sub.jsbundle --assets-dest dist/ios --config metro.config.js",
    "bundle:ios-main": "react-native bundle --entry-file index.js --platform ios --dev false --bundle-output dist/ios/main.jsbundle --assets-dest dist/ios --config metro.index.config.js && node insertGlobals.js",
    "bundle:ios-sub": "react-native bundle --entry-file index1.js --platform ios --dev false --bundle-output dist/ios/sub.jsbundle --assets-dest dist/ios --config metro.index.config.js",
    "test": "jest"
  },

打包命令不做过多阐述,需要注意的是入口文件--entry-file 的路径以及使用的--config 文件的路径,需要根据使用者的的目录结构,(demo使用的是index.js, index1.js, metro.index.config.js,处于根目录下), 和打包后生成的文件名称自定义,其实我们完全可以使用脚本.sh打包。

  1. 在原有iOS基础上更改吧,只写部分代码,具体业务代码,比如避免重复加载啦请自行添加判断逻辑,简单加个数组就行啦,避免多次进入同一模块重复加载;
// 首先- (void)executeSourceCode:(NSData *)sourceCode withSourceURL:(NSURL *)url sync:(BOOL)sync是RCTCxxBridge里面的方法,属于私有方法,我们使用category将其暴露出来;
//
//  RCTBridge+CustomerBridge.h
//  MetroBundlersDemo
//
//  Created by 产品1 on 2024/8/7.
//
#import <Foundation/Foundation.h>
#import <React/RCTBridge.h>

NS_ASSUME_NONNULL_BEGIN

@interface RCTBridge (CustomerBridge)
- (void)executeSourceCode:(NSData *)sourceCode withSourceURL:(NSURL *)url sync:(BOOL)sync;
@end

NS_ASSUME_NONNULL_END

//
//  RCTBridge+CustomerBridge.m
//  MetroBundlersDemo
//
//  Created by 产品1 on 2024/8/7.
//

#import "RCTBridge+CustomerBridge.h"

@implementation RCTBridge (CustomerBridge)

- (void)executeSourceCode:(nonnull NSData *)sourceCode withSourceURL:(nonnull NSURL *)url sync:(BOOL)sync {
  NSLog(@"执行我啦");
}

@end

// 其次直接更改appdelegate里面的,只写简单的拼接过程示例,业务逻辑自己处理哈,注意避免重复加载相同的bundle

#import <RCTAppDelegate.h>
#import <UIKit/UIKit.h>

@interface AppDelegate : RCTAppDelegate

@end

#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";
  // You can add your custom initial props in the dictionary below.
  // They will be passed down to the ViewController used by React Native.
  self.initialProps = @{};
  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 {
  NSURL *bundleURL = [[NSBundle mainBundle] URLForResource:bundleName withExtension:@"jsbundle"];
  NSData *bundleData = [NSData dataWithContentsOfURL:bundleURL];
  if (bundleData) {
    [self.bridge.batchedBridge executeSourceCode:bundleData withSourceURL:bundleURL 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
}

@end
以上过程已经完成了主包和业务包的拆分,依次我们还可以打包多个不同的业务包

第三阶段

优化注意事项:
  1. 可使用脚本编辑打包过程,避免过多的手动输入,比如把需要打包的入口文件以及配置文件生成map对象,根据选择打对应的common和business包,具体根据情况使用node.js或者.sh自行设置;
  2. iOS打包后资源文件怎么处理?通常来讲加载的资源文件是按照第一个bundle的assets加载的,在进行离线下载热更新的时候,我们需要在客户端进行assets的merge,合并到第一个通常来讲是common的assets目录下面;
  3. 开发调试的时候怎么办呢?开发调试的时候我们建议将不同的business.js入口文件导入index.js中,进行单文件调试;
  4. 混合开发的路由和网络请求如何处理?混合开发通常使用的网络请求和路由都是原生端提供,
    路由:原生端使用viewcontroller或者activity进行加载对应的RN_View, 在加载的时候我们可以设置不同的moduleName以及在附加参数中添加对应的pageName传递给RN,RN根据注册的ModuleName和页面的pageName显示对应的界面,rn界面的路由就可以类似这样:app://rnbase?page=home, pop的时候也根据此进行返回;
    网络请求:网络请求使用原生端网络请求,根据bridge进行方法的相互调用,可解决登录状态,header头设置等问题,也避免重复写网络请求。
  5. 在调试出现错误的时候,捕获异常可以使用@sentry/react-native在每个业务的componentDidCatch中,手动captureException到sentry解决问题,可以动态注入;

后记,demo工程只用OC写了iOS的加载,安卓自行补充

Metro已经很成熟啦,最近也是因为收到很多人的邮件反馈不能使用的问题,对此进行了更新,大家在以后使用过程中如果遇到无法加载,有时候可以尝试打全量包和你打的主包bundle进行大致的对比找出问题所在,这是一点意见哈

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容