摘要:最終選擇了將兩個分別打成兩個的方式,并通過熱更新。以下改動基于。嵌入多個完成之后,就容易的多。最后在中通過打出,將和圖片資源在中引入工程即可。解決方法暫時為記錄,一旦進入過就不再進行更新。
在 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 ListgetPackages() { MainCodePush = new CodePush(codePushKey, getApplicationContext(), BuildConfig.DEBUG,codePushIp); return Arrays. 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 ListgetPackages() { MainCodePush = new CodePush(codePushKey, getApplicationContext(), BuildConfig.DEBUG,codePushIp); return Arrays. 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 getPackages() { myCodePush = new CodePush(codePushKey, getApplicationContext(), BuildConfig.DEBUG,codePushIp); return Arrays. 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原生開發。
嵌入多個bundleios在引入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...
搞定(〃"▽"〃)。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/94601.html
摘要:在版本上,安卓系統上的渲染就有鋸齒,后來我們不分效果是使用圖片實現的,汗啊第三方組件不全。搖一搖問題的解決在我們使用了等狀態管理時,熱更新不會更新這些代碼,而頻繁搖一搖實在是太累了。此時可以使用此時相當于虛擬了一個搖一搖事件。 公司本年度有App任務,陸陸續續用RN開發了兩個應用。一款是涉及儀器控制的平板項目,另一款是客戶端的App。下文談談使用RN開發的部分認知(其實只是隨便扯一扯,...
閱讀 1721·2021-11-11 10:58
閱讀 4202·2021-09-09 09:33
閱讀 1265·2021-08-18 10:23
閱讀 1553·2019-08-30 15:52
閱讀 1632·2019-08-30 11:06
閱讀 1876·2019-08-29 14:03
閱讀 1513·2019-08-26 14:06
閱讀 2955·2019-08-26 10:39