Android项目集成React Native实践总结

【最新更新】关于协议, React 和 React Native 的开源license都已经更换成了MIT license。

【2017-12-30 更新】:

最近把 react native 版本更新到0.51.0,react版本更新到16.0.0 之后,再次尝试,发现有了一些变化。

  • 首先,js的入口文件变为了只有一个index.js,而不再是之前的 index.android.js 和
    index.ios.js。
  • 然后,对应上面一条,在创建 ReactInstanceManager 的实例时也有所变化:
        mReactInstanceManager = ReactInstanceManager.builder()
                .setApplication(MyApplication.instance)
                .setBundleAssetName("index.android.bundle")
                //.setJSMainModuleName("index.android") // 变为下面一行
                .setJSMainModulePath("index")
                .addPackage(new MainReactPackage())
                .setUseDeveloperSupport(Config.DEBUG)
                .setInitialLifecycleState(LifecycleState.RESUMED)
                .build();
  • 另外,还发现,如果原来的 android 工程名不叫 ‘android’(一般都不叫吧),在集成 RN 后无需一定改成 ‘android', 比如本文示例里的工程名叫 ’code', 集成 RN 后,依然叫 code 也可以,只需在同级目录创建 package.json 和 项目的js文件等等,即可,经测试可以跑起来。(这一条的好处是,你的 git 代码改动记录会没有那么吓人)
  • 还有,使用了 Atom + nuclide,并不好用,暂时还是不如 vscode 顺手,慢慢熟悉吧,毕竟官方推荐的。

【原文】:

React Native 面世已经挺长时间了,从去年开始接触 RN,做了一款小 App,一次开发,支持 Android 和 iOS 两个平台,很方便。但是这其间,尤其是刚开始,也是经历了一个比较陡峭的学习曲线,因为ES6Flexbox layout 等等这些都是从头学起,一些工具也是为了开发这个项目才开始接触,比如微软开源的 VS Code 这个编辑器(因为有很多的 plugin,也可以说是 IDE了) —— 开发 RN 好像还没有像 Eclipse 、Android Studio 或是 Xcode 那样方便的 IDE,VS Code 算是很不错的一个了(官网链接);

但是在这些基础知识基本上手之后,就还算比较顺利了,React Native 现在网上也有不少的开源项目和第三方库,别人造好的轮子已经基本可以满足几乎所有简单的需求,各种文章论坛也不少,遇到问题比较好找答案。如果是有基础的前端同学来学 RN 应该是一个非常顺畅的过程。

以上说的这个项目是从头开始就是选择了纯 RN 开发,坑还算不多,慢慢地也都填上了。最近开始尝试往一个已有的 Android 项目里集成 RN,按照官网以及网上找到的一些文章,还是遇到了一些坑,自己总结一下,也供大家参考。

一. 本文示例所依赖的环境:

  • minSdkVersion:14;为支持RN改成了16
  • compileSdkVersion: 25
  • buildToolsVersion: "25.0.3"
  • targetSdkVersion: 25
  • React Native:0.45.1 (2017-12-30更新: 已升级至0.51.0, react 版本16.0.0)
  • Mac

二. 结合官网的教程文章对整个集成过程做一个大致的翻译介绍,顺便讲一些遇到的坑:

  • 如果对 React Native 没有了解,建议先把 Getting Started 看一遍,对 RN 有个基本认识,安装好环境等等。

1. 前置条件:

(1). 设置目录结构:

由于 RN 支持 Android 和 iOS 双平台,所以,为了方便,最好在 android 项目的根目录之上一层创建一个新文件夹(比如叫 “code”),再把原来的项目的根目录改名为 android,再整个移入这个新文件夹 “code”。
(2017-12-30更新: 无需改名,详见文章顶部更新说明)

官网之所以这么建议,是因为当你从头创建一个 RN 项目时,目录结构就是这样的。

下面是我的项目集成 RN 前后的目录结构变化:

集成前:

before.png

集成后:

after.png
(2). 安装 JavaScript 依赖:

