React Native 拆包及實踐「iOS&Android」

一.拆包

拆包的方式通常有三種,分別爲Facebook的Metro、攜程的moles-packer和diff patch(可使用Google的diff-match-patch)。但目前最好的方式可能仍是Metro。在調研的過程當中,接觸最先的,也是最全的例子爲react-native-multibundler,這個例子甚至開發了可視化工具,進行拆包打包。node

bundle代碼拆分類型:基礎包與業務包。
基礎包:將一些重複的js代碼與第三方依賴庫打成一個包。
業務包:根據應用內的不一樣業務邏輯,拆分出一個或多個包。react

1.Metro安裝

實際在運行npm install時React Native已經安裝Metro了,只不過可能並非最新版(跟React Native版本有關),想使用最新版Metro,須要單獨安裝。android

npm install --save-dev metro metro-core
複製代碼

ios

yarn add --dev metro metro-core
複製代碼

2.Metro配置

配置Metro有三種方法,分別爲metro.config.jsmetro.config.jsonpackage.json中添加metro字段,經常使用的方式爲 metro.config.jsgit

Metro配置內部結構大體像這樣:github

module.exports = {
  resolver: {
    /* resolver options */
  },
  transformer: {
    /* transformer options */
  },
  serializer: {
    /* serializer options */
  },
  server: {
    /* server options */
  }

  /* general options */
};
複製代碼

每一個optoins內都有不少配置選項,而對於咱們這些初學者來講,最重要的是serializer選項內的createModuleIdFactoryprocessModuleFilternpm

如圖:json

createModuleIdFactory :在v0.24.1後,Metro支持了經過此方法配置自定義模塊ID,一樣支持字符串類型ID,用於生成require語句的模塊ID,其類型爲() => (path: string) => number(帶有返回參數的返回函數的函數),其中path爲各個module的完整路徑。此方法的另外一個用途就是屢次打包時,對於同一個模塊生成相同的ID,下次更新發版時,不會因ID不一樣找不到Module。react-native

processModuleFilter:根據給出的條件,對Module進行過濾,將不須要的模塊過濾掉。其類型爲(module: Array<Module>) => boolean,其中module爲輸出的模塊,裏面帶着相應的參數,根據返回的波爾值判斷是否過濾當前模塊。返回false爲過濾,不打入bundle。服務器

接下來上代碼:

function createModuleIdFactory() {
  //獲取命令行執行的目錄,__dirname是nodejs提供的變量
  const projectRootPath = __dirname;
  return (path) => {
    let name = '';
    // 若是須要去除react-native/Libraries路徑去除能夠放開下面代碼
    // if (path.indexOf('node_modules' + pathSep + 'react-native' + pathSep + 'Libraries' + pathSep) > 0) {
    //   //這裏是react native 自帶的庫,因其通常不會改變路徑,因此可直接截取最後的文件名稱
    //   name = path.substr(path.lastIndexOf(pathSep) + 1);
    // }
    if (path.indexOf(projectRootPath) == 0) {
      /*
        這裏是react native 自帶庫之外的其餘庫,因是絕對路徑,帶有設備信息,
        爲了不重複名稱,能夠保留node_modules直至結尾
        如/{User}/{username}/{userdir}/node_modules/xxx.js 須要將設備信息截掉
      */
      name = path.substr(projectRootPath.length + 1);
    }
    //js png字符串 文件的後綴名能夠去掉
    // name = name.replace('.js', '');
    // name = name.replace('.png', '');
    //最後在將斜槓替換爲下劃線
    let regExp = pathSep == '\\' ? new RegExp('\\\\', "gm") : new RegExp(pathSep, "gm");
    name = name.replace(regExp, '_');
    //名稱加密
    if (isEncrypt) {
      name = md5(name);
    }
    return name;
  };
}
複製代碼

須要生成什麼樣的模塊ID,能夠根據本身的狀況與喜愛而定,不管是加密,拼接,甚至能夠直接將獲取到的path返回,惟一注意的是規則要統一,不然會沒法找到相應的模塊,固然模塊ID定的越長,最終的bundle文件就越大,ID長短仍是要適中,不過經過MD5加密後,長短已經無所謂了。

在打業務包時,可使用filter對基礎包內已有模塊進行過濾,減少bundle文件大小。

function processModuleFilter(module) {
  //過濾掉path爲__prelude__的一些模塊(基礎包內已有)
  if (module['path'].indexOf('__prelude__') >= 0) {
    return false;
  }
  //過濾掉node_modules內的模塊(基礎包內已有)
  if (module['path'].indexOf(pathSep + 'node_modules' + pathSep) > 0) {
    /*
      但輸出類型爲js/script/virtual的模塊不能過濾,通常此類型的文件爲核心文件,
      如InitializeCore.js。每次加載bundle文件時都須要用到。
    */
    if ('js' + pathSep + 'script' + pathSep + 'virtual' == module['output'][0]['type']) {
      return true;
    }
    return false;
  }
  //其餘就是應用代碼
  return true;
}
複製代碼

