[置顶] 【稀饭】react native 系列教程之已有项目接入React Native

标签: androidreactnative实战教程bundle拆分
3472人阅读 评论(2) 收藏 举报
分类:

概述

本文是基于目前公司的一个真实项目编写的,由于是边实践边记录,遇到什么问题和如何解决的,所以你看这篇文章的时候,可能有时候会觉得不是很流畅,特此说明。

引入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,并就遇到的问题,自己摸索着,记录着,也有对项目多模块多业务方案的一点思考。而每个人的现有项目各不相同,遇到的问题也不尽相同,但就像和我一样,一步一步踩着坑过来,你也会成功的,踩坑的过程就是你成长的步伐。

1
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:645875次
    • 积分:4792
    • 等级:
    • 排名:第6305名
    • 原创:106篇
    • 转载:22篇
    • 译文:5篇
    • 评论:345条
    关于我
    博客专栏
    最新评论