ReactNative(0.74.5)拆分bundle包

注意

由于Metro默認打包配置盒卸,在bundle頭添加了如下globalVariables的全局配置较雕,因此在打第一個包的時候注意執(zhí)行拼接搪缨,下面已封裝為nodeJS代碼食拜,自行執(zhí)行
主包, 主包副编,切記负甸!不需要每個包都包含

"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 的函數(shù)柴淘。這個函數(shù)的目的是為每個模塊生成一個唯一的標識符迫淹,以便在打包過程中使用。

processModuleFilter 是在 React Native 的 Metro Bundler 配置中使用的函數(shù)之一为严。它用于過濾模塊敛熬,在打包過程中決定哪些模塊應該被包含,哪些應該被排除第股。

具體的拆包邏輯就孕育而生:


naotu.jpg

第一階段

  1. 建立index1.js業(yè)務模塊应民,demo是以index.js為入口第一個主包,index1.js入口的第二個子包(因為懶省事直接使用了默認的iOS工程夕吻,正橙鸶荆混合開發(fā)是原生頁面加載common+業(yè)務1+業(yè)務2,demo中是index 模態(tài) 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, 后續(xù)其實都用這一個就行控漠,只需要注意打包的順序
// 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;

/**
 * 檢查模塊路徑是否已經(jīng)在清單文件中
 *
 * @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)) {
      // 如果清單文件不存在,則創(chuàng)建它
      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;
}

/**
 * 創(chuàng)建唯一模塊 ID 的工廠函數(shù)
 *
 * @returns {function} - 用于生成模塊 ID 的函數(shù)
 */
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為入口朵耕,下面只是為了配合后續(xù)的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 文件的路徑淋叶,需要根據(jù)使用者的的目錄結構阎曹,(demo使用的是index.js, index1.js, metro.index.config.js,處于根目錄下), 和打包后生成的文件名稱自定義煞檩,其實我們完全可以使用腳本.sh打包处嫌。

  1. 在原有iOS基礎上更改吧,只寫部分代碼斟湃,具體業(yè)務代碼熏迹,比如避免重復加載啦請自行添加判斷邏輯,簡單加個數(shù)組就行啦桐早,避免多次進入同一模塊重復加載癣缅;
// 首先- (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(@"執(zhí)行我啦");
}

@end

// 其次直接更改appdelegate里面的,只寫簡單的拼接過程示例陶衅,業(yè)務邏輯自己處理哈屡立,注意避免重復加載相同的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(), ^{
  // 模擬進入業(yè)務模塊,注意這里添加邏輯判斷搀军,避免多次加載膨俐,例如數(shù)組記錄
    [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
以上過程已經(jīng)完成了主包和業(yè)務包的拆分,依次我們還可以打包多個不同的業(yè)務包

第三階段

優(yōu)化注意事項:
  1. 可使用腳本編輯打包過程罩句,避免過多的手動輸入焚刺,比如把需要打包的入口文件以及配置文件生成map對象,根據(jù)選擇打對應的common和business包门烂,具體根據(jù)情況使用node.js或者.sh自行設置乳愉;
  2. iOS打包后資源文件怎么處理?通常來講加載的資源文件是按照第一個bundle的assets加載的屯远,在進行離線下載熱更新的時候蔓姚,我們需要在客戶端進行assets的merge,合并到第一個通常來講是common的assets目錄下面慨丐;
  3. 開發(fā)調試的時候怎么辦呢坡脐?開發(fā)調試的時候我們建議將不同的business.js入口文件導入index.js中,進行單文件調試房揭;
  4. 混合開發(fā)的路由和網(wǎng)絡請求如何處理备闲?混合開發(fā)通常使用的網(wǎng)絡請求和路由都是原生端提供晌端,
    路由:原生端使用viewcontroller或者activity進行加載對應的RN_View, 在加載的時候我們可以設置不同的moduleName以及在附加參數(shù)中添加對應的pageName傳遞給RN,RN根據(jù)注冊的ModuleName和頁面的pageName顯示對應的界面浅役,rn界面的路由就可以類似這樣:app://rnbase?page=home, pop的時候也根據(jù)此進行返回斩松;
    網(wǎng)絡請求:網(wǎng)絡請求使用原生端網(wǎng)絡請求,根據(jù)bridge進行方法的相互調用觉既,可解決登錄狀態(tài)惧盹,header頭設置等問題,也避免重復寫網(wǎng)絡請求瞪讼。
  5. 在調試出現(xiàn)錯誤的時候钧椰,捕獲異常可以使用@sentry/react-native在每個業(yè)務的componentDidCatch中符欠,手動captureException到sentry解決問題嫡霞,可以動態(tài)注入;

后記希柿,demo工程只用OC寫了iOS的加載诊沪,安卓自行補充

Metro已經(jīng)很成熟啦,最近也是因為收到很多人的郵件反饋不能使用的問題曾撤,對此進行了更新端姚,大家在以后使用過程中如果遇到無法加載,有時候可以嘗試打全量包和你打的主包bundle進行大致的對比找出問題所在挤悉,這是一點意見哈

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末渐裸,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子装悲,更是在濱河造成了極大的恐慌昏鹃,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件诀诊,死亡現(xiàn)場離奇詭異洞渤,居然都是意外死亡,警方通過查閱死者的電腦和手機属瓣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門您宪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人奠涌,你說我怎么就攤上這事×仔樱” “怎么了溜畅?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長极祸。 經(jīng)常有香客問我慈格,道長怠晴,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任浴捆,我火速辦了婚禮蒜田,結果婚禮上,老公的妹妹穿的比我還像新娘选泻。我一直安慰自己冲粤,他們只是感情好,可當我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布页眯。 她就那樣靜靜地躺著梯捕,像睡著了一般。 火紅的嫁衣襯著肌膚如雪窝撵。 梳的紋絲不亂的頭發(fā)上傀顾,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天,我揣著相機與錄音碌奉,去河邊找鬼短曾。 笑死,一個胖子當著我的面吹牛赐劣,可吹牛的內容都是我干的嫉拐。 我是一名探鬼主播,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼隆豹,長吁一口氣:“原來是場噩夢啊……” “哼椭岩!你這毒婦竟也來了?” 一聲冷哼從身側響起璃赡,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤判哥,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后碉考,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體塌计,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年侯谁,在試婚紗的時候發(fā)現(xiàn)自己被綠了锌仅。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡墙贱,死狀恐怖热芹,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情惨撇,我是刑警寧澤伊脓,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站魁衙,受9級特大地震影響报腔,放射性物質發(fā)生泄漏株搔。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一纯蛾、第九天 我趴在偏房一處隱蔽的房頂上張望纤房。 院中可真熱鬧,春花似錦翻诉、人聲如沸炮姨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽剑令。三九已至,卻和暖如春拄查,著一層夾襖步出監(jiān)牢的瞬間吁津,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工堕扶, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留碍脏,地道東北人。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓稍算,卻偏偏與公主長得像典尾,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子糊探,可洞房花燭夜當晚...
    茶點故事閱讀 43,627評論 2 350

推薦閱讀更多精彩內容