在xxx.config.js文件內添加上述兩個方法後,將方法引入到module.exports內的serializeroptions內。

module.exports = {
  serializer: {
    createModuleIdFactory: config.createModuleIdFactory,
    processModuleFilter: config.processModuleFilter
    /* serializer options */
  }
}
複製代碼

3.Metro使用

根據基礎包業務包的不一樣,添加 --config <path/to/config> 參數對相應入口文件打包。Metro官文雖然標明支持其餘路徑的配置文件,但至今沒有成功過,只能在項目根目錄添加配置文件,多是我添加路徑的方式不對,若是你知道如何添加其餘路徑config.js,請在issue中偷偷告訴我:sweat_smile:。

基礎包:
將須要的第三方依賴包與React Native的包、js文件等,能夠經過import方式引入到一個js文件內,如basics.js,再使用basics.config.js當作參數傳入到--confg後。

使用終端切換到項目根目錄,執行命令:

react-native bundle --platform android --dev false --entry-file src/basics/basics.js --bundle-output ./android/app/src/main/assets/basics.android.bundle --assets-dest android/app/src/main/res/ --config basics.config.js
複製代碼

業務包:
根據本身應用的業務邏輯,分出不一樣的業務入口,並使用AppRegistry註冊業務的主Component,如index1.js,使用business.config.js傳入到--config後。

命令以下:

react-native bundle --platform android --dev false --entry-file src/index/index1.js --bundle-output ./android/app/src/main/assets/business1.android.bundle --assets-dest android/app/src/main/res/ --config business.config.js
複製代碼

將上述兩種命令中的路徑,替換爲本身的路徑,分了幾個業務包就須要執行幾回命令,能夠將命令使用&&鏈接,寫入到腳本文件內,如Linux的.sh或Windows的.bat文件,執行腳本文件便可。

經過react-native bundle -h命令能夠查看相應的參數配置選項,其中--entry-file爲加載的入口文件,如圖:

接下來看下createModuleIdFactory的log輸出結果:

應用內的js:

react native的js:
三方依賴庫的js:

第一行爲方法內的path路徑
第二行爲根據是React Native自帶文件仍是三方庫文件截取名稱
第三行是去除後綴的的名稱
第四行是替換斜槓的名稱
第五行是加密後的字符串

若是不加密的話,能夠去除項目的目錄,不然bundle文件會將項目結構暴露。

加密前:

加密後:

二.Android 原生加載

:sparkles:目前Demo中使用的是Koltin語言,若是須要Java語言,能夠切換build.gradleisUseKotlin的值爲false後點擊Sync按鈕進行同步。

1.源碼淺析

React Native 加載bundle文件有三種方式,分別是從assets目錄,本地File目錄與Metro本地Server的delta bundle 。而平時用模擬器開發運行,更新文件雙擊R鍵時,使用的就是delta bundle。接下來就須要尋找加載bundle的接口文件,調用接口完成對不一樣業務包的加載,而基礎包會在調用createReactContextInBackground時加載。

每一個React Native頁面都會繼承ReactActivity,在onCreate方法內,會調用mDelegate.onCreate,在此方法內建立RootView,並設置到ContentView上。

看下源碼邏輯:

Delegate.loadApp->ReactRootView.startReactApplication->
attachToReactInstanceManager->ReactInstanceManager.attachRootView->
attachRootViewToInstance->ReactRootView.runApplication->
catalystInstance.getJSModule(AppRegistry.class).runApplication
複製代碼

最後會經過CatalystInstance調用runApplication方法進行頁面的呈現,若是在沒有加載對應的bundle文件時,會報Application xxx has not been registered.之類的錯誤,只需在調用runApplication前將bundle文件加載便可。而接口CatalystInstance繼承了一個名爲JSBundleLoaderDelegate的接口,此接口中的三個方法分別爲loadScriptFromAssetsloadScriptFromFileloadScriptFromDeltaBundle,經過名稱可看出是用來load不一樣位置的bundle的。

在ReactRootView的runApplication內,CatalystInstance是調用ReactContext.getCatalystInstance方法獲取,而ReactContext內的CatalystInstance是在其建立時從ReactInstanceManager.createReactContext方法內由CatalystInstanceImpl的Builder新建。

ReactContext能夠經過ReactApplication.getReactNativeHost.getReactInstanceManager.getCurrentReactContext獲取,所以能夠直接本身寫一個工具類,在工具類內將須要加載的bundle文件提早加載好便可。

2.功能實現

實現此功能,Demo中用了兩種方式,兩種方式都須要使用工具類JsLoaderUtil

