原生嵌RN面板Metro拆包

原生嵌RN面板Metro拆包

概覽

背景:

在 React Native 中,我們打包生成的包只有一個(gè)jsbundle,里面包含了我們的業(yè)務(wù)代碼、RN 源碼及依賴的第三方庫(kù)。通常為了更好的性能,我們需要將這個(gè)jsbundle文件進(jìn)行拆分,得到一個(gè)基礎(chǔ)包和多個(gè)業(yè)務(wù)包。

問(wèn)題:盡管拆包可以帶來(lái)諸多好處,如減少頁(yè)面首次加載時(shí)間,降低內(nèi)存資源消耗,減少更新內(nèi)容包的大小等,但如何進(jìn)行有效的拆包呢?

策略:

我們采用基于 Metro 進(jìn)行拆包的方法,Metro 是 React Native 官方提供的打包工具,我們基于 Metro 二次開(kāi)發(fā),實(shí)現(xiàn)了 jsbundle 拆分為一個(gè)基礎(chǔ)包和多個(gè)業(yè)務(wù)包。拆包步驟如下:

● Metro提供了兩個(gè)配置項(xiàng)createModuleIdFactory和processModuleFilter,前者用于生成require語(yǔ)句的模塊ID,后者用于過(guò)濾掉一些特定的模塊。

● 公司基于這兩個(gè)配置項(xiàng)進(jìn)行了拆包的實(shí)現(xiàn),首先配置createModuleIdFactory讓它每次打包生成的module都使用固定的id,然后配置processModuleFilter過(guò)濾基礎(chǔ)包,打出對(duì)應(yīng)業(yè)務(wù)包。

● 為了避免基礎(chǔ)包內(nèi)的第三方庫(kù)重復(fù)打入,公司在生成基礎(chǔ)包時(shí),把所有依賴的模塊name放到一個(gè)數(shù)組并寫(xiě)入到一個(gè)本地文件中,這個(gè)文件保存了基礎(chǔ)包中的依賴信息。在打業(yè)務(wù)包時(shí),讀取這個(gè)文件的內(nèi)容,就可以識(shí)別基礎(chǔ)包已存在的依賴庫(kù),不再重復(fù)打入。

● 在打包過(guò)程中,公司將基礎(chǔ)包中包含的RN源碼、第三方依賴庫(kù)、內(nèi)部公共組件等,通過(guò)import方式引入,然后使用react-native的bundle命令執(zhí)行打包。

● 在加載過(guò)程中,公司讓APP在啟動(dòng)時(shí)先加載基礎(chǔ)包,然后再按需加載業(yè)務(wù)包。同時(shí),公司在iOS和Android上分別實(shí)現(xiàn)了基礎(chǔ)包和業(yè)務(wù)包的加載方式。

效果:

通過(guò)這種方式,我們可以在 APP 啟動(dòng)時(shí)提前加載基礎(chǔ)包,在需要進(jìn)入 RN 頁(yè)面時(shí),再動(dòng)態(tài)加載該頁(yè)面所在的業(yè)務(wù)模塊文件,實(shí)現(xiàn)按需加載。在熱更新時(shí)只更新有變化的業(yè)務(wù)包,再配合 bsdiff 差分算法,大大減少更新內(nèi)容包的大小。拆包后能更好地支持動(dòng)態(tài)下發(fā)業(yè)務(wù)包,動(dòng)態(tài)加載,從而讓我們更靈活地部署、上線。

拆包方案簡(jiǎn)介

在 React Native 中,我們打包生成的包只有一個(gè)jsbundle,里面包含了我們的業(yè)務(wù)代碼、RN 源碼及依賴的第三方庫(kù),通常為了更好的性能,我們會(huì)拆分這個(gè)jsbundle文件,得到一個(gè)基礎(chǔ)包和多個(gè)業(yè)務(wù)包。

基礎(chǔ)包:將重復(fù)的React Native代碼與第三方依賴庫(kù)打包成一個(gè)文件。

業(yè)務(wù)包:按照應(yīng)用內(nèi)的不同業(yè)務(wù)單元,拆分出一個(gè)或多個(gè)包。

