【最新更新】关于协议, 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 两个平台,很方便。但是这其间,尤其是刚开始,也是经历了一个比较陡峭的学习曲线,因为ES6、Flexbox 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 前后的目录结构变化:
集成前:
集成后:
(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 配置的问题。
(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 列出的名单来看,已经有不少重量级选手入坑了。