React-Native混编学习

2018-05-02  本文已影响0人  2林子易2

本篇主要涉及的是App和RN的混合开发环境搭建,对于基本的RN环境搭建请自行查阅文档

这里需要着重注意的是全局依赖:

最好的方式是使用react-native init生成一个新的RN项目,参考它的package.json依赖。这里我使用的是"react": "16.2.0" "react-native": "0.53.3"

APP配合

android环境搭建

基础部分

RNApp嵌入原生

1. 依赖引入

引入node_modules/react-native/android

后需改为我们的android包管理

2. MyApplication类继承ReactApplication

后需重新定义Bundle文件地址,使之能够热更。

3. RNActivity编写

public class MyReactActivity extends ReactActivity {

  public Bundle getBundle() { // 获取props入参
      return getIntent().getExtras();
  }

  protected @Nullable String getMainComponentName() { // 定义RN组件名称
      return "MyReactNativeApp";
  }

  @Override
  protected ReactActivityDelegate createReactActivityDelegate() { // 通过getLaunchOptions传值
      return new ReactActivityDelegate(this, getMainComponentName()) { 
          @Nullable
          @Override
          protected Bundle getLaunchOptions() {
              return getBundle();
          }
      };
  }

}

至此,通过startActivity就能够正常打开一个RNApp了。

原生跳转RN路由

这里跳转可以有很多方式,不过最根本的是如何通过原生将要跳转的路由传递给RN。
主要分为两大类方式:(见本篇原生和RN通信)

这里我采用的是主动传递中的Props传递。

在原生开启RNApp的时候,可以传递一个Bundle作为最初的Props,路由及部分参数信息通过这个Bundle带给RN。

Android部分

RN部分
RN这里采用了react-native-router-flux作为路由管理插件。这里需要注意的是版本问题,测试发现"react-native-router-flux": "^4.0.0-beta.25""react-navigation": "^1.0.0-beta.22"可用。

相关代码如下:

import React from 'react';
import { Router, Scene, Actions } from 'react-native-router-flux';
import { getUserInfo, finishActivity } from './communication'

import PageOne from './modules/PageOne'
import PageTwo from './modules/PageTwo'
import PageThree from './modules/PageThree'
import PageFour from './modules/PageFour'

export default class App extends React.Component {
  constructor(props) {
    super(props);
    console.log("RN启动");
  }

  componentDidMount(){
    const rnKey = this.props.rnKey || "PageOne";
    Actions.reset(rnKey, this.props);
  }

  // 导航栏回退方法
  onBack () {
    let popRouter = Actions.pop();
    !popRouter && finishActivity();
  }

  render() {
    return (
      <Router>
        <Scene key="root" hideNavBar={true}>
          <Scene key="PageOne" back={true} hideNavBar={false} component={PageOne} title="PageOne" onBack={() => this.onBack()}/>
          <Scene key="PageTwo" back={true} hideNavBar={false} component={PageTwo} title="PageTwo" onBack={() => this.onBack()}/>

          {/* 用户信息获取,用户已登陆 */}
          <Scene key="PageThree" back={true} hideNavBar={false} component={PageThree} title="PageThree" onBack={() => this.onBack()}/>
          <Scene key="PageFour" back={true} hideNavBar={false} component={PageFour} title="PageFour" onBack={() => this.onBack()}/>
        </Scene>
      </Router>
    )
  }
}

原生和RN的通信

上面说道通信分为两种,主动传递和被动传递。这里的主动/被动是以原生为参照。具体的可以参考和原生端通信,这里只贴关键部分的代码。如下:

Android部分

最后在MyApplication中注入,查看上面嵌入RNApp部分的【载入公共包,见原生和RN通信】注释。

RN部分
RN部分正常引用调用就好,代码如下:

import { NativeModules } from 'react-native';

const { commModule } = NativeModules;

export function startActivity (appPath, params = {}) {
  if(appPath) {
    commModule.startActivityFromJS(appPath, params);
  }
}

