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