美文网首页react-native开发Android知识Android开发
Android项目集成React Native实践总结

Android项目集成React Native实践总结

作者: 塘泥 | 来源:发表于2017-07-08 18:56 被阅读700次

    【最新更新】关于协议, 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 列出的名单来看,已经有不少重量级选手入坑了。

    相关文章

      网友评论

        本文标题:Android项目集成React Native实践总结

        本文链接:https://www.haomeiwen.com/subject/unqrhxtx.html