【稀飯】react native 系列教程之已有項目接入React Native

概述

本文是基於目前公司的一個真實項目編寫的,由於是邊實踐邊記錄,遇到什麼問題和如何解決的,所以你看這篇文章的時候,可能有時候會覺得不是很流暢,特此說明。

引入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界面

image

實現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參數了。

image

打包

在我們開發完後,需要將應用進行打包,這裏說明下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文件。

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界面。

image

嗯,這樣看起來好像初步實現了需求,但是在思考下,如果每次某個模塊修改了,就需要更新整個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/

image

image

最後,打包、安裝、運行即可。

但是,你會發現發現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,並就遇到的問題,自己摸索着,記錄着,也有對項目多模塊多業務方案的一點思考。而每個人的現有項目各不相同,遇到的問題也不盡相同,但就像和我一樣,一步一步踩着坑過來,你也會成功的,踩坑的過程就是你成長的步伐。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章