android项目集成多个RN界面

公司app数量较多,为了避免手机桌面上都是app的启动图标,不方便使用。因此业务提出需求:安装一个app,进入app后,界面上显示不同图标(对应不同业务),点击不同图标,启动对应的业务界面。
我司开发平台使用的是React Native,如果按照常规做法,创建一个RN项目,所有业务都写在该项目中,则打包后的apk将越来越大,代码维护管理成本也大。
为了解决apk大小问题,确认了一个方案:原生项目集成多个RN界面,每个RN界面对应不同的业务,并且每个RN界面的bundle文件相互独立,用户可按需下载,app不会很大。
原理图:


app集成原理图.png

要实现android集成多个RN界面,需要做如下工作:

1.android工程集成react native
2.编辑ReactActivity业务界面
3.打离线包
4.图片不显示问题解决

1.android工程集成react native

1.1 创建package.json文件

在工程根目录路径下,执行npm init命令,并填写相关信息。成功后,生成package.json文件。

  • 在文件中添加"start": "node node_modules/react-native/local-cli/cli.js start"
  • 执行yarn add react-native react
//package.json
{
  "name": "react2native-demo2",
  "version": "1.0.0",
  "description": "no",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node node_modules/react-native/local-cli/cli.js start"
  },
  "author": "cjj",
  "license": "ISC",
  "dependencies": {
    "react": "^16.3.1",
    "react-native": "^0.55.1"
  }
}

1.2 .flowconfig文件

.flowconfig文件可以从facebook的github上复制,然后在工程的根目录创建.flowconfig文件,将其内容复制进去即可。

1.3 创建rn入口文件index.js

在根目录下创建index.js文件即可。

1.4 工程目录下的build.gradle文件修改

allprojects {
    repositories {
        jcenter()
        maven {
            // All of React Native (JS, Android binaries) is installed from npm
            url "$rootDir/node_modules/react-native/android"
        }
    }

    configurations.all {
        resolutionStrategy.force 'com.google.code.findbugs:jsr305:3.0.0'
    }
}

添加的内容:
maven {url "$rootDir/node_modules/react-native/android"}
configurations.all { resolutionStrategy.force 'com.google.code.findbugs:jsr305:3.0.0' }

1.5 app目录下的build.gradle文件修改

添加的内容:
compile "com.facebook.react:react-native:+" // From node_modules

defaultConfig {
        ...
        ndk {
            abiFilters "armeabi-v7a", "x86"
        }
    }

添加完后,Sync。

1.6 AndroidManifest.xml文件修改

添加权限:



添加Activity:

1.7 gradle.properties文件

添加:
android.useDeprecatedNdk=true

2.编辑ReactActivity业务界面

创建BaseReactActivity,各业务Activity继承BaseReactActivity,重写对象的方法,加载不同的jsbundle。

