公司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ì)很大。
原理圖:

要實(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)題,需注意。