注意
由于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ù)之一为严。它用于過濾模塊敛熬,在打包過程中決定哪些模塊應該被包含,哪些應該被排除第股。
具體的拆包邏輯就孕育而生:
第一階段
- 建立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;
- 設置打包的配置文件,不使用默認的(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);
- 配置打包命令母剥,注意上面我們說的主包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打包处嫌。
- 在原有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)化注意事項:
- 可使用腳本編輯打包過程罩句,避免過多的手動輸入焚刺,比如把需要打包的入口文件以及配置文件生成map對象,根據(jù)選擇打對應的common和business包门烂,具體根據(jù)情況使用node.js或者.sh自行設置乳愉;
- iOS打包后資源文件怎么處理?通常來講加載的資源文件是按照第一個bundle的assets加載的屯远,在進行離線下載熱更新的時候蔓姚,我們需要在客戶端進行assets的merge,合并到第一個通常來講是common的assets目錄下面慨丐;
- 開發(fā)調試的時候怎么辦呢坡脐?開發(fā)調試的時候我們建議將不同的business.js入口文件導入index.js中,進行單文件調試房揭;
- 混合開發(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)絡請求瞪讼。- 在調試出現(xiàn)錯誤的時候钧椰,捕獲異常可以使用@sentry/react-native在每個業(yè)務的componentDidCatch中符欠,手動captureException到sentry解決問題嫡霞,可以動態(tài)注入;
后記希柿,demo工程只用OC寫了iOS的加載诊沪,安卓自行補充
Metro已經(jīng)很成熟啦,最近也是因為收到很多人的郵件反饋不能使用的問題曾撤,對此進行了更新端姚,大家在以后使用過程中如果遇到無法加載,有時候可以嘗試打全量包和你打的主包bundle進行大致的對比找出問題所在挤悉,這是一點意見哈