public abstract class BaseReactActivity extends AppCompatActivity
        implements DefaultHardwareBackBtnHandler,PermissionAwareActivity {

    private static final String TAG = "BaseReactActivity";
    private static final String JS_BUNDLE_LOCAL_FILE = "index.android.bundle";
    private ReactInstanceManager mReactInstanceManager;
    private ReactRootView mReactRootView;
    @Nullable
    private DoubleTapReloadRecognizer mDoubleTapReloadRecognizer;
    @Nullable
    private Callback mPermissionsCallback;
    @Nullable
    private PermissionListener mPermissionListener;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mReactRootView = new ReactRootView(this);
        initReactRootView();
        setContentView(mReactRootView);
    }

    protected void initReactRootView() {

        ReactInstanceManagerBuilder builder = ReactInstanceManager.builder()
                .setApplication(getApplication())
                .setJSMainModulePath(getJSMainModulePath())
                .addPackage(new MainReactPackage())
                .setUseDeveloperSupport(BuildConfig.DEBUG)
                .setInitialLifecycleState(LifecycleState.RESUMED);
        String jsBundleFile = getJSBundleFile();
        File file = null;
        if (!TextUtils.isEmpty(jsBundleFile)){
            file = new File(jsBundleFile);
        }
        if (file!=null && file.exists()){
            builder.setJSBundleFile(getJSBundleFile());
            Log.i(TAG, "load bundle from local cache");
        } else {
            String bundleAssetName = getBundleAssetName();
            builder.setBundleAssetName(TextUtils.isEmpty(bundleAssetName) ? JS_BUNDLE_LOCAL_FILE : bundleAssetName);
            Log.i(TAG, "load bundle from asset");
        }
        if (getPackages() != null){
            builder.addPackages(getPackages());
        }
        mReactInstanceManager = builder.build();
        mReactRootView.startReactApplication(mReactInstanceManager,getJsModuleName(),null);
        mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();

    }

    abstract protected String getJSMainModulePath();

    /**
     *读取bundle文件的路径,返回null时,从assets下读取
     *
     * @return
     */
    abstract protected String getJSBundleFile();

    /**
     * assets 中自带的 bundle名称
     *
     * @return
     */
    abstract protected String getBundleAssetName();

    /**
     * 自定义模块集
     * @return
     */
    abstract protected List getPackages();

    /**
     * 入口文件注册名
     * @return
     */
    abstract protected String getJsModuleName();

    @Override
    public void invokeDefaultOnBackPressed() {
        super.onBackPressed();
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (mReactInstanceManager!=null){
            mReactInstanceManager.onHostPause(this);
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (mReactInstanceManager!=null){
            mReactInstanceManager.onHostResume(this,this);
        }
        if (mPermissionsCallback != null) {
            mPermissionsCallback.invoke();
            mPermissionsCallback = null;
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mReactInstanceManager!=null){
            mReactInstanceManager.onHostDestroy(this);
        }
        ReactNativePreLoader.deatchView(getJsModuleName());
    }

    @Override
    public void onBackPressed() {
        if (mReactInstanceManager!=null){
            mReactInstanceManager.onBackPressed();
        } else {
            super.onBackPressed();
        }
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_MENU && mReactInstanceManager!=null){
            mReactInstanceManager.showDevOptionsDialog();
            return true;
        }
        return super.onKeyUp(keyCode, event);
    }


    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {

        if (mReactInstanceManager!=null) {
            mReactInstanceManager.onActivityResult(this,requestCode,resultCode,data);
        }else{
            super.onActivityResult(requestCode, resultCode, data);
        }
    }

    @TargetApi(Build.VERSION_CODES.M)
    public void requestPermissions(String[] permissions, int requestCode, PermissionListener listener){
        mPermissionListener = listener;
        requestPermissions(permissions,requestCode);
    }

    @Override
    public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) {
        mPermissionsCallback = new Callback() {
            @Override
            public void invoke(Object... args) {
                if (mPermissionListener != null && mPermissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults)) {
                    mPermissionListener = null;
                }
            }
        };
    }
}

3.打离线包

react-native bundle --entry-file index.js --platform android --dev false --bundle-output ./app/src/main/assets/index.android.bundle --assets-dest ./app/src/main/res/
相关指令可前往RN官网查看。
不同业务对应的--entry-file文件不一样,打包时填写正确的入口文件名,并且--bundle-output输出的文件名也需要根据业务区分。

4.图片不显示问题解决

如果jsbundle文件在assets路径下,图片加载显示正常,但是当我们加载sd卡上的jsbundle文件时,图片不显示。针对该问题,需要修改源码(react native 0.55.1):node_modules / react-native / Libraries / Image /AssetSourceResolver.js

defaultAsset(): ResolvedAssetSource {
    if (this.isLoadedFromServer()) {
      return this.assetServerURL();
    }

    if (Platform.OS === 'android') {
      return this.isLoadedFromFileSystem()
        ? this.drawableFolderInBundle()
        : this.resourceIdentifierWithoutScale();
    } else {
      return this.scaledAssetURLNearBundle();
    }
  }

defaultAsset方法中根据平台的不同分别执行不同的图片加载逻辑。重点我们来看android platform:
drawableFolderInBundle方法为在存在离线Bundle文件时,从Bundle文件所在目录加载图片。resourceIdentifierWithoutScale方法从Asset资源目录下加载。由此,我们需要修改isLoadedFromFileSystem方法中的逻辑。

修改isLoadedFromFileSystem方法

isLoadedFromFileSystem(): boolean {  
  var imgFolder = getAssetPathInDrawableFolder(this.asset);  
  var imgName = imgFolder.substr(imgFolder.indexOf("/") + 1);  
  var isPatchImg = patchImgNames.indexOf("|"+imgName+"|") > -1;  
  return !!(this.jsbundleUrl && this.jsbundleUrl.startsWith('file://')) && isPatchImg;  
}  

注:不同react native版本,源码变量存在不同问题,需注意。

你可能感兴趣的:(android项目集成多个RN界面)