1.搭建環境
具體步驟參考官方文檔,環境弄好后,工程目錄如下
- 原iOS項目被放在了根目錄的iOS文件夾下(沒做安卓,所以沒有安卓的路徑)
- React Native 的iOS入口是 index.ios.js
- 其他 React Native 的代碼放在了 component文件夾
- main.jsbundle 為我們所寫 React Native 代碼的集合,發布時才生成(或方便真機調試)
2.入口
RN入口index.ios.js
'use strict'; //使用嚴格模式
import React, { Component } from 'react';
import {
AppRegistry,//用于注冊組件
StyleSheet,//使用樣式
Text,
View,
Image,
NavigatorIOS,//導航控制器
TouchableHighlight,//點擊效果
NativeModules//調用native方法
} from 'react-native';
import Repayment from './component/repayment';
import SettlementAccountList from './component/SettlementAccountList';
export default class MECRM extends Component {
_handleNavigationBackRequest() {
var RNBridge = NativeModules.RNBridge;
RNBridge.back();
}
_settlementAccountList() {
var status = this.props["status"];
if (status === 0 || status === 3) {
this.refs['nav'].push({
title: '返款人信息表',
component: SettlementAccountList,
barTintColor: '#7B9DFD',
tintColor: 'white',
passProps: {
}
})
}
}
render() {
return (
<NavigatorIOS
ref='nav'
initialRoute={{
component: Repayment,//注冊的組件名一定要大寫
title: '返款申請',
rightButtonIcon: require('image!contacts'),
leftButtonTitle: '返回',
onLeftButtonPress: () => this._handleNavigationBackRequest(),
onRightButtonPress: () => this._settlementAccountList(),
passProps: {
orderid: this.props["orderid"],
status: this.props["status"],
price: this.props["price"]
},
barTintColor: '#7B9DFD'
}}
style={{flex: 1}}
itemWrapperStyle={styles.itemWrapper}
tintColor="white"
titleTextColor ='white'
/>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
});
AppRegistry.registerComponent('RNBackApply', () => MECRM);
index.ios.js作為RN代碼入口,關鍵點是 NavigatorIOS 標簽:
- ref='nav',把 NavigatorIOS 對象標記為‘nav’方便調用,類似iOS開發中的tag,this.refs['nav']便能找到 NavigatorIOS 對象。(彌補this傳遞的麻煩)
- initialRoute 初始化路由,這里初始化起始頁為Repayment,然后點擊左右按鈕分別執行handleNavigationBackRequest(返回native頁面)、settlementAccountList(跳轉到返款賬號列表頁面)
-
passProps,傳遞 orderid、status、price到Repayment頁面(此處這3個參數是從naive傳遞到index.ios.js,index.ios.js再傳遞給了Repayment)
native入口
let jsCodeLocation = URL(string: "http://localhost:8081/index.ios.bundle?platform=ios")
let mockData:NSDictionary = ["orderid": self.orderId,
"status" : self.orderDetailModel.status,
"price" : self.orderDetailModel.cost]
let rootView = RCTRootView(bundleURL: jsCodeLocation,
moduleName: "RNBackApply",
initialProperties: mockData as [NSObject : AnyObject],
launchOptions: nil)
let vc = UIViewController()
vc.view = rootView
self.navigationController?.isNavigationBarHidden = true
self.navigationController?.pushViewController(vc, animated: true)
- jsCodeLocation RN執行文件路徑,這里的路徑為開發時使用,發布時需更換為main.jsbundle的路徑
- mockData為從native傳遞到RN的數據
- moduleName: "RNBackApply"與index.ios.js中registerComponent('RNBackApply', () => MECRM)對應
3.構建頁面
前面提到起始頁為Repayment,那么Repayment是怎么實現如上圖的喃?以下為簡要實現
'use strict';
import React, { Component } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableHighlight,//整塊區域有按壓效果
AlertIOS,
TouchableOpacity,//文字有按壓效果
TextInput,
Image,
NativeModules,
DeviceEventEmitter//通知
} from 'react-native';
import PayTypeChoice from './PayTypeChoice';//注意路徑是以當前文件為準
export default class repayment extends Component {
constructor(props) {
super(props);
var defaultMoney = (this.props["price"]*0.2<0.01?0.01:this.props["price"]);
this.state = {events: {
info: {
id: '',
orderId: this.props["orderid"].toString(),
account: '',
accountType: 1,
accountName: '',
bankName: '',
branchName: '',
money: defaultMoney.toString(),
status: '',
remark: '',
failReason: '',
}
}};
this._applySettlementRequest();
this._accountInfoChoiced();
}
_accountInfoChoiced() {
this.subscription = DeviceEventEmitter.addListener('accountInfoChoiced',(accountInfo) => {
var newEvents = this.state.events;
newEvents.info.account = accountInfo.account;
newEvents.info.accountName = accountInfo.accountName;
newEvents.info.accountType = accountInfo.accountType;
newEvents.info.bankName = accountInfo.bankName;
newEvents.info.branchName = accountInfo.branchName;
this.setState({events: newEvents});
})
}
_renderRow(title: string, subTitle: string, placeholder: string, onChangeText: Function, maxLength: int) {
var status = this.props["status"];
return (
<View>
<View style={styles.row}>
<Text style={styles.rowText}>
{title}
</Text>
{(status === 0 || status === 3)?
<TextInput
style={styles.rowInputText}
autoCapitalize={'none'}
maxLength = {maxLength}
onChangeText={onChangeText}
value={subTitle}
placeholder={placeholder}
selectionColor='#0064FF'
clearButtonMode={'while-editing'}
returnKeyType={'done'}
/>
:
<TextInput
style={styles.rowInputText}
autoCapitalize={'none'}
onChangeText={onChangeText}
value={subTitle}
placeholder={placeholder}
selectionColor='#0064FF'
clearButtonMode={'while-editing'}
returnKeyType={'done'}
editable={false}
/>
}
</View>
<View style={styles.separator} />
</View>
);
}
......
_renderButton(onPress: Function) {
var status = this.props["status"];
var buttonString = '申請返款';
switch (status) {
case 0:
var buttonString = '申請返款';
break;
case 1:
var buttonString = '返款處理中';
break;
case 2:
var buttonString = '已返款';
break;
case 3:
var buttonString = '已拒絕,重新申請';
break;
case 4:
var buttonString = '待審核';
break;
}
var canPost = false;
var orderInfo = this.state.events.info;
if ((status === 0 || status === 3) && orderInfo.accountName.length > 0 && orderInfo.account.length > 0 && orderInfo.money.length > 0 ) {
if (orderInfo.accountType === 2) {
if (orderInfo.bankName.length > 0 && orderInfo.branchName.length > 0) {
canPost = true;
}
} else {
canPost = true;
}
}
return (
<View style={styles.container}>
<View>
{canPost?
<TouchableOpacity style={styles.button} onPress={onPress}>
<Text style={styles.buttonText}>{buttonString}</Text>
</TouchableOpacity>
:
<View style={styles.disableButton}>
<Text style={styles.buttonText}>{buttonString}</Text>
</View>
}
</View>
</View>
);
}
_onButtonPress() {
var orderInfo = this.state.events.info;
orderInfo.money = Number(orderInfo.money*100);
if(isNaN(orderInfo.money)){
AlertIOS.alert(
'請輸入正確的返款金額',
)
return;
}
var orderPrice = (this.props["price"]*100);
if (orderInfo.money > orderPrice*0.8) {
AlertIOS.alert(
'',
'當前返款大于支付金額的80%,是否繼續?',
[
{text: '返回修改'},
{text: '繼續發起', onPress: () => {
var RNBridge = NativeModules.RNBridge;
RNBridge.setSettlement(orderInfo,(error, events) => {
if (error) {
// console.error(error);
} else {
this._handleNavigationBackRequest();
}
})
}}
]
)
return;
}
var RNBridge = NativeModules.RNBridge;
RNBridge.setSettlement(orderInfo,(error, events) => {
if (error) {
// console.error(error);
} else {
this._handleNavigationBackRequest();
}
})
};
_applySettlementRequest() {
var status = this.props["status"];
// status參數說明
// 0 未申請
// 1 返款中
// 2 返款成功
// 3 返款失敗
if (status === 0) {
return
}
var RNBridge = NativeModules.RNBridge;
var orderid = this.props["orderid"].toString();
RNBridge.applySettlement(orderid,(error, events) => {
if (error) {
console.error(error);
} else {
events.info.money = (events.info.money/100).toString();
this.setState({events: events});
}
})
}
render() {
var orderInfo = this.state.events.info;
var status = this.props["status"];
return (
<ScrollView style={styles.list}>
<View style={styles.line}/>
<View style={styles.group}>
<View>
{this._renderPayTypeRow(() => {
this.props.navigator.push({
title: '返款方式',
component: PayTypeChoice,
barTintColor: '#7B9DFD',
tintColor: 'white',
passProps: {
accountType: this.state.events.info.accountType,
getPayType:(accountType)=>{
var newEvents = this.state.events;
newEvents.info.accountType = accountType;
this.setState({events: newEvents});
}
}
})
})}
{this._renderRow('姓名', orderInfo.accountName, '請輸入姓名', (accountName) => {
var newEvents = this.state.events;
newEvents.info.accountName = accountName;
this.setState({events: newEvents});
},10)}
<View>
{(orderInfo.accountType === 2)?
<View>
{this._renderRow('開戶銀行', orderInfo.bankName, '請輸入開戶銀行', (bankName) => {
var newEvents = this.state.events;
newEvents.info.bankName = bankName;
this.setState({events: newEvents});
})}
{this._renderRow('開戶支行', orderInfo.branchName, '請輸入開戶支行', (branchName) => {
var newEvents = this.state.events;
newEvents.info.branchName = branchName;
this.setState({events: newEvents});
})}
</View>
:
null
}
</View>
......
</View>
</View>
<View style={styles.line}/>
{this._renderButton(() => {
this._onButtonPress();
})}
</ScrollView>
);
}
}
const styles = StyleSheet.create({
......
});
- constructor 初始化數據,這里的數據結構和網絡請求結果保持一致。
- renderRow 函數以及被省略掉的其他renderXXXRow函數只是讓總的render函數沒那么臃腫,返回一些JSX片段,在構建界面中根據不同條件展示不同樣式是常見需求,但是JSX中不支持 if.else,只支持三目運算符?:,在上面代碼中多次用到。
-
需求功能1:點擊返款方式,跳轉到返款方式選擇頁面,然后把選擇的方式回傳。
這里頁面傳值采用的方式是將修改返款方式后的操作作為一個函數傳遞給下一個頁面,實現如下。
Repayment.js
{this._renderPayTypeRow(() => {
this.props.navigator.push({
title: '返款方式',
component: PayTypeChoice,
barTintColor: '#7B9DFD',
tintColor: 'white',
passProps: {
accountType: this.state.events.info.accountType,
getPayType:(accountType) => {
var newEvents = this.state.events;
newEvents.info.accountType = accountType;
this.setState({events: newEvents});
}
}
})
})}
PayTypeChoice.js
render() {
return (
<ScrollView style={styles.list}>
<View style={styles.line}/>
<View style={styles.group}>
<View>
{this._renderRow('支付寶', this.state.alipay,() => {
this.props.getPayType(1);
this.props.navigator.popToTop()
})}
<View style={styles.separator} />
{this._renderRow('銀行卡', this.state.bankcard,() => {
this.props.getPayType(2);
this.props.navigator.popToTop()
})}
</View>
</View>
<View style={styles.line}/>
</ScrollView>
);
}
Repayment.js中的getPayType就是傳遞到下一個頁面,當返款方式選擇后以執行的函數。在PayTypeChoice.js中當cell點擊的時候將返款方式作為參數傳入,例如this.props.getPayType(1),就將返款方式設置為了支付寶。
-
需求功能2:點擊右上角圖標,跳轉到返款賬號列表頁,然后把選擇的賬號信息帶回來填充頁面。
如之前所述,點擊圖標跳轉的邏輯是寫在index.ios.js文件中的
_settlementAccountList() {
var status = this.props["status"];
if (status === 0 || status === 3) {
this.refs['nav'].push({
title: '返款人信息表',
component: SettlementAccountList,
barTintColor: '#7B9DFD',
tintColor: 'white',
passProps: {
}
})
}
}
想通過剛才傳遞函數的方式達到頁面傳值,那么index.ios.js就要先獲取到Repayment用于回調的函數,然后再傳遞給SettlementAccountList。很麻煩,并且我嘗試了一下沒成功。這種時候,通知就顯得非常簡單粗暴了,運用React Native中的通知組件DeviceEventEmitter,頁面傳值都不是事兒。
當賬號信息被選擇時在SettlementAccountList中發送通知
_onPressCell(rowData: string) {
this.props.navigator.popToTop()
DeviceEventEmitter.emit('accountInfoChoiced', rowData);
}
在Repayment中接收通知
_accountInfoChoiced() {
this.subscription = DeviceEventEmitter.addListener('accountInfoChoiced',(accountInfo) => {
var newEvents = this.state.events;
newEvents.info.account = accountInfo.account;
newEvents.info.accountName = accountInfo.accountName;
newEvents.info.accountType = accountInfo.accountType;
newEvents.info.bankName = accountInfo.bankName;
newEvents.info.branchName = accountInfo.branchName;
this.setState({events: newEvents});
})
}
- 需求功能3:在進入頁面時拉取之前填寫的返款信息,點擊左上的返回按鈕回到Native頁面,以及返款賬號信息頁面拉取已有的信息。這3點都是調用的Native方法。雖然RN也有網絡請求方法,但是APP中的網絡請求會有公共參數、公共的鑒權方法、錯誤處理等,所以網絡請求還是選擇走Native的好。
創建待RN調用的Native方法的步驟,在官方文檔中也講得很清楚,這里貼出我寫的代碼片段(因為Objective-C寫著更方便就沒用Swift,偷懶了一下)
RNBridge.m
#import "RNBridge.h"
#import <UIKit/UIKit.h>
#import <MECRM-Swift.h>
@implementation RNBridge
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(back)
{
dispatch_async(dispatch_get_main_queue(), ^{
UITabBarController *tabvc = (UITabBarController *)[self getCurrentVC];
UINavigationController *navi = [tabvc selectedViewController];
navi.navigationBarHidden = NO;
[navi popViewControllerAnimated:YES];
});
}
//獲取返款信息
RCT_EXPORT_METHOD(applySettlement:(NSString *)orderID callback:(RCTResponseSenderBlock)callback)
{
dispatch_async(dispatch_get_main_queue(), ^{
UITabBarController *tabvc = (UITabBarController *)[self getCurrentVC];
UINavigationController *navi = [tabvc selectedViewController];
UIViewController * vc = navi.viewControllers.lastObject;
[vc startAnimating];
NSString *path = [NSString stringWithFormat:@"/order/%@/applySettlement",orderID];
[NetworkTool GET:path parameters:nil successHandler:^(id _Nonnull result) {
[vc stopAnimating];
callback(@[[NSNull null], result]);
} failureHandler:^(NSError * _Nullable error) {
[vc stopAnimating];
callback(@[error.localizedDescription, [NSNull null]]);
}];
});
}
//設置返款信息
RCT_EXPORT_METHOD(setSettlement:(NSDictionary *)orderInfo callback:(RCTResponseSenderBlock)callback)
{
dispatch_async(dispatch_get_main_queue(), ^{
UITabBarController *tabvc = (UITabBarController *)[self getCurrentVC];
UINavigationController *navi = [tabvc selectedViewController];
UIViewController * vc = navi.viewControllers.lastObject;
[vc startAnimating];
NSString *orderID = orderInfo[@"orderId"];
NSString *path = [NSString stringWithFormat:@"/order/%@/applySettlement",orderID];
[NetworkTool POST:path parameters:orderInfo successHandler:^(id _Nonnull result) {
[vc stopAnimating];
callback(@[[NSNull null], result]);
} failureHandler:^(NSError * _Nullable error) {
[vc stopAnimating];
callback(@[error.localizedDescription, [NSNull null]]);
}];
});
}
//返款人信息表
RCT_EXPORT_METHOD(getSettlementAccount:(NSInteger)start callback:(RCTResponseSenderBlock)callback)
{
dispatch_async(dispatch_get_main_queue(), ^{
UITabBarController *tabvc = (UITabBarController *)[self getCurrentVC];
UINavigationController *navi = [tabvc selectedViewController];
UIViewController * vc = navi.viewControllers.lastObject;
[vc startAnimating];
NSString *path = [NSString stringWithFormat:@"/order/getSettlementAccount?start=%ld&limit=%d",(long)start,100];
[NetworkTool GET:path parameters:nil successHandler:^(id _Nonnull result) {
[vc stopAnimating];
callback(@[[NSNull null], result]);
} failureHandler:^(NSError * _Nullable error) {
[vc stopAnimating];
callback(@[error.localizedDescription, [NSNull null]]);
}];
});
}
在RN中調用返回方法
_handleNavigationBackRequest() {
var RNBridge = NativeModules.RNBridge;
RNBridge.back();
}
在RN中獲取已填寫的返款信息
_applySettlementRequest() {
var status = this.props["status"];
// status參數說明
// 0 未申請
// 1 返款中
// 2 返款成功
// 3 返款失敗
if (status === 0) {
return
}
var RNBridge = NativeModules.RNBridge;
var orderid = this.props["orderid"].toString();
RNBridge.applySettlement(orderid,(error, events) => {
if (error) {
console.error(error);
} else {
events.info.money = (events.info.money/100).toString();
this.setState({events: events});
}
})
}
4.調試
在模擬器中 command+D 調出RN的菜單,點擊Debug JS Remotely。在需要調試的代碼前面加debugger,例如
_onButtonPress() {
debugger
var orderInfo = this.state.events.info;
orderInfo.money = Number(orderInfo.money*100);
if(isNaN(orderInfo.money)){
AlertIOS.alert(
'請輸入正確的返款金額',
)
return;
}
......
};
簡陋,夠用?(°?‵?′??)
5.上線以及熱更新
上線的時候需要將代碼中的jsCodeLocation修改一下
//let jsCodeLocation = URL(string: "http://localhost:8081/index.ios.bundle?platform=ios")
let jsCodeLocation = Bundle.main.url(forResource: "main", withExtension: "jsbundle")
這個main.jsbundle是需要手動生成的,生成方法如下:
1.在React Native項目根目錄下運行 npm start
2.使用curl命令生成 main.jsbundle
curl http://localhost:8081/index.ios.bundle -o main.jsbundle
這樣進入RN頁面的時候,頂上就不再有提示信息了。如果提示找不到這個main.jsbundle文件,記得把main.jsbundle拖到iOS工程中引用一下。
打開main.jsbundle文件,你會發現里面包含了你所寫的所有js文件內容。所以其實你寫的RN邏輯全在這里面。那么RN的熱更新就很好理解了,更新這個文件就好了。不管你自己實現還是選擇什么第三方熱更新方案,都是在各種花式更新這個main.jsbundle文件而已。
6.一些補充和問題(碎碎念)
之前只講了push跳轉頁面,modal喃?舉例一發
<View style={{marginTop: 22}}>
<Modal
animationType={"slide"}
transparent={false}
visible={this.state.modalVisible}
onRequestClose={() => {alert("closed")}}
>
<View style={{marginTop: 22}}>
<View>
<Text>Hello World!</Text>
<TouchableHighlight onPress={() => {
this.setModalVisible(!this.state.modalVisible)
}}>
<Text>Hide Modal</Text>
</TouchableHighlight>
</View>
</View>
</Modal>
<TouchableHighlight onPress={() => {this.setModalVisible(true)}}>
<Text>Show Modal</Text>
</TouchableHighlight>
</View>
另外補充一個小問題,如果npm start命令不好使的時候,嘗試一下react-native start
還有一個遺留問題,返回native頁面的時候我是調用的native方法返回的,難道RN自己不能返回嗎,我其實是嘗試這樣的,也覺得這很理所當然。打印native的navi.viewControllers,最后的UIViewController就是RN頁面,NavigatorIOS的pop方法調用了竟然沒反應,難道是我姿勢不對?
(lldb) po navi.viewControllers
<__NSArrayI 0x600000254160>(
<MECRM.MainOrderViewController: 0x7f82a1226ac0>,
<MECRM.OrderDetailViewController: 0x7f829f54e2c0>,
<UIViewController: 0x7f82a1004ed0>
)
RN官方提供了NavigatorIOS和Navigator,使用方法大同小異,總感覺帶個iOS更貼近原生。不過都說Navigator更cool。