同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 热更新。

这个过程中遇到了很多问题,但是在网络上并没有找到太多相关的资料,所以在此做一个记录,也让有相似需求的朋友少走一些弯路。

前提

  • 在某一个版本后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的运行环境隔离开,我们需要一个新的ReactNativeHostReactNativeHost是在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,重写其继承自ReactActivitycreateReactActivityDelegate()方法:

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,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,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...

搞定(〃'▽'〃)。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom