概述
本文是基於目前公司的一個真實項目編寫的,由於是邊實踐邊記錄,遇到什麼問題和如何解決的,所以你看這篇文章的時候,可能有時候會覺得不是很流暢,特此說明。
引入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,並就遇到的問題,自己摸索着,記錄着,也有對項目多模塊多業務方案的一點思考。而每個人的現有項目各不相同,遇到的問題也不盡相同,但就像和我一樣,一步一步踩着坑過來,你也會成功的,踩坑的過程就是你成長的步伐。