由於須要將各業務線經過劃分jsbundle的形式進行分離,以達到node
參考了攜程以及各類網絡版本的作法,大體總結爲三種react
綜上所述,js端的bundle拆分用第三種方案最優android
由於Metro官方文檔過於簡陋,實在看不懂,因此借鑑了一些使用Metro的項目git
好比(感謝開原做者的貢獻):github.com/smallnew/re…github
這個項目較爲完整,簡要配置下就能夠直接使用,因此js端拆包主要參考自這個項目,經過配置Metro的createModuleIdFactory,processModuleFilter回調,咱們能夠很容易的自定義生成moduleId,以及篩選基礎包內容,來達到基礎業務包分離的目的,由於實際上拆分jsbundle主要工做也就在於moduleId分配以及打包filter配置,咱們能夠觀察下打包後的js代碼結構json
經過react-native bundle --platform android --dev false --entry-file index.common.js --bundle-output ./CodePush/common.android.bundle.js --assets-dest ./CodePush --config common.bundle.js --minify false
指令打出基礎包(minify設爲false便於查看源碼)react-native
function (global) {
"use strict";
global.__r = metroRequire;
global.__d = define;
global.__c = clear;
global.__registerSegment = registerSegment;
var modules = clear();
var EMPTY = {};
var _ref = {},
hasOwnProperty = _ref.hasOwnProperty;
function clear() {
modules = Object.create(null);
return modules;
}
function define(factory, moduleId, dependencyMap) {
if (modules[moduleId] != null) {
return;
}
modules[moduleId] = {
dependencyMap: dependencyMap,
factory: factory,
hasError: false,
importedAll: EMPTY,
importedDefault: EMPTY,
isInitialized: false,
publicModule: {
exports: {}
}
};
}
function metroRequire(moduleId) {
var moduleIdReallyIsNumber = moduleId;
var module = modules[moduleIdReallyIsNumber];
return module && module.isInitialized ? module.publicModule.exports : guardedLoadModule(moduleIdReallyIsNumber, module);
}
複製代碼
這裏主要看__r
,__d
兩個變量,賦值了兩個方法metroRequire
,define
,具體邏輯也很簡單,define
至關於在表中註冊,require
至關於在表中查找,js代碼中的import
,export
編譯後就就轉換成了__d
與__r
,再觀察一下原生Metro代碼的node_modules/metro/src/lib/createModuleIdFactory.js
文件,代碼爲:數組
function createModuleIdFactory() {
const fileToIdMap = new Map();
let nextId = 0;
return path => {
let id = fileToIdMap.get(path);
if (typeof id !== "number") {
id = nextId++;
fileToIdMap.set(path, id);
}
return id;
};
}
module.exports = createModuleIdFactory;
複製代碼
邏輯比較簡單,若是查到map裏沒有記錄這個模塊則id自增,而後將該模塊記錄到map中,因此從這裏能夠看出,官方代碼生成moduleId的規則就是自增,因此這裏要替換成咱們本身的配置邏輯,咱們要作拆包就須要保證這個id不能重複,可是這個id只是在打包時生成,若是咱們單獨打業務包,基礎包,這個id的連續性就會丟失,因此對於id的處理,咱們仍是能夠參考上述開源項目,每一個包有十萬位間隔空間的劃分,基礎包從0開始自增,業務A從1000000開始自增,又或者經過每一個模塊本身的路徑或者uuid等去分配,來避免碰撞,可是字符串會增大包的體積,這裏不推薦這種作法。因此總結起來js端拆包仍是比較容易的,這裏就再也不贅述緩存
用過CodePush的同窗都能感覺到它強大的功能以及穩定的表現,更新,回滾,強更,環境管控,版本管控等等功能,越用越香,可是它不支持拆包更新,若是本身從新實現一套功能相似的代價較大,因此我嘗試經過改造來讓它支持多包獨立更新,來知足咱們拆包的業務需求,改造原則:安全
經過閱讀源碼,咱們能夠發現,只要隔離了包下載的路徑以及每一個包本身的狀態信息文件,而後對多包併發更新時,作一些同步處理,就能夠作到多包獨立更新
改造後的包存放路徑如上圖所示app.json文件存放包的信息,由檢測更新的接口返回以及本地邏輯寫入的一些信息,好比hash值,下載url,更新包的版本號,bundle的相對路徑(本地代碼寫入)等等
codepush.json會記錄當前包的hash值以及上一個包的hash值,用於回滾,因此正常來說一個包會有兩個版本,上一版本用於備份回滾,回滾成功後會刪除掉當前版本,具體邏輯能夠自行閱讀了解,因此我這裏總結一下改動
主要改動爲增長pathPrefix和bundleFileName兩個傳參,用於分離bundle下載的路徑
增長了bundleFileName和pathPrefix參數的方法有
只增長了pathPrefix參數的方法有
由於官方代碼只對單個包狀態作管理,因此這裏咱們要改成支持對多個包狀態作管理
由於拆包後,對包的加載是增量的,因此咱們在初始化業務場景A的ReactRootView時,增量加載業務A的jsbundle,其餘業務場景同理,獲取業務A jsbundle路徑須要藉助改造後的CodePush方法,經過傳入bundleFileName,pathPrefix
官方代碼爲更新到新的bundle後,加載完bundle即從新建立整個RN環境,拆包後此種方法不可取,若是業務包更新完後,從新加載業務包而後再重建RN環境,會致使基礎包代碼丟失而報錯,因此增長一個只加載jsbundle,不重建RN環境的方法,在更新業務包的時候使用
好比官方更新代碼爲:
CodePushNativeModule#loadBundle方法
private void loadBundle(String pathPrefix, String bundleFileName) {
try {
// #1) Get the ReactInstanceManager instance, which is what includes the
// logic to reload the current React context.
final ReactInstanceManager instanceManager = resolveInstanceManager();
if (instanceManager == null) {
return;
}
String latestJSBundleFile = mCodePush.getJSBundleFileInternal(bundleFileName, pathPrefix);
// #2) Update the locally stored JS bundle file path
setJSBundle(instanceManager, latestJSBundleFile);
// #3) Get the context creation method and fire it on the UI thread (which RN enforces)
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
try {
// We don't need to resetReactRootViews anymore // due the issue https://github.com/facebook/react-native/issues/14533 // has been fixed in RN 0.46.0 //resetReactRootViews(instanceManager); instanceManager.recreateReactContextInBackground(); mCodePush.initializeUpdateAfterRestart(pathPrefix); } catch (Exception e) { // The recreation method threw an unknown exception // so just simply fallback to restarting the Activity (if it exists) loadBundleLegacy(); } } }); } catch (Exception e) { // Our reflection logic failed somewhere // so fall back to restarting the Activity (if it exists) CodePushUtils.log("Failed to load the bundle, falling back to restarting the Activity (if it exists). " + e.getMessage()); loadBundleLegacy(); } } 複製代碼
改造爲業務包增量加載,基礎包才重建ReactContext
if ("CommonBundle".equals(pathPrefix)) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
try {
// We don't need to resetReactRootViews anymore // due the issue https://github.com/facebook/react-native/issues/14533 // has been fixed in RN 0.46.0 //resetReactRootViews(instanceManager); instanceManager.recreateReactContextInBackground(); mCodePush.initializeUpdateAfterRestart(pathPrefix); } catch (Exception e) { // The recreation method threw an unknown exception // so just simply fallback to restarting the Activity (if it exists) loadBundleLegacy(); } } }); } else { JSBundleLoader latestJSBundleLoader; if (latestJSBundleFile.toLowerCase().startsWith("assets://")) { latestJSBundleLoader = JSBundleLoader.createAssetLoader(getReactApplicationContext(), latestJSBundleFile, false); } else { latestJSBundleLoader = JSBundleLoader.createFileLoader(latestJSBundleFile); } CatalystInstance catalystInstance = resolveInstanceManager().getCurrentReactContext().getCatalystInstance(); latestJSBundleLoader.loadScript(catalystInstance); mCodePush.initializeUpdateAfterRestart(pathPrefix); } 複製代碼
啓動業務ReactRootView時增量加載jsbundle的邏輯同上
CodePush#sync代碼
const sync = (() => {
let syncInProgress = false;
const setSyncCompleted = () => { syncInProgress = false; };
return (options = {}, syncStatusChangeCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback) => {
let syncStatusCallbackWithTryCatch, downloadProgressCallbackWithTryCatch;
if (typeof syncStatusChangeCallback === "function") {
syncStatusCallbackWithTryCatch = (...args) => {
try {
syncStatusChangeCallback(...args);
} catch (error) {
log(`An error has occurred : ${error.stack}`);
}
}
}
if (typeof downloadProgressCallback === "function") {
downloadProgressCallbackWithTryCatch = (...args) => {
try {
downloadProgressCallback(...args);
} catch (error) {
log(`An error has occurred: ${error.stack}`);
}
}
}
if (syncInProgress) {
typeof syncStatusCallbackWithTryCatch === "function"
? syncStatusCallbackWithTryCatch(CodePush.SyncStatus.SYNC_IN_PROGRESS)
: log("Sync already in progress.");
return Promise.resolve(CodePush.SyncStatus.SYNC_IN_PROGRESS);
}
syncInProgress = true;
const syncPromise = syncInternal(options, syncStatusCallbackWithTryCatch, downloadProgressCallbackWithTryCatch, handleBinaryVersionMismatchCallback);
syncPromise
.then(setSyncCompleted)
.catch(setSyncCompleted);
return syncPromise;
};
})();
複製代碼
改造後
const sync = (() => {
let syncInProgress = false;
//增長一個管理併發任務的隊列
let syncQueue = [];
const setSyncCompleted = () => {
syncInProgress = false;
回調完成後執行隊列裏的任務
if (syncQueue.length > 0) {
log(`Execute queue task, current queue: ${syncQueue.length}`);
let task = syncQueue.shift(1);
sync(task.options, task.syncStatusChangeCallback, task.downloadProgressCallback, task.handleBinaryVersionMismatchCallback)
}
};
return (options = {}, syncStatusChangeCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback) => {
let syncStatusCallbackWithTryCatch, downloadProgressCallbackWithTryCatch;
if (typeof syncStatusChangeCallback === "function") {
syncStatusCallbackWithTryCatch = (...args) => {
try {
syncStatusChangeCallback(...args);
} catch (error) {
log(`An error has occurred : ${error.stack}`);
}
}
}
if (typeof downloadProgressCallback === "function") {
downloadProgressCallbackWithTryCatch = (...args) => {
try {
downloadProgressCallback(...args);
} catch (error) {
log(`An error has occurred: ${error.stack}`);
}
}
}
if (syncInProgress) {
typeof syncStatusCallbackWithTryCatch === "function"
? syncStatusCallbackWithTryCatch(CodePush.SyncStatus.SYNC_IN_PROGRESS)
: log("Sync already in progress.");
//檢測到併發任務,放入隊列排隊
syncQueue.push({
options,
syncStatusChangeCallback,
downloadProgressCallback,
handleBinaryVersionMismatchCallback
});
log(`Enqueue task, current queue: ${syncQueue.length}`);
return Promise.resolve(CodePush.SyncStatus.SYNC_IN_PROGRESS);
}
syncInProgress = true;
const syncPromise = syncInternal(options, syncStatusCallbackWithTryCatch, downloadProgressCallbackWithTryCatch, handleBinaryVersionMismatchCallback);
syncPromise
.then(setSyncCompleted)
.catch(setSyncCompleted);
return syncPromise;
};
})();
複製代碼
該方案主流程已經ok,多包併發更新,單包獨立更新基本沒有問題,如今還在邊界場景以及壓力測試當中,待方案健壯後再上源碼作詳細分析
該方案一樣知足自建server的需求,關於自建server能夠參考:github.com/lisong/code…
再次感謝開源做者的貢獻