android項(xiàng)目集成多個(gè)RN界面

公司app數(shù)量較多,為了避免手機(jī)桌面上都是app的啟動(dòng)圖標(biāo),不方便使用。因此業(yè)務(wù)提出需求:安裝一個(gè)app,進(jìn)入app后,界面上顯示不同圖標(biāo)(對(duì)應(yīng)不同業(yè)務(wù)),點(diǎn)擊不同圖標(biāo),啟動(dòng)對(duì)應(yīng)的業(yè)務(wù)界面。
我司開(kāi)發(fā)平臺(tái)使用的是React Native,如果按照常規(guī)做法,創(chuàng)建一個(gè)RN項(xiàng)目,所有業(yè)務(wù)都寫(xiě)在該項(xiàng)目中,則打包后的apk將越來(lái)越大,代碼維護(hù)管理成本也大。
為了解決apk大小問(wèn)題,確認(rèn)了一個(gè)方案:原生項(xiàng)目集成多個(gè)RN界面,每個(gè)RN界面對(duì)應(yīng)不同的業(yè)務(wù),并且每個(gè)RN界面的bundle文件相互獨(dú)立,用戶(hù)可按需下載,app不會(huì)很大。
原理圖:


app集成原理圖.png

要實(shí)現(xiàn)android集成多個(gè)RN界面,需要做如下工作:

1.android工程集成react native
2.編輯ReactActivity業(yè)務(wù)界面
3.打離線(xiàn)包
4.圖片不顯示問(wèn)題解決

1.android工程集成react native

1.1 創(chuàng)建package.json文件

在工程根目錄路徑下,執(zhí)行npm init命令,并填寫(xiě)相關(guān)信息。成功后,生成package.json文件。

  • 在文件中添加"start": "node node_modules/react-native/local-cli/cli.js start"
  • 執(zhí)行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上復(fù)制,然后在工程的根目錄創(chuàng)建.flowconfig文件,將其內(nèi)容復(fù)制進(jìn)去即可。

1.3 創(chuàng)建rn入口文件index.js

在根目錄下創(chuàng)建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'
    }
}

添加的內(nèi)容:
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文件修改

添加的內(nèi)容:
compile "com.facebook.react:react-native:+" // From node_modules

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

添加完后,Sync。

1.6 AndroidManifest.xml文件修改

添加權(quán)限:

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>

添加Activity:
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />

1.7 gradle.properties文件

添加:
android.useDeprecatedNdk=true

2.編輯ReactActivity業(yè)務(wù)界面

創(chuàng)建BaseReactActivity,各業(yè)務(wù)Activity繼承BaseReactActivity,重寫(xiě)對(duì)象的方法,加載不同的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時(shí),從assets下讀取
     *
     * @return
     */
    abstract protected String getJSBundleFile();

    /**
     * assets 中自帶的 bundle名稱(chēng)
     *
     * @return
     */
    abstract protected String getBundleAssetName();

    /**
     * 自定義模塊集
     * @return
     */
    abstract protected List<ReactPackage> getPackages();

    /**
     * 入口文件注冊(cè)名
     * @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.打離線(xiàn)包

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/
相關(guān)指令可前往RN官網(wǎng)查看。
不同業(yè)務(wù)對(duì)應(yīng)的--entry-file文件不一樣,打包時(shí)填寫(xiě)正確的入口文件名,并且--bundle-output輸出的文件名也需要根據(jù)業(yè)務(wù)區(qū)分。

4.圖片不顯示問(wèn)題解決

如果jsbundle文件在assets路徑下,圖片加載顯示正常,但是當(dāng)我們加載sd卡上的jsbundle文件時(shí),圖片不顯示。針對(duì)該問(wèn)題,需要修改源碼(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方法中根據(jù)平臺(tái)的不同分別執(zhí)行不同的圖片加載邏輯。重點(diǎn)我們來(lái)看android platform:
drawableFolderInBundle方法為在存在離線(xiàn)Bundle文件時(shí),從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版本,源碼變量存在不同問(wèn)題,需注意。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容