概述
本文是基于目前公司的一个真实项目编写的,由于是边实践边记录,遇到什么问题和如何解决的,所以你看这篇文章的时候,可能有时候会觉得不是很流畅,特此说明。
引入React Native
build.gradle配置
compile 'com.facebook.react:react-native:+'
react-native的res使用到了23sdk的资源,因此编译的sdk要求是23
compileSdkVersion 23
buildToolsVersion '23.0.3'
但这样如果你项目中使用到了HttpClient这个类的话,由于sdk 23版本已经将其移除掉,所以要多加配置
android {
useLibrary 'org.apache.http.legacy'
}
项目原来的gradle版本是1.2.3,但这句配置需要升级到最新版本2.0.0
dependencies {
classpath 'com.android.tools.build:gradle:2.2.0'
}
gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
react-native的minSdkVersion是16
android:minSdkVersion="16"
如果你在AndroidManifest.xml配置了该项,并且低于16,为了编译通过,需配置overrideLibrary
<uses-sdk
tools:overrideLibrary="com.facebook.react"
android:minSdkVersion="14"
android:targetSdkVersion="21" />
还需添加react native的DevSettingActivity
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
multiDex
然后试着编译运行,结果报错,原因是由于引进react-native,方法超出了64k限制,需要拆分dex。
再配置build.gradle
defaultConfig {
multiDexEnabled true
}
然后自己的Application继承MultiDexApplication,或者重写attachBaseContext方法
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
RN配置本地仓库
这下编译通过了,但是发现react-native版本是0.21,并不是最新版本的,所以这里我们要将项目目录修改为react-native项目目录。
创建了DX目录,将原来的项目android移到二级目录,然后剩下的几个文件和node_modules可以从react-native初始项目中拷贝过来(也可以执行npm init&npm install命令,但是太慢了),修改package.json里面的name为项目名称。
react-native项目中android项目的文件夹名称是为‘android’,刚好和我们原来的android项目一致,但是是否一定要取名为‘android’有待验证。
接着,修改android项目的根目录下的build.gradle
allprojects {
repositories {
mavenLocal()
jcenter()
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
//使用本地仓库,使react native 版本是最新的
url "$rootDir/../node_modules/react-native/android"
}
}
}
添加了本地仓库,url填写的是node_modules目录下的react-native
好了,重新编译一下,react-native版本是0.31的了(目前官网最新的版本是0.34,本地还没有更新)。
本地打开RN界面
实现ReactApplication接口
首先需要在自己的Application,比如本项目中的ElnApplication实现ReactApplication接口,重写getReactNativeHost方法,给RN提供一个默认的ReactNativeHost
public class ElnApplication extends BaseApplication implements ReactApplication{
//...省略其它代码
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
protected boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage()
);
}
@Override
protected String getJSMainModuleName() {
//定义js入口文件名称
return super.getJSMainModuleName();
}
@Nullable
@Override
protected String getBundleAssetName() {
//定义存放在项目asset文件夹下的bundle文件名称
return super.getBundleAssetName();
}
@Nullable
@Override
protected String getJSBundleFile() {
//自定义bundle文件路径
return super.getJSBundleFile();
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
}
创建Activity继承ReactActivity
新建TestRnActivity类,并继承ReactActivity
public class TestRnActivity extends ReactActivity {
@Override
protected String getMainComponentName() {
return "eln";//这个名称与js端AppRegistry.registerComponent要一致,可以注册多个入口,例如TestRnActivity2
}
}
编写js代码
接着打开项目的index.android.js,修改代码
import React, { Component } from 'react';
import {
AppRegistry,
Text,
View,
} from 'react-native';
class Eln extends Component {
render(){
return(
<View>
<Text>我是RN页面第一个入口</Text>
</View>
);
}
}
//eln字符串必修与TestRnActivity$getMainComponentName一致
AppRegistry.registerComponent('eln', () => Eln);
然后和普通RN项目运行一样,运行项目,就看到可以打开RN界面了。
本地给RN界面传递参数
那在打开RN界面时,有时候需要传递参数,那该如何呢?
打开TestRnActivity.java重写getLaunchOptions方法
@Override
protected Bundle getLaunchOptions() {//给js层传递数据,js层通过组件的props获取数据
Bundle bundle = new Bundle();
bundle.putString("des","我是从native传递过来的");
return bundle;
}
然后js代码调用
class Eln extends Component {
render(){
return(
<View>
<Text>我是RN页面第一个入口</Text>
<Text>{this.props.des}</Text>
</View>
);
}
}
这样就可以获取到des参数了。
打包
在我们开发完后,需要将应用进行打包,这里说明下RN和android项目混合开发的打包事项
混淆
按照官网的混淆配置还是报错
Caused by: java.lang.NoSuchFieldError: no field with name='mHybridData' signature='Lcom/facebook/jni/HybridData;' in class Lcom/facebook/react/cxxbridge/CatalystInstanceImpl;
at com.facebook.react.cxxbridge.ModuleRegistryHolder.initHybrid(Native Method)
at com.facebook.react.cxxbridge.ModuleRegistryHolder.<init>(Proguard:26)
at com.facebook.react.cxxbridge.i.a(Proguard:63)
at com.facebook.react.cxxbridge.CatalystInstanceImpl.<init>(Proguard:106)
at com.facebook.react.cxxbridge.CatalystInstanceImpl.<init>(Proguard:50)
at com.facebook.react.cxxbridge.c.a(Proguard:483)
at com.facebook.react.p.a(Proguard:868)
at com.facebook.react.p.a(Proguard:103)
at com.facebook.react.q.a(Proguard:203)
at com.facebook.react.q.doInBackground(Proguard:182)
at android.os.AsyncTask$2.call(AsyncTask.java:287)
at java.util.concurrent.FutureTask.run(FutureTask.java:234)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1080)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:573)
at java.lang.Thread.run(Thread.java:841)
混淆配置增加这句
-keep class com.facebook.** { *; }
配置bundle gradle打包命令
在build.gradle配置
apply from: "../../node_modules/react-native/react.gradle"
然后执行在项目下执行命令(dev环境)
gradlew assembleDevRelease
遇到各种编译问题。。。。
执行gradlew assembleDevRelease命令异常
Unsupported major.minor version 52.0
修改gradle.properties
android.useDeprecatedNdk=true
和gradle版本
classpath 'com.android.tools.build:gradle:2.1.0'
还是报错duplicate file。。。
开始排查定位错误因素。。。
- gradle 版本2.2.0,引入react.gradle,assembleRelease报错
- gradle 版本2.1.0-2.1.3,引入react.gradle,assembleRelease报错
- gradle 版本2.1.3,去掉react.gradle的引入,assembleRelease可以正常打包
在去掉脚本的引入因素之前,尝试修改buildTools和gradle版本号,还是报各种错。。。
无奈之下,先放弃使用脚本打包,转向手动打包。
使用bundle手动打包命令
react-native bundle
--platform android
--dev false
--entry-file index.android.js \
--bundle-output android/eln_base/assets/index.android.bundle \
--assets-dest android/eln_base/res/
目的是将bundle包生成放在android项目的assets文件夹下
然后项目去掉react.gradle脚本的引入,执行assembleDevRelease,成功打包,解压压缩包,在assets下可以看到多了两个bundle文件。
安装运行,也可以正常打开RN界面
多业务分模块
考虑到真实项目场景,可能不止一个RN入口,有多个业务模块需要使用到RN,但是它们的入口可能又不同,如一开始的图,比如在‘发现’大模块下,有两个小功能模块需要使用RN技术来实现,那么此时就需要各自打开各自的RN界面,那么这种需求如何实现呢?
单bundle
你可能想到了,那就是,一个新的入口,那么我就再建一个ReactActivity。没错的,那么我们创建下TestRnActivity2类。
同TestRnActivity一样,继承ReactActivity,但是getMainComponentName返回不同的名称,加以区别。
public class TestRnActivity2 extends ReactActivity {
@Override
protected String getMainComponentName() {
return "eln2";
}
}
接着,js端,打开index.android.js,编写eln2
class Eln extends Component {
render(){
return(
<View>
<Text>我是RN页面第一个入口</Text>
<Text>{this.props.des}</Text>
</View>
);
}
}
class Eln2 extends Component {
render(){
return(
<View>
<Text>我是RN页面第二个入口</Text>
</View>
);
}
}
AppRegistry.registerComponent('eln', () => Eln);
AppRegistry.registerComponent('eln2', () => Eln2);
可以看到我们registerComponent了两个组件,eln和eln2。
最后按上面的打包流程,在assets下生成bundle文件,再打包成apk,安装运行。
点击‘测试RN2’,进入第二个RN界面。
嗯,这样看起来好像初步实现了需求,但是在思考下,如果每次某个模块修改了,就需要更新整个bundle。是否可以这样:各自模块独立,更新也独立?
多bundle
使用多bundle的方案,首先需要让各自的模块加载自己的bundle文件。
修改TestRnActivity和TestRnActivity2,分别重写getReactNativeHost方法
TestRnActivity.java
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(ElnApplication.getInstance()) {
@Override
protected boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage()
);
}
@Nullable
@Override
protected String getBundleAssetName() {
//定义存放在项目asset文件夹下的bundle文件名称
return "eln1.android.bundle";
}
@Override
protected String getJSMainModuleName() {
//定义TestRnActivity2启动入口的js文件
return "eln1.android";
}
};
@Override
protected ReactNativeHost getReactNativeHost() {//重写ReactNativeHost
return mReactNativeHost;
}
TestRnActivity.java
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(ElnApplication.getInstance()) {
@Override
protected boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage()
);
}
@Nullable
@Override
protected String getBundleAssetName() {
//定义存放在项目asset文件夹下的bundle文件名称
return "eln2.android.bundle";
}
@Override
protected String getJSMainModuleName() {
//定义TestRnActivity2启动入口的js文件
return "eln2.android";
}
};
@Override
protected ReactNativeHost getReactNativeHost() {//重写ReactNativeHost
return mReactNativeHost;
}
两个模块的bundle文件分别取名为eln1.android.bundle和eln2.android.bundle,它们的js入口文件分别为eln1.android.js和eln2.android.js
接着,需要在js层编写这两个文件。在RN项目目录下创建eln1.android.js和eln2.android.js(和之前的index.android.js同级)
eln1.android.js
import React, { Component } from 'react';
import {
AppRegistry,
Text,
View,
} from 'react-native';
class Eln extends Component {
render(){
return(
<View>
<Text>我是RN页面第一个入口</Text>
<Text>{this.props.des}</Text>
</View>
);
}
}
AppRegistry.registerComponent('eln', () => Eln);
eln2.android.js
import React, { Component } from 'react';
import {
AppRegistry,
Text,
View,
} from 'react-native';
class Eln2 extends Component {
render(){
return(
<View>
<Text>我是RN页面第二个入口</Text>
</View>
);
}
}
AppRegistry.registerComponent('eln2', () => Eln2);
然后使用react-native bundle命令分别生成这两个bundle文件
react-native bundle --platform android --dev false --entry-file eln1.android.js \ --bundle-output android/eln_base/assets/eln1.android.bundle \ --assets-dest android/eln_base/res/
react-native bundle --platform android --dev false --entry-file eln2.android.js \ --bundle-output android/eln_base/assets/eln2.android.bundle \ --assets-dest android/eln_base/res/
最后,打包、安装、运行即可。
但是,你会发现发现eln1和eln2这两个模块并没多少代码,它们的bundle文件就达到来的500多k了,那后面岂不是更大。是的,这是因为react-native在生成bundle文件的时候,会把你import到的模块都打包进去。比如eln1和eln2都使用到了react和react-native模块,那它们的bundle都打包了这两个模块文件。所以,如何优化bundle文件也是个问题,这里给出了58和携程对bundle拆分的方案,满满的干货。
58是通过生成一个common bundle,然后和不同模块的bundle进行diff拆分,客户端再进行合并;而携程是直接修改react-native bundle脚本命令,过滤不需要的依赖模块。
总结
本文讲述了,在原有的android项目上集成RN,并就遇到的问题,自己摸索着,记录着,也有对项目多模块多业务方案的一点思考。而每个人的现有项目各不相同,遇到的问题也不尽相同,但就像和我一样,一步一步踩着坑过来,你也会成功的,踩坑的过程就是你成长的步伐。