export function finishActivity () {
  commModule.activityFinish();
}

export function getUserInfo() {
  return commModule.getUserInfo()
}

export function getEnv() {}

// test
export function rnCallNativeFromCallback(msg, callback) {
  commModule.rnCallNativeFromCallback(msg, callback);
}

export function rnCallNativeFromPromise(msg) {
  return commModule.rnCallNativeFromPromise(msg);
}

export function getConstans() {
  console.log(commModule.test)
}

RN资源管理

测试发现如果将打包的Bundle文件和资源文件放到统一目录下,就可以正常引用资源。
参考命令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/assets

有需要做热更的可以将资源放到SD卡中,只需要指定以下Bundle文件路径,将资源和Bundle文件放到一起就好。

RN开发调试

这里只说我的调试方式,更详细的请查阅调试文档

  1. AndroidManifest.xml中添加<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />启用调试模式。
  2. 通过npm start启动RN服务,即node node_modules/react-native/local-cli/cli.js start
  3. 真机安装APP进入RNApp界面,摇动手机可以打开开发者列表,通过设置Dev Host可以连接RN服务。Reload可以重载RNApp。
  4. 通过在Android Studio输出中检索React可以查看RN的输出(包括console)。

这里的调试基于HOST Bundle获取方式上,也就是说你的Bundle获取地址应当是默认的super.getJSBundleFile()

其他问题

  1. 原生加载RN白屏问题
    白屏是因为加载Bundle文件过慢导致的,这个网上有很多的解释了。这里我的解决办法是在App开启动画的Activity里执行了一次加载。

     private void preLoadReactNative() {
         // 1.创建ReactRootView
         ReactRootView rootView = new ReactRootView(this);
         rootView.startReactApplication(
                 ((ReactApplication) getApplication()).getReactNativeHost().getReactInstanceManager(),
                 "MyReactNativeApp",
                 null);
     }
    
  2. 返回按键问题
    这里的返回键分为两种,一种是手机硬件后退按键,另一种是导航栏后退按键。手机后退这里无需做处理,导航栏后退如下处理:

    • RN

       onBack () {
         let popRouter = Actions.pop();
         !popRouter && finishActivity(); // 判断是否为RN首页,返回原生上个页面。
       }
      

部分代码参考ReactNativeApp项目

iOS环境搭建

基于Android环境搭建,iOS部署大同小异,这里仅介绍不同的部分。

依赖安装

修改Podfile,添加如下内容:(环境配置参考集成到现有原生应用)

# 'node_modules'目录一般位于根目录中
# 但是如果你的结构不同,那你就要根据实际路径修改下面的`:path`
pod 'React', :path => '../node_modules/react-native', :subspecs => [
    'Core',
    'CxxBridge', # 如果RN版本 >= 0.45则加入此行
    'DevSupport', # 如果RN版本 >= 0.43,则需要加入此行才能开启开发者菜单
    # 这里注意一下!!!添加这些解决react-navigation的native module not be null的问题
    'ART',
    'RCTActionSheet',
    'RCTGeolocation',
    'RCTImage',
    'RCTNetwork',
    'RCTPushNotification',
    'RCTSettings',
    'RCTText',
    'RCTVibration',
    'RCTWebSocket', # 这个模块是用于调试功能的
    'RCTLinkingIOS',
]
# 如果你的RN版本 >= 0.42.0,则加入下面这行
pod "yoga", :path => "../node_modules/react-native/ReactCommon/yoga"

# 如果RN版本 >= 0.45则加入下面三个第三方编译依赖
pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec'
pod 'GLog', :podspec => '../node_modules/react-native/third-party-podspecs/GLog.podspec' // 这里修改glog为GLog
pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'

end

然后运行pod install安装依赖

这里是离线的包,怎么去合并到打包流程?(可以通过npm install去解决,不过比较麻烦)

RNApp嵌入原生

和安卓一样,需要写一个类似于activity的壳子供给RN。

ReactView.h

