首页 热点资讯 义务教育 高等教育 出国留学 考研考公
您的当前位置:首页正文

现有iOS项目中嵌入几个 React Native 页面

2024-12-15 来源:华佗小知识

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。
显示全文