在这个新文件夹 “code” 下创建 package.json 文件:

{
    "name": "MyReactNativeApp", // (2017-12-30 新增备注: 这个名字需要和后面提到的 ReactRootView.startReactApplication() 的第二个参数一致 )
    "version": "0.0.1",
    "private": true,
    "scripts": {
        "start": "node node_modules/react-native/local-cli/cli.js start"
    }
}

然后打开终端执行一下命令安装 react 和 react-native 的 package:

npm install --save react react-native

这个命令会在我们的 "code" 目录下创建一个 /node_modules 文件夹,里面是所有需要的 JavaScript 依赖,可以打开查看一下,非常多。

2. 集成 React Native 的配置:

(1). 配置依赖:

在 app module 的 build.gradle 文件里 (在本文的例子里,即 code/android/app/build.gradle ) 加入 react-native 的依赖:

dependencies {
    ...
    compile "com.facebook.react:react-native:+" // From node_modules.
}

注:像别的依赖一样,+号表示依赖最新版,也可以指定明确的版本号。

然后,在android根目录的 build.gradle 文件里 (在本文的例子里,即 code/android/build.gradle ) 添加 React Native 的 Maven url 配置:

allprojects {
    repositories {
        ...
        maven {
            // 这里是指定所依赖的 React Native 是来自从 npm 安装来的 /node_modules 目录,
            // 因为 Maven 中央仓库里的 React Native 可能不是最新的。
            url "$rootDir/node_modules/react-native/android"
        }
    }
    ...
}

注意: 这里可能有个坑,不能无脑跟随官网教程。由于 一个 RN 工程支持两个平台,而 $rootDir 指的只是 android 项目的根目录而并非整个 RN 工程的根目录(也就是 node_modules 所在的目录),因为如前文所说,官网教程建议把目录结构做一番调整,android 项目目录在整个RN项目根目录的下一层(见上面两张图)。所以其实如果按照官网建议的调整完目录结构后,这里的 Maven url 应该是:

url "$rootDir/../node_modules/react-native/android"

而不是

url "$rootDir/node_modules/react-native/android"

因为这个 maven url 配置错误,有可能遇到Crash:

Caused by: java.lang.IllegalAccessError: Method 'void android.support.v4.net.ConnectivityManagerCompat.<init>()' is inaccessible to class 'com.facebook.react.modules.netinfo.NetInfoModule' (declaration of 'com.facebook.react.modules.netinfo.NetInfoModule' appears in /data/app/[your-package-name]/base.apk:classes41.dex)

开始还怀疑是 Multidex 导致的问题,后来才发现是 Maven url 配置错了导致需要依赖的 React Native 的版本不对所致。可以在 Android Studio 里点开 External Libraries,查看 React Native 的版本是不是所需要依赖的版本,如果不是,多半是因为这个 maven url 配置的问题。


External Libraries.png
(2) 权限配置:

确保 app 的 AndroidManifest.xml 里申明了 Internet 权限:

<uses-permission android:name="android.permission.INTERNET" />

DevSettingsActivity是 React Native 用于开发调试的一个界面,发布 Release 版本的时候不需要,可以在 Release 版本去掉,但调试时一定需要的,还可以用来从开发服务器 Reload JS 代码,把它加进 AndroidManifest.xml 即可:

<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />

3. 代码集成:

(1) JS 部分:

在工程根目录(package.json 所在目录)下创建 index.android.js 文件。这个文件就是 JavaScript 代码所在,或者说是 JavaScript 代码的入口文件。(如果需要还可以在同目录创建一个 index.ios.js 文件)
这里用官网的简单 Hello World 示例:

'use strict';

import React from 'react';
import {
  AppRegistry,
  StyleSheet,
  Text,
  View
} from 'react-native';

class HelloWorld extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.hello}>Hello, World</Text>
      </View>
    )
  }
}
var styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
  },
  hello: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
});