#import <Foundation/Foundation.h>

@interface ReactView : UIViewController

@end

ReactView.m

#import "ReactView.h"
#import <React/RCTRootView.h>
#import <React/RCTBridgeModule.h>
#import "commModule.h" # 这里是RN和原生交互所需要的一些方法,可先去掉

@interface ReactView()<UINavigationBarDelegate>
@end
@implementation ReactView

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    # 这里的http和localhost要给一下权限
    NSString * strUrl = @"http://localhost:8081/index.bundle?platform=ios&dev=true";    
    NSURL * jsCodeLocation = [NSURL URLWithString:strUrl];

    # 除http之外,和安卓一样,也可以通过bundle
    RCTRootView * rootView = [[RCTRootView alloc] 
                                initWithBundleURL:jsCodeLocation
                                moduleName:@"MyReactNativeApp"
                                initialProperties:nil
                                launchOptions:nil];
    self.view = rootView;
}

#pragma mark - 导航条处理,取消RN的导航栏
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    [navigationController setNavigationBarHidden:YES animated:YES];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

Bundle地址重定义

NSString *cachePath = @"XXX";
cachePath = [cachePath stringByAppendingPathComponent:@"index.ios.bundle"];
NSURL *jsCodeLocation = [NSURL URLWithString:cachePath];

中间遇到了几个编译问题导致build失败。问题与解决方法如下:

这样配个正常的iOS跳转地址,就可以正常跳转到RN了。这里的RN界面最好用一件简单的hello world测试下。

编译问题怎么合到打包代码中?

原生和RN的通信

通信一样是原生提供一下方法,可供给RN调用,直接上代码。

commModule.h

#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>

@interface commModule : NSObject <RCTBridgeModule>

@end

commModule.m

#import "commModule.h"
#import <React/RCTConvert.h>

@implementation commModule

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(rnCallNativeFromCallback:(NSString *)msg callback:(RCTResponseSenderBlock)callback)
{
    NSString *result = [@"处理结果:" stringByAppendingString:msg];
    callback(@[[NSNull null], result]);
}

RCT_REMAP_METHOD(rnCallNativeFromPromise, msg:(NSString *)msg
                findEventsWithResolver:(RCTPromiseResolveBlock)resolve
                rejecter:(RCTPromiseRejectBlock)reject)
{
    NSString *result = [@"处理结果:" stringByAppendingString:msg];
    resolve(result);
}

RCT_REMAP_METHOD(getUserInfo, msg:(NSDictionary *)msg
                resolver:(RCTPromiseResolveBlock)resolve
                rejecter:(RCTPromiseRejectBlock)reject)
{
    
    resolve(@{@"name": @"iOS"});
}

RCT_EXPORT_METHOD(startActivityFromJS:(NSString *)path params:(NSDictionary *)params)
{
    # 路由跳转,注意UI主线程
}

RCT_EXPORT_METHOD(activityFinish)
{
    # 结束RN finish,注意UI主线程
}

- (NSDictionary *)getConstants
{
    int n = arc4random_uniform(100);
    return @{ @"test": @"test" };
}

@end

这样就可以在RN端调用了,调用方法和Android一样。

RN资源管理

测试发现如果将打包的Bundle文件和资源文件放到统一目录下,就可以正常引用资源。
参考命令react-native bundle --platform ios --dev false --entry-file index.ios.js --bundle-output ReactNative/ios/index.ios.bundle --assets-dest ReactNative/ios

有需要做热更的可以指定以下Bundle文件路径,将资源和Bundle文件放到一起就好。

RN开发调试

这里只说我的调试方式,更详细的请查阅调试文档

  1. 通过initWithBundleURL方式连接本地RN服务
  2. 通过npm start启动RN服务,即node node_modules/react-native/local-cli/cli.js start
  3. command + R可以在RN界面Reload RN
  4. 通过在Xcode输出中检索React可以查看RN的输出(包括console)。

遗留问题

上一篇下一篇

猜你喜欢

热点阅读