在 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 熱更新。
這個過程當中遇到了許多題目,然則在收集上並沒有找到太多相干的材料,所以在此做一個紀錄,也讓有相似需求的朋儕少走一些彎路。
條件
- 在某一個版本后RN會在運轉的時刻搜檢RN原生部份的版本和RN js部份的版本,所以我們末了只能將RN升級到B app的0.52 。從代碼看假如有一兩個版本的差異應當也能夠,然則沒有做嘗試。
- 終究處理方案中是以我方app的原生部份為基本,到場B app的bundle,這意味着,雖然我們能夠把B app的原生代碼複製到我們的工程當中,然則雙方須要
link
的依靠庫不能存在爭執。
Android
嵌入多個app
這一步比較簡樸,RN自身就支撐這麼做,只須要新建一個 Activity
,在getMainComponentName()
函數中返回新的app註冊的名字,(即js代碼中AppRegistry.registerComponent()
的第一個參數)就能夠了。跳轉app可參照android跳轉Activity
舉行。
嵌入多個bundle
嵌入多個bundle還要互不影響,這就須要把js的運轉環境隔脫離,我們須要一個新的ReactNativeHost
,ReactNativeHost
是在MainApplication
類中new出來的,我們new一個新的即可。然後我們會發明,底本RN是經由過程完成了接口ReactApplication
中的getReactNativeHost()
要領對外返回ReactNativeHost
的。
public class MainApplication extends Application implements ReactApplication {
...
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
};
...
}
搜檢了一下這個要領的挪用,發明RN框架中只要一處挪用了此要領。在ReactActivityDelegate
類中,
protected ReactNativeHost getReactNativeHost() {
return ((ReactApplication) getPlainActivity().getApplication()).getReactNativeHost();
}
因而我起首在MainApplication
類中new了一個新的ReactNativeHost
,而且重寫了getBundleAssetName()
要領,返回了新的bundle名index.my.android.bundle
private final ReactNativeHost mReactNativeMyHost = new ReactNativeHost(this) {
@Override
protected String getBundleAssetName() {
return "index.my.android.bundle";
}
}
然後寫了一個新的接口MyReactApplication
,而且在MainApplication
類中完成了這個接口,這個接口與完成以下
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()
要領:
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 熱更新
運用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。
IOS
嵌入多個app
android完成以後,ios就輕易的多。嵌入多個app和android相似,在ios上運用的是UIViewController
,新建一個UIViewController
,其他都和主app一致,只是在 init rootView的時刻修正一下moduleName為新的app註冊的名字即可。經由過程UINavigationController
來舉行頁面跳轉,詳細開闢拜見IOS原生開闢。
嵌入多個bundle
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中引入工程即可。
code-push 熱更新
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…
搞定(〃’▽’〃)。