在 react-native (如下称RN)仍是0.39的时候,咱们开始着手构建了一个纯RN app,以后因为长列表的性能问题,进行了一次更新,将版本更新到了0.46,并一直维持 。直到前段时间,遇到了一个新的需求,要把隔壁部门用RN写的一个app(如下称为B app)的一部分业务嵌入咱们的app中。因为B app的业务重度依赖路由,而B app的路由和咱们app所用的路由有一些冲突,简单的组件化而后引用的方式并不适用,同时将两个app打成一个bundle的方法因为依赖冲突也没法采用。最终选择了将两个app分别打成两个bundle的方式,并经过 code-push 热更新。java
这个过程当中遇到了不少问题,可是在网络上并无找到太多相关的资料,因此在此作一个记录,也让有类似需求的朋友少走一些弯路。react
link
的依赖库不能存在冲突。这一步比较简单,RN自己就支持这么作,只须要新建一个 Activity
,在getMainComponentName()
函数中返回新的app注册的名字,(即js代码中AppRegistry.registerComponent()
的第一个参数)就能够了。跳转app可参照android跳转Activity
进行。android
嵌入多个bundle还要互不影响,这就须要把js的运行环境隔离开,咱们须要一个新的ReactNativeHost
,ReactNativeHost
是在MainApplication
类中new出来的,咱们new一个新的便可。而后咱们会发现,本来RN是经过实现了接口ReactApplication
中的getReactNativeHost()
方法对外返回ReactNativeHost
的。ios
public class MainApplication extends Application implements ReactApplication { ... @Override public ReactNativeHost getReactNativeHost() { return mReactNativeHost; }; ... }
检查了一下这个方法的调用,发现RN框架中只有一处调用了此方法。在ReactActivityDelegate
类中,git
protected ReactNativeHost getReactNativeHost() { return ((ReactApplication) getPlainActivity().getApplication()).getReactNativeHost(); }
因而我首先在MainApplication
类中new了一个新的ReactNativeHost
,而且重写了getBundleAssetName()
方法,返回了新的bundle名index.my.android.bundlegithub
private final ReactNativeHost mReactNativeMyHost = new ReactNativeHost(this) { @Override protected String getBundleAssetName() { return "index.my.android.bundle"; } }
而后写了一个新的接口MyReactApplication
,而且在MainApplication
类中实现了这个接口,这个接口与实现以下react-native
MyReactApplication.java public interface MyReactApplication { /** * Get the default {@link ReactNativeHost} for this app. */ ReactNativeHost getReactNativeMyHost(); } -------------------- MainApplication.java public class MainApplication extends Application implements ReactApplication, MyReactApplication { ... @Override public ReactNativeHost getReactNativeHost() { return mReactNativeHost; }; @Override public ReactNativeHost getReactNativeMyHost() { return mReactNativeMyHost; }; ... }
而后重写了ReactActivityDelegate
类,重点在于getReactNativeHost()
方法,其余都是复制了ReactActivityDelegate
类中须要用到的私有方法:网络
public class MyReactActivityDelegate extends ReactActivityDelegate{ private final @Nullable Activity mActivity ; private final @Nullable FragmentActivity mFragmentActivity; private final @Nullable String mMainComponentName ; public MyReactActivityDelegate(Activity activity, @Nullable String mainComponentName) { super(activity, mainComponentName); mActivity = activity; mMainComponentName = mainComponentName; mFragmentActivity = null; } public MyReactActivityDelegate(FragmentActivity fragmentActivity, @Nullable String mainComponentName) { super(fragmentActivity, mainComponentName); mFragmentActivity = fragmentActivity; mMainComponentName = mainComponentName; mActivity = null; } @Override protected ReactNativeHost getReactNativeHost() { return ((MyReactApplication) getPlainActivity().getApplication()).getReactNativeMyHost(); } private Context getContext() { if (mActivity != null) { return mActivity; } return Assertions.assertNotNull(mFragmentActivity); } private Activity getPlainActivity() { return ((Activity) getContext()); } }
而后ReactActivityDelegate
是在Activity
中new出来的,回到咱们为新app写的Activity,重写其继承自ReactActivity
的createReactActivityDelegate()
方法:app
public class MyActivity extends ReactActivity { @Override protected String getMainComponentName() { return "newAppName"; } @Override protected ReactActivityDelegate createReactActivityDelegate() { return new MyReactActivityDelegate(this, getMainComponentName()); } }
而后只须要在B app中经过react-native bundle --platform android --dev false --entry-file index.js --bundle-output outputAndroid/index.my.android.bundle --assets-dest outputAndroid/
打出bundle,而后将bundle和图片资源分别移动到主工程的android的assets和res目录下,打release包便可。须要注意的是,在debug模式下仍然没法访问第二个app,因为debug模式下android的bundle读取机制比较复杂,未作深刻研究,若有必要,能够经过改变默认activity的方式进入第二个activity。框架
使用code-push进行两个bundle更新须要对code-push作一些更改,同时没法采用code-push react-release
的一键式打包,须要手动打包。如下改动基于code-push@5.2.1。
使用code-push须要用getJSBundleFile()
函数取代上一节所写的getBundleAssetName()
方法,因为code-push内经过一个静态常量存储了惟一的一个code-push实例,因此为了不在取bundle的时候发生没必要要的错误,我在new ReactNativeHost
的时候用一个变量保存了code-push实例,并在CodePush.getJSBundleFile("index.android.bundle", MainCodePush)
的时候,经过新增一个参数将这个实例传递了进去。固然须要在code-push中作一些对应的改动。
MainApplication.java private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { ... public CodePush MainCodePush = null; @Override protected String getJSBundleFile() { return CodePush.getJSBundleFile("index.android.bundle", MainCodePush); } @Override protected List<ReactPackage> getPackages() { MainCodePush = new CodePush(codePushKey, getApplicationContext(), BuildConfig.DEBUG,codePushIp); return Arrays.<ReactPackage>asList( new MainReactPackage(), MainCodePush ); } ... mReactNativeMyHost一样如此 ... }; -------- codePush.java public static String getBundleUrl(String assetsBundleFileName) { return getJSBundleFile(assetsBundleFileName, mCurrentInstance); } public static String getJSBundleFile() { return CodePush.getJSBundleFile(CodePushConstants.DEFAULT_JS_BUNDLE_NAME, mCurrentInstance); } public static String getJSBundleFile(String assetsBundleFileName, CodePush context) { mCurrentInstance = context; if (mCurrentInstance == null) { throw new CodePushNotInitializedException("A CodePush instance has not been created yet. Have you added it to your app's list of ReactPackages?"); } return mCurrentInstance.getJSBundleFileInternal(assetsBundleFileName); }
此外,code-push在取bundle的时候会作一些检查,在CodePushUpdateManager
中getCurrentPackageBundlePath()
方法会尝试从更新包的元数据中获取bundle名,在此处我作了一个处理,当元数据的bundle名和传入的bundle名不一致时,采用传入的bundle名,固然这也会使代码的健壮性有所降低。
CodePushUpdateManager.java public String getCurrentPackageBundlePath(String bundleFileName) { String packageFolder = getCurrentPackageFolderPath(); if (packageFolder == null) { return null; } JSONObject currentPackage = getCurrentPackage(); if (currentPackage == null) { return null; } String relativeBundlePath = currentPackage.optString(CodePushConstants.RELATIVE_BUNDLE_PATH_KEY, null); if (relativeBundlePath == null) { return CodePushUtils.appendPathComponent(packageFolder, bundleFileName); } else { String fileName = relativeBundlePath.substring(relativeBundlePath.lastIndexOf("/")+1); if(fileName.equals(bundleFileName)){ return CodePushUtils.appendPathComponent(packageFolder, relativeBundlePath); }else{ String newRelativeBundlePath = relativeBundlePath.substring(0,relativeBundlePath.lastIndexOf("/")+1) + bundleFileName; return CodePushUtils.appendPathComponent(packageFolder, newRelativeBundlePath); } } }
此外,以前的getReactNativeMyHost()
方法存在一些问题,由于code-push只会去调用RN定义的接口getReactNativeHost()
,若是大幅度自定义code-push比较麻烦,并且可能形成更多的潜在问题,因此我修改了一下getReactNativeHost()
接口。经过android的生命周期在MainApplication
中获取当前的Activity
,并保存起来,在getReactNativeHost()
中经过,判断当前Activity
的方式,决定返回的ReactNativeHost
。同时仍然保留以前的写法,由于这种方法是不可靠的,有可能在跳转Activity
后返回错误的ReactNativeHost
,因此保留以前的方法为RN框架提供准确的ReactNativeHost
,这种写法暂时能知足code-push的须要,因为本人java和android的水平所限只能作到这种程度,但愿大佬赐教。最后完整版的MainApplication
以下:
public class MainApplication extends Application implements ReactApplication, MyReactApplication { ... public static String currentActivity = "MainActivity"; private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { public CodePush MainCodePush = null; @Override protected String getJSBundleFile() { return CodePush.getJSBundleFile("index.android.bundle", MainCodePush); } public boolean getUseDeveloperSupport() { return BuildConfig.DEBUG; } @Override protected List<ReactPackage> getPackages() { MainCodePush = new CodePush(codePushKey, getApplicationContext(), BuildConfig.DEBUG,codePushIp); return Arrays.<ReactPackage>asList( new MainReactPackage(), MainCodePush ); } @Override protected String getJSMainModuleName() { return "index"; } }; private final ReactNativeHost mReactNativeMyHost = new ReactNativeHost(this) { public CodePush myCodePush = null; @Override public boolean getUseDeveloperSupport() { return BuildConfig.DEBUG; } @Override protected List<ReactPackage> getPackages() { myCodePush = new CodePush(codePushKey, getApplicationContext(), BuildConfig.DEBUG,codePushIp); return Arrays.<ReactPackage>asList( new MyMainReactPackage(), myCodePush ); } @Override protected String getJSBundleFile() { return CodePush.getJSBundleFile("index.my.android.bundle", myCodePush); } @Override protected String getJSMainModuleName() { return "index"; } }; @Override public ReactNativeHost getReactNativeHost() { if(MainApplication.currentActivity.equals("MainActivity")){ return mReactNativeHost; }else if(MainApplication.currentActivity.equals("MyActivity")){ return mReactNativeMyHost; } return mReactNativeHost; }; @Override public ReactNativeHost getReactNativeMyHost() { return mReactNativeMyHost; }; @Override public void onCreate() { super.onCreate(); this.registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { public String getActivityName(Activity activity){ String allName = activity.getClass().getName(); return allName.substring(allName.lastIndexOf(".")+1); } @Override public void onActivityStopped(Activity activity) {} @Override public void onActivityStarted(Activity activity) { MainApplication.currentActivity = getActivityName(activity); Log.i(getActivityName(activity), "onActivityStarted"); } @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {} @Override public void onActivityResumed(Activity activity) {} @Override public void onActivityPaused(Activity activity) {} @Override public void onActivityDestroyed(Activity activity) {} @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { MainApplication.currentActivity = getActivityName(activity); Log.i(getActivityName(activity), "onActivityCreated" ); } }); } ... }
到此为止,android的code-push改造就完成了。
更新的时候,须要首先分别经过上文提到的react-native bundle ...
命令将两边的工程分别打包,而后合并到同一个文件夹中,最后经过code-push release appName ./outputAndroid x.x.x
命令上传更新,命令的具体细节请参考code-push github。
android完成以后,ios就容易的多。嵌入多个app和android相似,在ios上使用的是UIViewController
,新建一个UIViewController
,其余都和主app一致,只是在 init rootView的时候修改一下moduleName为新的app注册的名字便可。经过UINavigationController
来进行页面跳转,具体开发参见IOS原生开发。
ios在引入bundle的时候十分灵活,只须要在 init 新的 rootView 的时候修改 initWithBundleURL 的值便可。可以下:
@implementation MyViewController - (void)viewDidLoad{ [super viewDidLoad]; NSURL *jsCodeLocation; #ifdef DEBUG jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.bundle?platform=ios&dev=true"]; #else jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; #endif RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation moduleName:@"appName" initialProperties:nil launchOptions:nil]; rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; self.view = rootView; } @end
无论debug时的远程packager服务的地址仍是release时包名均可以自行更改。
最后在B app中经过react-native bundle --platform ios --dev false --entry-file index.js --bundle-output outputIOS/my.jsbundle --assets-dest outputIOS/
打出bundle,将jsbundle和图片资源在Xcode中引入工程便可。
ios下的热更新依然须要对code-push作一些修改,在取bundle的时候,code-push会去比较一个本地bundle修改时间与元数据中是否一致,当取第二个bundle的时候,此值会不一致,具体缘由因时间缘由没有深究,暂时处理为,当bundle名与元数据中不一样时,不检查修改时间。修改的代码以下:
+ (NSURL *)bundleURLForResource:(NSString *)resourceName withExtension:(NSString *)resourceExtension subdirectory:(NSString *)resourceSubdirectory bundle:(NSBundle *)resourceBundle { bundleResourceName = resourceName; bundleResourceExtension = resourceExtension; bundleResourceSubdirectory = resourceSubdirectory; bundleResourceBundle = resourceBundle; [self ensureBinaryBundleExists]; NSString *logMessageFormat = @"Loading JS bundle from %@"; NSError *error; NSString *packageFile = [CodePushPackage getCurrentPackageBundlePath:&error]; NSURL *binaryBundleURL = [self binaryBundleURL]; if (error || !packageFile) { CPLog(logMessageFormat, binaryBundleURL); isRunningBinaryVersion = YES; return binaryBundleURL; } NSString *binaryAppVersion = [[CodePushConfig current] appVersion]; NSDictionary *currentPackageMetadata = [CodePushPackage getCurrentPackage:&error]; if (error || !currentPackageMetadata) { CPLog(logMessageFormat, binaryBundleURL); isRunningBinaryVersion = YES; return binaryBundleURL; } NSString *packageDate = [currentPackageMetadata objectForKey:BinaryBundleDateKey]; NSString *packageAppVersion = [currentPackageMetadata objectForKey:AppVersionKey]; Boolean checkFlag = true;//双bundle状况下bundle名和meta中不一致不检查修改时间 //用来取自定义的bundle NSArray *urlSeparated = [[NSArray alloc]init]; NSString *fileName = [[NSString alloc]init]; NSString *fileWholeName = [[NSString alloc]init]; urlSeparated = [packageFile componentsSeparatedByString:@"/"]; fileWholeName = [urlSeparated lastObject]; fileName = [[fileWholeName componentsSeparatedByString:@"."] firstObject]; if([fileName isEqualToString:resourceName]){ checkFlag = true; }else{ checkFlag = false; } if ((!checkFlag ||[[CodePushUpdateUtils modifiedDateStringOfFileAtURL:binaryBundleURL] isEqualToString:packageDate]) && ([CodePush isUsingTestConfiguration] ||[binaryAppVersion isEqualToString:packageAppVersion])) { // Return package file because it is newer than the app store binary's JS bundle if([fileName isEqualToString:resourceName]){ NSURL *packageUrl = [[NSURL alloc] initFileURLWithPath:packageFile]; CPLog(logMessageFormat, packageUrl); isRunningBinaryVersion = NO; return packageUrl; }else{ NSString *newFileName = [[NSString alloc]init]; NSString *baseUrl = [packageFile substringToIndex:([packageFile length] - [fileWholeName length] )]; newFileName = [newFileName stringByAppendingFormat:@"%@%@%@", resourceName, @".", resourceExtension]; NSString *newPackageFile = [baseUrl stringByAppendingString:newFileName]; NSURL *packageUrl = [[NSURL alloc] initFileURLWithPath:newPackageFile]; CPLog(logMessageFormat, packageUrl); isRunningBinaryVersion = NO; return packageUrl; } } else { BOOL isRelease = NO; #ifndef DEBUG isRelease = YES; #endif if (isRelease || ![binaryAppVersion isEqualToString:packageAppVersion]) { [CodePush clearUpdates]; } CPLog(logMessageFormat, binaryBundleURL); isRunningBinaryVersion = YES; return binaryBundleURL; } }
到此为止,ios的code-push改造就完成了。
更新的时候,须要首先分别经过上文提到的react-native bundle ...命令将两边的工程分别打包,而后合并到同一个文件夹中,最后经过code-push release appName ./outputIOS x.x.x命令上传更新,命令的具体细节请参考code-push github。
暂时已发现的崩溃只有一个,当进入过B app以后,返回主app,这个时候若是进行code-push更新检查,而且发现更新以后进行更新,ios会崩溃,更新失败;android会报更新错误,但实际上更新成功,须要下次启动app才生效。
android的缘由没深刻研究,ios的缘由主要是由于code-push中有些静态变量是在加载bundle的时候保存的,当进入B app的时候修改了这些变量的值,返回主app的时候并无从新加载bundle,因此仍然保留了错误的值,更新的时候会涉及到相关的值,而后就会崩溃报错。
解决方法暂时为记录flag,一旦进入过B app就再也不进行更新。
修改过的code-push@5.2.1 见 https://github.com/haven2worl...
搞定(〃'▽'〃)。