一種是新建一個類做爲基類,它繼承ReactActivity,並重寫了createReactActivityDelegategetMainComponentName兩個方法,在createReactActivityDelegate方法內新建ReactActivityDelegate時的onCreate方法調用super前,經過工具類將約定的組件加載好。這種方式的好處是,在子類內或進入子類前不用關心加載bundle過程的代碼,基類中已經寫好了,只須要告訴基類加載哪一個業務的bundle文件,如Business1Activity。這種方式的另外一個用法就是在進入子類前直接告訴工具類須要加載的bundle文件,而在子類中則無需增長任何代碼,僅僅繼承BaseReactActivity,如Business2Activity

public class BaseReactActivity extends ReactActivity {
  @Override
  protected ReactActivityDelegate createReactActivityDelegate() {
      String localBundleName = getBundleName();
      if (!TextUtils.isEmpty(localBundleName)) {
          JsLoaderUtil.jsState.bundleName = localBundleName;
      }
      return new ReactActivityDelegate(this, getMainComponentName()) {
          @Override
          protected void onCreate(Bundle savedInstanceState) {
              JsLoaderUtil.load(getApplication(), 
                  () -> super.onCreate(savedInstanceState));
          }
      };
  }

  @Nullable
  @Override
  protected String getMainComponentName() {
      return JsLoaderUtil.jsState.componentName;
  }

  protected String getBundleName() {
      return "";
  }

}
複製代碼

Demo中另外一種方式是,讓子類直接繼承ReactActivity,而在進入子類前就用工具類加載好須要的業務bundle文件。這種方式的好處是不用拘泥於繼承的父類,但須要注意是在進入頁面前,必定要對業務包加載,不然會報錯。 如Business3ActivityBusiness4Activity

3.Double Tap R

到此咱們的bundle文件已經加載好了,但不可能老是進行打包調試,平時開發時仍是須要雙擊R進行熱更新加載的。但JS代碼都已經進行了業務拆分,而且Application中只對React Native返回了基礎包的bundle,業務包分散在各個業務邏輯上。這時就須要一個開關來控制究竟是加載文件bundle仍是delta bundle,這大體分爲三步或四步完成。

第一步,在index.js文件內將拆分出來的業務包導入,至關於一次性將業務模塊所有註冊。

import './src/index/index1';
import './src/index/index2';
import './src/index/index3';
import './src/index/index4';
複製代碼

第二步,在JsLoaderUtil工具類內增長判斷,若是是Dev模式,直接返回,不加載bundle而且不調用createReactContextInBackground

第三步,在MainApplicationReactNativeHostgetUseDeveloperSupport方法內返回是否爲Dev模式標誌,並在getJSMainModuleName方法內返回以前的index.js名稱,告訴React Native此爲入口文件。

這時就能夠進入一個業務頁面後,雙擊R更新頁面內容了,但在切換開關時重啓應用,會沒法正常reload,就算進入頁面,也會報錯崩潰導致被殺掉進程,再進入應用就能夠了。與其讓它崩潰,不如要麼將應用進程殺掉重啓,要麼增長第四步內容。

第四步,在MainActivityonDestroy內,調用System.exit(0),切換開關後重啓應用就能夠正常使用了。

4.特殊說明

每個js文件都至關於一個Module,而React Native對加載過的Module不會再次加載,也就是說,若是先加載assets內的bundle再加載本地File的bundle文件,呈現的還會是assets內的bundle文件,除非殺掉進程重啓後,先加載本地File的bundle文件,纔會生效,並沒找到很好的解決方法。若是你知道如何解決請在issue中告訴我。

assets目錄下的bundle.zip壓縮包爲帶有File文字的業務包,用來測試從本地File加載功能。而assets內其餘的業務bundle文件,如business1.android.bundle,是帶有Assets文字的bundle包,用來測試從assets加載功能。JS代碼中,如Business1.js,是帶有Runtime文字的業務,用來測試開發過程當中雙擊R鍵熱更新功能。

5.效果演示:

三.iOS 原生加載

1.源碼接入

相對於Android,iOS加載多個bundle文件較簡單,只須要對RCTBridge擴展暴露如下接口便可:

-(void)executeSourceCode:(NSData *)sourceCode sync:(BOOL)sync;

2.實踐(如下是以一個基礎包和一個業務包測試)

1.將打包好的基礎包和業務包導入項目中

圖1:

2.在App啓動時加載基礎包 圖2:

3.在詳情頁或者加載基礎包以後預加載業務包

圖3:

4.輸出信息:先加載了基礎包,後成功加載業務包,且頁面&邏輯正常

圖4:

四. 功能展望

以上就是Demo中的所有內容了,對於下一步的功能展望就是,經過向工具類中傳遞不一樣的與服務器定好的模塊Key,去下載不一樣的bundle內容,一樣能夠根據Key的不一樣,下載須要更新的圖片資源,由工具類拷貝到指定的本地目錄,供應用進行更新加載。

GitHub地址:github.com/yxyhail/Met…

相關文章
相關標籤/搜索