拆包后,讓基礎(chǔ)包在 APP 啟動(dòng)時(shí)提前加載到內(nèi)存中,在需要進(jìn)入 RN 頁(yè)面時(shí),再動(dòng)態(tài)加載該頁(yè)面所在的業(yè)務(wù)模塊文件,按需加載。

拆包給我們帶來(lái)了很多好處,如下:

● 提前加載 js 框架,這樣在進(jìn)入RN頁(yè)面時(shí),只需要加載業(yè)務(wù)js代碼,從而減少RN頁(yè)面首次加載時(shí)間;

● 打開(kāi)哪個(gè)頁(yè)面加載哪個(gè)業(yè)務(wù)包,避免一次性加載全部js代碼,降低內(nèi)存資源消耗;

● 在熱更新時(shí)只更新有變化的業(yè)務(wù)包,再配合 bsdiff 差分算法,大大減少更新內(nèi)容包的大?。?/p>

● 拆包后能更好地支持動(dòng)態(tài)下發(fā)業(yè)務(wù)包,動(dòng)態(tài)加載,從而讓我們更靈活地部署、上線。

現(xiàn)有的幾種拆包方案:

1,diff patch

首先生成基礎(chǔ)包,只引用RN源碼和第三方依賴庫(kù),然后現(xiàn)生成完成的jsbundle,通過(guò)diff比對(duì)基礎(chǔ)包和完整的jsbundle,得出業(yè)務(wù)包。

優(yōu)點(diǎn):簡(jiǎn)單

缺點(diǎn):只能拆分包,對(duì)性能沒(méi)有提升,反而增加了合包帶來(lái)的時(shí)間消耗

2,CRN

攜程最近開(kāi)源的拆包方案,包含了拆包、框架代碼預(yù)加載、兩端一套產(chǎn)物、懶require等。

優(yōu)點(diǎn):性能好,兩端一套產(chǎn)物

缺點(diǎn):成本高,對(duì)RN源碼、打包工具改動(dòng)較大,難升級(jí)、難維護(hù)

3,Metro

官方出的打包工具,從 0.57 開(kāi)始,已經(jīng)支持拆包了。

優(yōu)點(diǎn):穩(wěn)定可靠,無(wú)需改動(dòng)RN源碼

缺點(diǎn):性能沒(méi)有CRN好

我們的業(yè)務(wù)規(guī)模還不大,哪個(gè)方案下頁(yè)面加載速度和內(nèi)存問(wèn)題都不會(huì)很?chē)?yán)重,出于成本和穩(wěn)定性考慮,最終選擇了 Metro 方案。

下面介紹如何基于 Metro 進(jìn)行拆包的原理和實(shí)現(xiàn)過(guò)程。

拆包

Metro 是 React Native 官方提供的打包工具,它將我們的業(yè)務(wù)代碼及依賴的第三方庫(kù)打包生成一個(gè)jsbundle文件。我們基于 Metro 二次開(kāi)發(fā),實(shí)現(xiàn)了 jsbundle 拆分為一個(gè)基礎(chǔ)包和多個(gè)業(yè)務(wù)包。

其中有兩個(gè)配置項(xiàng)(更改 metro.config.js 文件):

createModuleIdFactory:用于生成 require 語(yǔ)句的模塊ID,配置 createModuleIdFactory 讓它每次打包生成的 module 都使用固定的id。它的返回值是一個(gè)函數(shù),參數(shù) path 是各個(gè) module 的絕對(duì)路徑,返回的是打包后的 module 的 id。

image.png

processModuleFilter:按照給定的規(guī)則,過(guò)濾掉一些特定的 module,配置processModuleFilter 過(guò)濾基礎(chǔ)包,打出對(duì)應(yīng)業(yè)務(wù)包。它返回一個(gè) boolean 類(lèi)型,輸入?yún)?shù)為 module 信息,如果返回 false,就過(guò)濾掉,不打入 bundle。

image.png

打包

通?;A(chǔ)包中包含RN源碼、第三方依賴庫(kù)、內(nèi)部公共組件等,通過(guò) import 方式引入進(jìn)來(lái),common.js代碼如下:

image.png

基礎(chǔ)包打包命令:

"commonBundle": "react-native bundle --platform android --dev false --entry-file shell/common.js --bundle-output Desktop/index.bundle --assets-dest Desktop/android --config common.config.js"

