同app下多個react-native jsBundle的解決方案

在 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

前提

  • 在某一個版本後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進行。android

嵌入多個bundle

嵌入多個bundle還要互不影響,這就須要把js的運行環境隔離開,咱們須要一個新的ReactNativeHostReactNativeHost是在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,重寫其繼承自ReactActivitycreateReactActivityDelegate()方法: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 熱更新

使用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的時候會作一些檢查,在CodePushUpdateManagergetCurrentPackageBundlePath()方法會嘗試從更新包的元數據中獲取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...

搞定(〃'▽'〃)。

相關文章
相關標籤/搜索