【最新更新】關(guān)于協(xié)議胆筒, React 和 React Native 的開源license都已經(jīng)更換成了MIT license。
【2017-12-30 更新】:
最近把 react native 版本更新到0.51.0诈豌,react版本更新到16.0.0 之后仆救,再次嘗試,發(fā)現(xiàn)有了一些變化矫渔。
- 首先彤蔽,js的入口文件變?yōu)榱酥挥幸粋€index.js,而不再是之前的 index.android.js 和
index.ios.js庙洼。 - 然后顿痪,對應(yīng)上面一條,在創(chuàng)建
ReactInstanceManager
的實例時也有所變化:
mReactInstanceManager = ReactInstanceManager.builder()
.setApplication(MyApplication.instance)
.setBundleAssetName("index.android.bundle")
//.setJSMainModuleName("index.android") // 變?yōu)橄旅嬉恍? .setJSMainModulePath("index")
.addPackage(new MainReactPackage())
.setUseDeveloperSupport(Config.DEBUG)
.setInitialLifecycleState(LifecycleState.RESUMED)
.build();
- 另外油够,還發(fā)現(xiàn)蚁袭,如果原來的 android 工程名不叫 ‘a(chǎn)ndroid’(一般都不叫吧),在集成 RN 后無需一定改成 ‘a(chǎn)ndroid', 比如本文示例里的工程名叫 ’code', 集成 RN 后石咬,依然叫 code 也可以揩悄,只需在同級目錄創(chuàng)建 package.json 和 項目的js文件等等,即可鬼悠,經(jīng)測試可以跑起來删性。(這一條的好處是,你的 git 代碼改動記錄會沒有那么嚇人)
- 還有焕窝,使用了 Atom + nuclide蹬挺,并不好用,暫時還是不如 vscode 順手袜啃,慢慢熟悉吧汗侵,畢竟官方推薦的。
【原文】:
React Native 面世已經(jīng)挺長時間了群发,從去年開始接觸 RN晰韵,做了一款小 App,一次開發(fā)熟妓,支持 Android 和 iOS 兩個平臺雪猪,很方便。但是這其間起愈,尤其是剛開始只恨,也是經(jīng)歷了一個比較陡峭的學(xué)習(xí)曲線译仗,因為ES6、Flexbox layout 等等這些都是從頭學(xué)起官觅,一些工具也是為了開發(fā)這個項目才開始接觸纵菌,比如微軟開源的 VS Code 這個編輯器(因為有很多的 plugin,也可以說是 IDE了) —— 開發(fā) RN 好像還沒有像 Eclipse 休涤、Android Studio 或是 Xcode 那樣方便的 IDE咱圆,VS Code 算是很不錯的一個了(官網(wǎng)鏈接);
但是在這些基礎(chǔ)知識基本上手之后功氨,就還算比較順利了序苏,React Native 現(xiàn)在網(wǎng)上也有不少的開源項目和第三方庫,別人造好的輪子已經(jīng)基本可以滿足幾乎所有簡單的需求捷凄,各種文章論壇也不少忱详,遇到問題比較好找答案。如果是有基礎(chǔ)的前端同學(xué)來學(xué) RN 應(yīng)該是一個非常順暢的過程跺涤。
以上說的這個項目是從頭開始就是選擇了純 RN 開發(fā)匈睁,坑還算不多,慢慢地也都填上了钦铁。最近開始嘗試往一個已有的 Android 項目里集成 RN软舌,按照官網(wǎng)以及網(wǎng)上找到的一些文章,還是遇到了一些坑牛曹,自己總結(jié)一下佛点,也供大家參考。
一. 本文示例所依賴的環(huán)境:
- 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
二. 結(jié)合官網(wǎng)的教程文章對整個集成過程做一個大致的翻譯介紹,順便講一些遇到的坑:
- 如果對 React Native 沒有了解阅虫,建議先把 Getting Started 看一遍演闭,對 RN 有個基本認(rèn)識,安裝好環(huán)境等等颓帝。
1. 前置條件:
(1). 設(shè)置目錄結(jié)構(gòu):
由于 RN 支持 Android 和 iOS 雙平臺米碰,所以,為了方便购城,最好在 android 項目的根目錄之上一層創(chuàng)建一個新文件夾(比如叫 “code”)吕座,再把原來的項目的根目錄改名為 android,再整個移入這個新文件夾 “code”瘪板。
(2017-12-30更新: 無需改名吴趴,詳見文章頂部更新說明)
官網(wǎng)之所以這么建議,是因為當(dāng)你從頭創(chuàng)建一個 RN 項目時侮攀,目錄結(jié)構(gòu)就是這樣的锣枝。
下面是我的項目集成 RN 前后的目錄結(jié)構(gòu)變化:
集成前:
集成后:
(2). 安裝 JavaScript 依賴:
在這個新文件夾 “code” 下創(chuàng)建 package.json 文件:
{
"name": "MyReactNativeApp", // (2017-12-30 新增備注: 這個名字需要和后面提到的 ReactRootView.startReactApplication() 的第二個參數(shù)一致 )
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start"
}
}
然后打開終端執(zhí)行一下命令安裝 react 和 react-native 的 package:
npm install --save react react-native
這個命令會在我們的 "code" 目錄下創(chuàng)建一個 /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"
}
}
...
}
注意: 這里可能有個坑,不能無腦跟隨官網(wǎng)教程涵但。由于 一個 RN 工程支持兩個平臺杈绸,而 $rootDir 指的只是 android 項目的根目錄而并非整個 RN 工程的根目錄(也就是 node_modules 所在的目錄),因為如前文所說矮瘟,官網(wǎng)教程建議把目錄結(jié)構(gòu)做一番調(diào)整瞳脓,android 項目目錄在整個RN項目根目錄的下一層(見上面兩張圖)。所以其實如果按照官網(wǎng)建議的調(diào)整完目錄結(jié)構(gòu)后澈侠,這里的 Maven url 應(yīng)該是:
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 導(dǎo)致的問題,后來才發(fā)現(xiàn)是 Maven url 配置錯了導(dǎo)致需要依賴的 React Native 的版本不對所致哨啃∩斩埃可以在 Android Studio 里點開 External Libraries,查看 React Native 的版本是不是所需要依賴的版本拳球,如果不是审姓,多半是因為這個 maven url 配置的問題。
(2) 權(quán)限配置:
確保 app 的 AndroidManifest.xml
里申明了 Internet 權(quán)限:
<uses-permission android:name="android.permission.INTERNET" />
DevSettingsActivity
是 React Native 用于開發(fā)調(diào)試的一個界面祝峻,發(fā)布 Release 版本的時候不需要魔吐,可以在 Release 版本去掉,但調(diào)試時一定需要的莱找,還可以用來從開發(fā)服務(wù)器 Reload JS 代碼酬姆,把它加進 AndroidManifest.xml
即可:
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
3. 代碼集成:
(1) JS 部分:
在工程根目錄(package.json 所在目錄)下創(chuàng)建 index.android.js 文件。這個文件就是 JavaScript 代碼所在宋距,或者說是 JavaScript 代碼的入口文件轴踱。(如果需要還可以在同目錄創(chuàng)建一個 index.ios.js 文件)
這里用官網(wǎng)的簡單 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 文件里的對應(yīng)。
AppRegistry.registerComponent('MyRnModule', () => HelloWorld);
(1) Java 部分:
在 Android 代碼目錄里創(chuàng)建一個新的 Activity 用于承載 React Native 的運行谚赎。網(wǎng)上很多教程說這個 Activity 需要繼承 ReactActivity淫僻,可能是在集成較舊版本的 RN 時需要這樣诱篷,現(xiàn)在已經(jīng)不需要,只需要直接繼承 Activity 或者 AppCompatActivity 即可雳灵,但是要實現(xiàn)一個 DefaultHardwareBackBtnHandler
接口棕所。
為了在開發(fā)過程中彈出出錯浮層,如果 targetSdkVersion 在23或以上悯辙,需要在進入這個 Activity 時判斷是否有相應(yīng)權(quán)限琳省,可以用 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); 第一個參數(shù)一致躲撰。
mReactRootView.startReactApplication(mReactInstanceManager, "MyRnModule", null);
setContentView(mReactRootView);
// 判斷權(quán)限用于顯示設(shè)置界面浮層
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...
}
}
}
}
// 在模擬器中調(diào)試時针贬,Ctrl + M 打開設(shè)置界面
@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, "未允許彈窗權(quán)限,無法打開設(shè)置彈窗拢蛋!");
} 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
單例實例的工廠類。官網(wǎng)建議對 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));
至此,代碼部分已經(jīng)準(zhǔn)備妥當(dāng)了垃瞧,接下來要讓整個項目跑起來蔫劣。
4. Get it Running!
首先,要啟動開發(fā)服務(wù)器个从,只需在工程根目錄(package.json 所在目錄)運行命令:
npm start
脉幢,然后正常在Android Studio 里面 點擊 Run App 即可。
如果是真機調(diào)試信姓,要在連上手機后鸵隧,新啟一個命令行終端,執(zhí)行
adb reverse tcp:8081 tcp:8081
真機調(diào)試首次加載可能會報錯:
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.
這是因為還沒有在手機上設(shè)置 server 和 port意推,搖一搖啟動設(shè)置頁面豆瘫,點擊 DevSettings -> Debug server host & port for device -> 輸入 [本機ip]:8081,本機 ip 可用ifconfig
命令查看菊值。輸入完后返回外驱,再搖一搖然后點擊 Reload 即可。如果遇到這個錯:undefined is not an object (evaluating 'ReactInternals.ReactCurrentOwner')
出現(xiàn)這個錯是因為 react 版本不對腻窒,react-native 0.45.1依賴 react 16.0.0-alpha昵宇,可到 /node_modules 目錄下查看 react 版本是否正確,如果不對儿子,執(zhí)行npm install --save react@16.0.0-alpha.12
即可瓦哎。以上是針對開發(fā)調(diào)試,如果要發(fā)布 release 版,先執(zhí)行
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. 關(guān)于 minSdkVersion
由于 React Native 只支持 API Level 16 及以上割岛, 所以如果你的固有項目是支持更低的 API Level 的話,就需要考慮一下犯助,是不是針對不同系統(tǒng)版本做不同的方案癣漆,比如只在 API 16 及以上的設(shè)備上用 RN 方案,較舊的機型仍然用原生開發(fā)(但是這樣做引入 RN 的意義就大打折扣了)剂买;API 16 以下即 Android 4.0.x 及以下惠爽,這樣的舊機型現(xiàn)在幾乎已經(jīng)沒有了,我們的數(shù)據(jù)庫中這部分用戶只有不到 100 個瞬哼,而且大概率隨著時間會慢慢地減少婚肆,因此可以考慮分系統(tǒng)版本打包,讓這部分舊機型用戶可以使用APP坐慰,但不能使用 RN 部分新功能了旬痹〗龃叮總的來說需要綜合舊機型用戶量灸拍、活躍度峡捡、產(chǎn)品業(yè)務(wù)需求等綜合考慮了。
6. 寫在最后
從我個人用 React Native 開發(fā) APP 的體驗來看把跨,React Native 適合 C/S 結(jié)構(gòu)、業(yè)務(wù)型的 APP 或其中的模塊沼死,對于偏重底層技術(shù)的比如工具類 APP (或者模塊)着逐,我還沒有使用 RN 嘗試過,不過想必顯然是不太適合的意蛀∷时穑總的來說,一個對于底層技術(shù)依賴不多县钥,業(yè)務(wù)型秀姐,尤其是業(yè)務(wù)變動頻繁的應(yīng)用或模塊適合 RN 開發(fā),而且一次開發(fā)若贮,基本可以完全重用于兩個平臺省有,重要的是可以熱更新來應(yīng)對業(yè)務(wù)邏輯更新頻繁、更新要求快谴麦、迅速修復(fù)線上 bug 等需求場景蠢沿,目前看,RN 的熱更新并沒有被 Apple 封殺匾效。
建議符合上述描述的應(yīng)用類型的嘗試 React Native舷蟀,畢竟,從官網(wǎng) Showcase 列出的名單來看,已經(jīng)有不少重量級選手入坑了野宜。