業(yè)務(wù)包打包命令:

"bussinessBundle": "react-native bundle --platform android --dev false --entry-file shell/business.js --bundle-output Desktop/bussinessBundle.bundle --assets-dest Desktop/android --config business.config.js"

image.png

打包效果圖:

基礎(chǔ)包

image.png

業(yè)務(wù)包

image.png

基礎(chǔ)包、業(yè)務(wù)包加載

iOS

基礎(chǔ)包預(yù)加載

APP 啟動(dòng)時(shí),先加載基礎(chǔ)包,不展示視圖。

//直接使用基礎(chǔ)包初始化js框架 ?NSURL *jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"common.ios" withExtension:@"jsbundle"];

self.bridge= [[RCTBridge alloc] initWithBundleURL:jsCodeLocation moduleProvider:nil launchOptions:launchOptions];

業(yè)務(wù)包加載

暴露RCTBridge的executeSourceCode方法

NSURL *jsCodeLocationBuz = [[NSBundle mainBundle] URLForResource:bundleName withExtension:@"jsbundle"];

NSError *error = nil;?NSData *sourceBus = [NSData dataWithContentsOfFile:jsCodeLocationBuz.path options:NSDataReadingMappedIfSafe error:&error];?[bridge.batchedBridge executeSourceCode:sourceBus sync:NO];

RCTRootView* view = [[RCTRootView alloc] initWithBridge:bridge moduleName:moduleName initialProperties:nil]; //bridge和module傳入

Android

基礎(chǔ)包預(yù)加載以及HomePage業(yè)務(wù)包預(yù)加載

private void preLoadBundle(){

ReactInstanceManager reactInstanceManager = getReactInstanceManager();

//這里會(huì)先加載基礎(chǔ)包index.bundle

if (reactInstanceManager != null && !reactInstanceManager.hasStartedCreatingInitialContext()) {

getReactInstanceManager().addReactInstanceEventListener(new ReactInstanceManager.ReactInstanceEventListener() {

  @Override

  public void onReactContextInitialized(ReactContext context) {

    //加載完成預(yù)加載HomePage.bundle

    ScriptLoadUtil.loadScript(getReactInstanceManager(),  BridgeUtil.getScriptPathType("Me"), BridgeUtil.getScriptPath("Me"));

    if (getReactInstanceManager() != null) {

      getReactInstanceManager().removeReactInstanceEventListener(this);

    }

  }

});

reactInstanceManager.createReactContextInBackground();

}

}

業(yè)務(wù)包加載

通過(guò)傳入業(yè)務(wù)包的類(lèi)型和路徑加載

public static void loadScript(ReactInstanceManager instanceManager, RNUpdateConfig.ScriptType pathType, String scriptPath){

// 當(dāng)設(shè)置成debug模式時(shí),所有需要的業(yè)務(wù)代碼已經(jīng)都加載好了

if (DevKitConfig.DEBUG && ReactUtil.isFromServer(instanceManager)){

return;

}

if (instanceManager != null && instanceManager.getCurrentReactContext() != null){

CatalystInstance instance = instanceManager.getCurrentReactContext().getCatalystInstance();

if(pathType== RNUpdateConfig.ScriptType.ASSET) {

  ScriptLoadUtil.loadScriptFromAsset(WYCoreUtils.getApp(), instance, scriptPath,false);

}else {

  File scriptFile = new File(scriptPath);

  scriptPath = scriptFile.getAbsolutePath();

  ScriptLoadUtil.loadScriptFromFile(scriptPath, instance, scriptPath,false);

}

}

}

業(yè)務(wù)包類(lèi)型分為ScriptType.ASSET和ScriptType.FILE,通過(guò)當(dāng)前手機(jī)是否存在比內(nèi)置包更高版本的RN包進(jìn)行判斷。

業(yè)務(wù)包路徑則通過(guò)頁(yè)面?zhèn)鬏斪侄蜳ageName來(lái)判斷加載哪個(gè)業(yè)務(wù)包,如:HomePage/Me,則就是加載HomePage.bundle

int index = pageName.indexOf("/");

if (index != -1){

return pageName.substring(0, index)+".bundle";

}

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

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

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