現有iOS項目中嵌入幾個React Native頁面
2016.12.20 15:19 *字數1500閱讀1631評論5喜歡25
1.搭建環境
具體步驟參考官方文檔,環境弄好后,工程目錄如下
原的iOS項目被放在了根目錄的iOS的文件夾下(沒做安卓,所以沒有安卓的路徑)
React Native的iOS入口是index.ios.js
其他React Native的代碼放在了組件文件夾
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 ( 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開發中的標簽,this.refs ['nav']便能找到NavigatorIOS對象(彌補這個傳遞的麻煩)
initialRoute初始化路由,這里初始化起始頁為還款,然后點擊左右按鈕分別執行handleNavigationBackRequest(返回原始頁面),settlementAccountList(跳轉到返款賬號列表頁面)
passProps,傳遞順序,狀態,價格到償還頁面(此處這3個參數是從naive傳遞到index.ios.js,index.ios.js再傳遞給Yet還款)
本地入口
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 = rootViewself.navigationController?.isNavigationBarHidden = trueself.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 ({title}{(status === 0 || status === 3)?:});}......_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 ({canPost?{buttonString}:{buttonString}});}_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 ({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)}{(orderInfo.accountType === 2)?{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});})}:null}......{this._renderButton(() => {this._onButtonPress();})});}}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 ({this._renderRow('支付寶', this.state.alipay,() => {this.props.getPayType(1);this.props.navigator.popToTop()})}{this._renderRow('銀行卡', this.state.bankcard,() => {this.props.getPayType(2);this.props.navigator.popToTop()})});}
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 #import @implementation RNBridgeRCT_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() {debuggervar 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 start2.使用curl命令生成 main.jsbundlecurl http://localhost:8081/index.ios.bundle -o main.jsbundle
這樣進入RN頁面的時候,頂上就不再有提示信息了。如果提示找不到這個main.jsbundle文件,記得把main.jsbundle拖到iOS工程中引用一下。
打開main.jsbundle文件,你會發現里面包含了你所寫的所有JS文件內容。所以其實你寫的RN邏輯全在這里面。那么RN的熱更新就很好理解了,更新這個文件就好了。不管你自己實現還是選擇什么第三方熱更新方案,都是在各種花式更新這個main.jsbundle文件而已。
6.一些補充和問題(碎碎念)
之前只講了推跳轉頁面,模態喃?舉例一發
{alert("closed")}}>Hello World! {this.setModalVisible(!this.state.modalVisible)}}>Hide Modal {this.setModalVisible(true)}}>Show Modal
另外補充一個小問題,如果npm start命令不好使的時候,嘗試一下react-native start
還有一個遺留問題,返回原始頁面的時候我是調用的本地方法返回的,難道RN自己不能返回嗎,我其實是嘗試這樣的,也覺得這很理所當然。打印本地的navi.viewControllers,最后的的UIViewController就是RN頁面,NavigatorIOS的流行方法調用了竟然沒反應,難道是我姿勢不對?
(lldb) po navi.viewControllers<__NSArrayI 0x600000254160>(,,)
RN官方提供了NavigatorIOS和導航儀,使用方法大同小異,總感覺帶個iOS版更貼近原生。不過都說導航更涼爽。