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 文件內(nèi)容
fs.readFile(bundleFilePath, 'utf8', (err, data) => {
  if (err) {
    console.error('Error reading the bundle file:', err);
    return;
  }

  // 插入全局變量到 bundle 文件的開頭
  const modifiedData = globalVariables + data;

  // 寫入修改后的內(nèi)容到 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工具在序列化階段其實調(diào)用的是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';

  // 如果清單文件尚未加載到內(nèi)存中,則加載它
  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ù)使用者的的目錄結(jié)構(gòu),(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 產(chǎn)品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 產(chǎn)品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ù)選擇打?qū)腸ommon和business包,具體根據(jù)情況使用node.js或者.sh自行設置;
  2. iOS打包后資源文件怎么處理?通常來講加載的資源文件是按照第一個bundle的assets加載的,在進行離線下載熱更新的時候,我們需要在客戶端進行assets的merge,合并到第一個通常來講是common的assets目錄下面;
  3. 開發(fā)調(diào)試的時候怎么辦呢?開發(fā)調(diào)試的時候我們建議將不同的business.js入口文件導入index.js中,進行單文件調(diào)試;
  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進行方法的相互調(diào)用,可解決登錄狀態(tài),header頭設置等問題,也避免重復寫網(wǎng)絡請求。
  5. 在調(diào)試出現(xiàn)錯誤的時候,捕獲異??梢允褂聾sentry/react-native在每個業(yè)務的componentDidCatch中,手動captureException到sentry解決問題,可以動態(tài)注入;

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容