// 注意:这个 "MyRnModule" 名字要和后面要讲到的 Java 文件里的对应。
AppRegistry.registerComponent('MyRnModule', () => HelloWorld);
(1) Java 部分:

在 Android 代码目录里创建一个新的 Activity 用于承载 React Native 的运行。网上很多教程说这个 Activity 需要继承 ReactActivity,可能是在集成较旧版本的 RN 时需要这样,现在已经不需要,只需要直接继承 Activity 或者 AppCompatActivity 即可,但是要实现一个 DefaultHardwareBackBtnHandler 接口。

为了在开发过程中弹出出错浮层,如果 targetSdkVersion 在23或以上,需要在进入这个 Activity 时判断是否有相应权限,可以用 Settings.canDrawOverlays(context) 来判断。

完整代码:

package com.my-pkg-name.rn;

import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.support.v7.app.AppCompatActivity;
import android.view.KeyEvent;

import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactRootView;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.my-pkg-name.base.Config;
import com.my-pkg-name.ToastUtil;

public class MyReactActivity extends AppCompatActivity implements DefaultHardwareBackBtnHandler {

    private static final int OVERLAY_PERMISSION_REQ_CODE = 100;

    private ReactRootView mReactRootView;
    private ReactInstanceManager mReactInstanceManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mReactRootView = new ReactRootView(this);
        mReactInstanceManager = ReactInstanceManagerProvider.getReactInstanceManager();

        // 这里的 "MyRnModule" 名字要与前面 index.android.js 里 AppRegistry.registerComponent('MyRnModule', () => HelloWorld); 第一个参数一致。
        mReactRootView.startReactApplication(mReactInstanceManager, "MyRnModule", null);

        setContentView(mReactRootView);

        // 判断权限用于显示设置界面浮层
        if (Config.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (!Settings.canDrawOverlays(this)) {
                Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                        Uri.parse("package:" + getPackageName()));
                startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE);
            }
        }
    }

    @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);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        if (mReactInstanceManager != null) {
            mReactInstanceManager.onHostDestroy();
        }
    }

    @Override
    public void onBackPressed() {
        if (mReactInstanceManager != null) {
            mReactInstanceManager.onBackPressed();
        } else {
            super.onBackPressed();
        }
    }

    @Override
    public void invokeDefaultOnBackPressed() {
        super.onBackPressed();
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == OVERLAY_PERMISSION_REQ_CODE) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                if (!Settings.canDrawOverlays(this)) {
                    // SYSTEM_ALERT_WINDOW permission not granted...
                }
            }
        }
    }

    // 在模拟器中调试时,Ctrl + M 打开设置界面
    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        if (Config.DEBUG) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) {
                ToastUtil.showLong(this, "未允许弹窗权限,无法打开设置弹窗!");
            } else if (keyCode == KeyEvent.KEYCODE_MENU && mReactInstanceManager != null) {
                mReactInstanceManager.showDevOptionsDialog();
                return true;
            }
        }
        return super.onKeyUp(keyCode, event);
    }
}

在 Manifest 里注册新 Activity,注意要用 Theme.AppCompat.Light.NoActionBar 这个主题:

<activity
  android:name=".MyReactActivity"
  android:label="@string/app_name"
  android:theme="@style/Theme.AppCompat.Light.NoActionBar">
</activity>

ReactInstanceManagerProvider 是一个 提供 ReactInstanceManager 单例实例的工厂类。官网建议对 ReactInstanceManager 使用单例实例。

ReactInstanceManagerProvider.java:

package com.my-pkg-name.rn;

//import com.facebook.react.LifecycleState;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.common.LifecycleState;
import com.facebook.react.shell.MainReactPackage;
import com.my-pkg-name.MyApplication;
import com.my-pkg-name.base.Config;

public class ReactInstanceManagerProvider {

    private ReactInstanceManager mReactInstanceManager;

