由于须要将各业务线经过划分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…
再次感谢开源做者的贡献