環境搭建
本次開發環境
node: v8.9.4
npm: 5.6.0
react: 16.0.0
react-native: 0.51.0
代碼編輯器:webstorm
模擬器:ios => siMulator or 真機 / android => Android Studio自帶模擬器 or 真機
- 安裝Chocolatey,並使用Chocolatey安裝python2和node.js
cmd下輸入如下命令安裝Chocolatey(或需翻牆)
@powershell -NoProfile -ExecutionPolicy Bypass -Command "iex ((new-object net.webclient).DownloadString('https://chocolatey.org/install.ps1'))" && SET PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin
安裝python2
choco install python2
安裝node.js
choco install nodejs.install
設置npm鏡像
npm config set registry https://registry.npm.taobao.org --global
npm config set disturl https://npm.taobao.org/dist --global
- Yarn、React Native的命令行工具
安裝react-native腳手架
npm install -g yarn react-native-cli
配置yarn鏡像
yarn config set registry https://registry.npm.taobao.org --global
yarn config set disturl https://npm.taobao.org/dist --global
- 安裝Android SDK 並 配置ANDROID_HOME
- 創建並初始化項目,運行
react-native init projectDemo
cd projectDemo
react-native run-ios // ios啓動
react-native run-android // android啓動
最終生成項目目錄
android: 安卓開發源碼
ios: ios開發源碼
node_modules: 項目依賴
.babelrc: babel的配置文件,將各種js版本的語言轉換成運行環境所能識別的
App.js: react-native入口模板文件,供index.js使用,非固定
index.js: 項目入口文件
package.json 依賴配置文件
根據項目需求小做修改
可以看到我們爲前端專門創建了一個項目文件src,並將 react-native入口模板文件遷移到項目文件夾裏。根據項目需求分包
assets: 資源目錄
imgs: 圖片資源
styles: 樣式資源
|—common.js: 公用樣式資源
|—color.js: 定義項目顏色
|—layout.js: 定於項目佈局
|—size.js: 定義項目尺寸
|—index.js: 整合以上資源
|—entry.js 對應頁面路由樣式資源
utils: 工具資源
|—api.js: 集成了整個項目的請求
|—http.js: 封裝了fetch請求
|—screen.js: 有關屏幕的各種屬性資源
|—system.js: 有關係統的各種資源
|—utils.js: 其他工具封裝
components: 全局組件封裝
pages: 頁面
router: 路由
store: 全局狀態管理
App.js: 入口模板文件
PS:編譯錯誤一般會提示在node.js中,運行錯誤手機或模擬器彈出紅色錯誤頁面。修改js代碼無需重裝,點擊紅色錯誤頁面的【RELOAD】即可或搖動手機彈出開發菜單【RELOAD】
組件 控件
RN中每個頁面都是由【Component 】組成,也可用【Component 】來做組合控件
基本組件代碼
//引入React Component
import React, {Component} from 'react'
//引入控件
import {
View,
Text,
} from 'react-native'
//變量的定義只能放Component不可放Component裏面,否則調用報錯。函數可以放Component裏面
let val = 1;
//export default 纔可以把這個組件暴露出去,否則其他文件調用會報錯(提示找不到Demo組件或者找到的Demo組件不是Component)
export default class Demo extends Component {
//構造函數,props作爲父子控件直接傳遞數據,後面講
constructor(props) {
super(props)
}
//渲染 最終的UI效果在此
render() {
return (
<View>
<Text style={{marginTop: 20, fontSize: 16}}>react-native通用基礎模板</Text>
<View>
{/*<Text>{listStore.num}</Text>*/}
</View>
</View>
)
}
}
常用控件
Text Button Image TextInput ScrollView ListView ,具體控件和屬性、方法參考 React Native 中文網文檔
佈局 與 導航
佈局 在控件的style中直接設置,共有5種屬性,可搭配使用(正常是Flex Direction確認主軸後搭配其他)
<View style={{flex: 1, flexDirection: 'row'}}>
Flex Direction:確定主軸即確定佈局方向,可以有從左往右【row】、從上往下【column】、從右往左【row-reverse】、從下往上【column-reverse】
Justify Content:沿着主軸的排列方式,可以有:flex-start、center、flex-end、space-around以及space-between
Align Items:決定其子元素沿着次軸(與主軸垂直的軸,比如若主軸方向爲row,則次軸方向爲column)的排列方式,可以有flex-start、center、flex-end以及stretch
Flex wrap:根據子元素內容大小自適應佈局。可以有nowrap、warp、wrap-reverse
Align Content:根據子元素內容大小自適應佈局。可以有stretch、flexstart、center、flex-end、space-between、space-around
導航 :路由導航器【StackNavigator】 Tab選項卡【TabNavigator】側滑抽屜【DrawerNavigator】
【StackNavigator】 組件採用堆棧式的頁面導航來實現各個界面跳轉。它的構造函數:
StackNavigator(RouteConfigs, StackNavigatorConfig)
RouteConfigs 參數表示各個頁面路由配置
StackNavigatorConfig 參數表示導航器的配置
const RouteConfigs = {
Home: {
screen: HomePage,
navigationOptions: ({navigation}) => ({
title: '首頁',
}),
},
Find: {
screen: FindPage,
navigationOptions: ({navigation}) => ({
title: '發現',
}),
},
Mine: {
screen: MinePage,
navigationOptions: ({navigation}) => ({
title: '我的',
}),
},
};
const StackNavigatorConfig = {
initialRouteName: 'Home',
initialRouteParams: {initPara: '初始頁面參數'},
navigationOptions: {
title: '標題',
headerTitleStyle: {fontSize: 18, color: '#666666'},
headerStyle: {height: 48, backgroundColor: '#fff'},
},
mode: 'card',
headerMode: 'screen',
cardStyle: {backgroundColor: "#ffffff"},
transitionConfig: (() => ({
screenInterpolator: CardStackStyleInterpolator.forHorizontal,
})),
onTransitionStart: (() => {
console.log('頁面跳轉動畫開始');
}),
onTransitionEnd: (() => {
console.log('頁面跳轉動畫結束');
}),
};
navigation:導航器對象,在導航器中的每一個頁面,都有 navigation 屬性,該屬性有以下幾個屬性/方法:
navigate - 跳轉到其他頁面
state - 當前頁面導航器的狀態
setParams - 更改路由的參數
goBack - 返回
dispatch - 發送一個action
跳轉示例
this.props.navigation.navigate('MinePage', { key: 'val' })
【TabNavigator】
構造函數
TabNavigator(RouteConfigs, TabNavigatorConfig)
api和 StackNavigator 類似
參數 RouteConfigs 是路由配置,
參數 TabNavigatorConfig是Tab選項卡配置。
【DrawerNavigator】
基本配置和上面兩者api相似
屬性(Props) 與 狀態(State)
props 組件屬性,只可以用來保存組件間數據傳遞
eg:父組件傳遞參數給子組件
// Father.js
export default class Father extends Components {
render() {
return (
<View>
<Son val="18" />
</View>
)
}
}
// Son.js
export default class Son extends Component {
render() {
return (
<View>
<Text>{this.props.val}</Text>
</View>
)
}
}
eg:子組件傳遞參數給父組件
// Father.js
export default class Father extends Components {
render() {
return (
<View>
<Son age="18" todo={this.show} />
</View>
)
}
show(sonV) {
}
}
// Son.js
export default class Son extends Components {
render() {
return (
<View>
<Text
onPress={() => {this.props.todo('子組件傳遞值')}}
>{this.props.age}</Text>
</View>
)
}
}
state 組件狀態,組件是更新渲染依賴於其state屬性,所以不是跟渲染相關的屬性不要設置到這裏,加大RN的渲染負擔(state有所變動都會導致RN進行是否渲染的判斷)。
import React, { Component } from 'react';
import { AppRegistry, Text, View,} from 'react-native';
export default class ResultScreen extends Component {
constructor(props){
super(props);
//設置state屬性
this.state = { showData: false };
//設置變量
this.dataText = "";
}
render() {
return (
<View>
<Text tyle={{backgroundColor:'white',textAlign:'center',textAlignVertical:'center'}} onPress={this._onPressButton.bind(this)}>
{this.state.showData ? this.dataText : "點擊改變狀態"}
</Text>
</View>
);
_onPressButton() {
this.dataText = "Success";
this.setState({
showData: true,
});
}
}
手勢識別
https://reactnative.cn/docs/0.51/gesture-responder-system.html#content
定時器
setTimeout, clearTimeout
setInterval, clearInterval
setImmediate, clearImmediate
requestAnimationFrame, cancelAnimationFrame
requestAnimationFrame(fn)和setTimeout(fn, 0)不同,前者會在每幀刷新之後執行一次,而後者則會儘可能快的執行(在iPhone5S上有可能每秒1000次以上)。
setImmediate則會在當前JavaScript執行塊結束的時候執行,就在將要發送批量響應數據到原生之前。注意如果你在setImmediate的回調函數中又執行了setImmediate,它會緊接着立刻執行,而不會在調用之前等待原生代碼。
Promise的實現就使用了setImmediate來執行異步調用。
網絡訪問
async getMoviesFromApi() {
try {
// 注意這裏的await語句,其所在的函數必須有async關鍵字聲明
let response = await fetch('https://facebook.github.io/react-native/movies.json');
let responseJson = await response.json();
return responseJson.movies;
} catch(error) {
console.error(error);
}
}
調試
- Reload: 重載rn項目
- Debug JS Remotely : 打開瀏覽器debug模式,會在瀏覽器打開一個新的標籤頁面,
地址是http://localhost:8081/debugger-ui - Live Reload : 實時刷新 當你的js代碼發生變化後,React Native會自動生成bundle然後傳輸到模擬器或手機上
- Hot Reloading: 熱加載 當你每次保存代碼時Hot Reloading功能便會生成此次修改代碼的增量包,然後傳輸到手機或模擬器上以實現熱加載。相比 Enable Live Reload需要每次都返回到啓動頁面,Enable Live Reload則會在保持你的程序狀態的情況下,就可以將最新的代碼部署到設備上
- Toggle Inspector 調試網絡、佈局、點擊等
Show Perf Monitor 打開性能監控
【debug調試】
在工程目錄下輸入npm start啓動服務
- 手機與電腦同一個wifi環境下,真機搖晃彈出上述菜單,選擇【Dev Settings】–【Debug server host & port for device】填寫代理的ip和端口(eg:172.22.185.210:8081)。
- Chrome瀏覽器打開 http://localhost:8081/debugger-ui/ 然後手機調試菜單【Reload】下,會自動連接到Chrome上(沒有UI)
- 連上後畫面如下
原生與React-Native交互
【例子爲已有老項目,接入少量RN模塊,仍以原生爲主流】
按上面教程搭建好環境,
初始化一個新項目
react-native init ReactTest
cd ReactTest
react-native run-android
用AS打開android目錄,裏面默認生成的是一個MainActivity和MainApplication,且MainActivity繼承自ReactActivity,即app一啓動打開MainActivity,MainActivity又去通過【原生調用RN界面】方式去啓動我們的RN項目。(老項目遷移建議按照環境搭建步驟創建一個新項目,然後再對android目錄下代碼進行修改,把老項目copy過來比較簡單)
然後把自己的老項目遷移過來,啓動頁的Activity也換成自己的,MainActivity可以刪除,MainApplication裏的代碼是需要的可以跟自己原有的Application合併下。
【自動更新 和 手動更新 index.android.bundle 】
React-Native的內容依賴於【index.android.bundle】文件,必須保證【index.android.bundle】文件是最新的才能看到最新修改的效果,在項目目錄下啓動服務(即npm start)並配置到代理,每次run項目就會自動更新;或者打離線【index.android.bundle】包也可不依賴RN服務(一般打包的時候用,否則RN內容打開會白屏)。
【自動更新】
用Node.js命令行進入項目工程輸入
npm start
啓動服務(如果服務連接不上可直接運行react-native run-android重新跑下)
搖晃手機調出RN的調試界面,選擇【Dev Settings】-> 【Debug server host & port for device】輸入電腦的ip,端口號是8081,格式如【172.22.185.190:8081】。
【手動更新 即 離線包】
如果無法自動更新,可以產用手動更新的方法
在android下創建assets【android/app/src/main/assets】目錄。然後打開Node.js命令行工具,進入根目錄,運行如下代碼
react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res/
這會在assets下生成兩個文件
PS:運行的app可能會報如下【index.android.bundle文件找不到錯誤】也是這個原因,啓動服務,配置代理即可或打離線包均可解決。
【原生調RN界面】
修改MainActivity變成我們的項目,這樣app啓動起來就不調用RN而是我們的MainActivity了
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.one).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(MainActivity.this,OneReactActivity.class));
}
});
findViewById(R.id.two).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(MainActivity.this, TwoReactActivity.class));
}
});
}
}
這個原生界面有兩個Button,點擊後分別跳轉OneReactActivity和TwoReactActivity兩個模塊,OneReactActivity和TwoReactActivity是跳轉RN界面的入口Activity,繼承自ReactActivity。
OneReactActivity
public class OneReactActivity extends ReactActivity {
@Nullable
@Override
protected String getMainComponentName() {
return "One";
}
}
Two也類似,getMainComponentName返”Two”即可
MainApplication保持原來的不動。
這樣原生部分的代碼編寫完畢,現在寫兩個RN的界面並註冊
One和Two的代碼類似,One的代碼如下
import React from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View
} from 'react-native';
export default class One extends React.Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.hello}>One</Text>
</View>
)
}
}
var styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
},
hello: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
});
然後在index.js中註冊下
import { AppRegistry } from 'react-native';
import One from './One';
import Two from './Two'
AppRegistry.registerComponent('Two', () => Two);
AppRegistry.registerComponent('One', () => One);
ok編譯運行。
【RN調用原生/原生界面】
這部分的流程就是原生將自己的方法註冊到RN中,RN對方法進行調用,跳轉界面也是RN調用原生方法,再在原生方法中進行跳轉。其實原生跳RN也一樣,只不過這部分封裝在ReactActivity中所以感知不深。
編寫ReactContextBaseJavaModule子類,寫一個MyJavaModule吧
public class MyJavaModule extends ReactContextBaseJavaModule {
public MyJavaModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return MyJavaModule.class.getSimpleName();
}
@ReactMethod
public void JumpResultActivity(){
Activity activity = getCurrentActivity();
activity.startActivity(new Intent(activity, ResultActivity.class));
}
}
注意 【ReactMethod】這個註解
編寫ReactPackage子類,編寫一個MyReactPackage吧
public class MyReactPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new MyJavaModule(reactContext));
return modules;
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
最後再MainApplication中註冊下
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),new MyReactPackage()
);
}
寫一個ResultActivity來提供跳轉,其實你做其他的事情也可以。原生部分結束。
RN部分修改下One.js
import React from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View,
NativeModules
} from 'react-native';
let JavaModule = NativeModules.MyJavaModule;
export default class One extends React.Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.hello} onPress={this.buttonClick}>One</Text>
</View>
)
}
buttonClick(){
JavaModule.JumpResultActivity();
}
}
var styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
},
hello: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
});
很簡單,主要是NativeModules部分。
【原生與RN通信】
MyJavaModule添加如下代碼
public static void sendEvent(String eventName, WritableMap params){
if(context != null){
context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, paramss);
}
}
context是ReactApplicationContext對象,取自MyJavaModule的構造函數
原生部分這樣就可以了,我們在需要的地方調用sendEvent即可,params參數可以爲空
RN部分,我們在接收的地方加入以下代碼即可
componentDidMount(){
DeviceEventEmitter.addListener('result', function() {
alert("send success");
});
}