    private ReactInstanceManagerProvider() {
        mReactInstanceManager = ReactInstanceManager.builder()
                .setApplication(MyApplication.instance)
                .setBundleAssetName("index.android.bundle")
                .setJSMainModuleName("index.android")
                .addPackage(new MainReactPackage())
                .setUseDeveloperSupport(Config.DEBUG)
                .setInitialLifecycleState(LifecycleState.RESUMED)
                .build();
    }

    private static ReactInstanceManagerProvider getInstance() {
        return Holder.sInstance;
    }

    private static class Holder {
        private static ReactInstanceManagerProvider sInstance = new ReactInstanceManagerProvider();
    }

    private ReactInstanceManager getReactInstanceManagerInstance() {
        return mReactInstanceManager;
    }

    public static ReactInstanceManager getReactInstanceManager() {
        return getInstance().getReactInstanceManagerInstance();
    }
}

最后,需要在合适的地方启动这个新的 Activity:

startActivity(new Intent(getContext(), MyReactActivity.class));

至此,代码部分已经准备妥当了,接下来要让整个项目跑起来。

4. Get it Running!

首先,要启动开发服务器,只需在工程根目录(package.json 所在目录)运行命令:
npm start,然后正常在Android Studio 里面 点击 Run App 即可。

  • 如果是真机调试,要在连上手机后,新启一个命令行终端,执行 adb reverse tcp:8081 tcp:8081

  • 真机调试首次加载可能会报错:
    java.lang.RuntimeException: Unable to load script from assets 'index.android.bundle'. Make sure your bundle is packaged correctly or you're running a packager server.
    这是因为还没有在手机上设置 server 和 port,摇一摇启动设置页面,点击 DevSettings -> Debug server host & port for device -> 输入 [本机ip]:8081,本机 ip 可用 ifconfig 命令查看。输入完后返回,再摇一摇然后点击 Reload 即可。

  • 如果遇到这个错:undefined is not an object (evaluating 'ReactInternals.ReactCurrentOwner')
    出现这个错是因为 react 版本不对,react-native 0.45.1依赖 react 16.0.0-alpha,可到 /node_modules 目录下查看 react 版本是否正确,如果不对,执行 npm install --save react@16.0.0-alpha.12 即可。

  • 以上是针对开发调试,如果要发布 release 版,先执行
    react-native bundle --platform android --dev false --entry-file index.android.js --bundle-output android/com/your-company-name/app-package-name/src/main/assets/index.android.bundle --assets-dest android/com/your-company-name/app-package-name/src/main/res/
    再照常
    ./gradlew assembleRelease
    即可。

5. 关于 minSdkVersion

由于 React Native 只支持 API Level 16 及以上, 所以如果你的固有项目是支持更低的 API Level 的话,就需要考虑一下,是不是针对不同系统版本做不同的方案,比如只在 API 16 及以上的设备上用 RN 方案,较旧的机型仍然用原生开发(但是这样做引入 RN 的意义就大打折扣了);API 16 以下即 Android 4.0.x 及以下,这样的旧机型现在几乎已经没有了,我们的数据库中这部分用户只有不到 100 个,而且大概率随着时间会慢慢地减少,因此可以考虑分系统版本打包,让这部分旧机型用户可以使用APP,但不能使用 RN 部分新功能了。总的来说需要综合旧机型用户量、活跃度、产品业务需求等综合考虑了。

6. 写在最后

从我个人用 React Native 开发 APP 的体验来看,React Native 适合 C/S 结构、业务型的 APP 或其中的模块,对于偏重底层技术的比如工具类 APP (或者模块),我还没有使用 RN 尝试过,不过想必显然是不太适合的。总的来说,一个对于底层技术依赖不多,业务型,尤其是业务变动频繁的应用或模块适合 RN 开发,而且一次开发,基本可以完全重用于两个平台,重要的是可以热更新来应对业务逻辑更新频繁、更新要求快、迅速修复线上 bug 等需求场景,目前看,RN 的热更新并没有被 Apple 封杀。

建议符合上述描述的应用类型的尝试 React Native,毕竟,从官网 Showcase 列出的名单来看,已经有不少重量级选手入坑了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容