美文网首页
React-Native混编学习

React-Native混编学习

作者: 2林子易2 | 来源:发表于2018-05-02 14:04 被阅读0次

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

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

    • node v8.1.3(nvm管理)
    • react-native-cli 0.53.3(npm全局包)
    • nrm(npm全局包,主要是为了管理npm源,切换npm源为taobao)
    • gulp(npm全局包,主要是打包zip压缩)

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

    APP配合

    • 环境搭建
      • 依赖如何引入
    • 接口定义
      • 获取当前用户信息
      • 获取当前环境状态
      • 结束RN activity
      • 路由跳转结构定义(互相跳转的接口)
    • RN热更部分完善(Android和iOS)
      • 资源包地址重定义
      • 热更代码完善
    • RN开发调试
      • 兼容开发环境,方便我们调试(Bundle地址,开发模式启动)
    • 打包脚本修改
      • 依赖如何注入
      • 编译问题解决
    • 部分常量配置
      • RN服务地址常量
      • RN主组件名称
      • Bundle地址
    • RN路由定义
    • 登陆验证问题

    android环境搭建

    基础部分

    • RNApp嵌入原生
    • 原生跳转RN路由
    • 原生和RN的通信
    • RN资源管理
    • RN开发调试
    • 其他问题
      • 原生加载RN白屏问题
      • 回退问题

    RNApp嵌入原生

    1. 依赖引入

    引入node_modules/react-native/android

    • 主build.gradle 添加

      maven {
          // All of React Native (JS, Android binaries) is installed from npm
          url "$rootDir/../node_modules/react-native/android"
      }
      
    • app/build.gradle 添加 compile "com.facebook.react:react-native:0.53.3"

    后需改为我们的android包管理

    2. MyApplication类继承ReactApplication

    • onCreate中添加SoLoader.init(this, false);

    • Override public ReactNativeHost getReactNativeHost() { return mReactNativeHost; }代码如下:

      private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
        @Nullable
        @Override
        protected String getJSBundleFile() { // 定义RN Bundle文件地址
          return super.getJSBundleFile(); // 默认地址为assets/index.android.bundle
        }
      
        @Override
        public boolean getUseDeveloperSupport() { // 定义DEBUG模式
            return BuildConfig.DEBUG;
        }
      
        @Override
        protected List<ReactPackage> getPackages() { // RN包载入
            return Arrays.<ReactPackage>asList(
                    new MainReactPackage(),
                    new CommPackage(); // 载入公共包,见原生和RN通信
            );
        }
      };
      

    后需重新定义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部分

    • ReactActivityDelegate类

      protected void loadApp(String appKey) {
        if (mReactRootView != null) {
          throw new IllegalStateException("Cannot loadApp while app is already running.");
        }
        mReactRootView = createRootView();
        mReactRootView.startReactApplication(
          getReactNativeHost().getReactInstanceManager(),
          appKey,
          getLaunchOptions()); // 在这里有个getLaunchOptions方法,就是传递Bundle的地方
        getPlainActivity().setContentView(mReactRootView);
      }
      
      protected @Nullable Bundle getLaunchOptions() {
        return null;
      }
      
    • MyReactActivity类
      我们这里通过在MyReactActivity类里重载getLaunchOptions方法,传递Bundle(参考RNApp嵌入原生代码)

    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部分

    • Package类

      package xxx;
      
      import com.facebook.react.ReactPackage;
      import com.facebook.react.bridge.NativeModule;
      import com.facebook.react.bridge.ReactApplicationContext;
      import com.facebook.react.uimanager.ViewManager;
      
      import java.util.ArrayList;
      import java.util.Collections;
      import java.util.List;
      
      public class CommPackage implements ReactPackage {
          public CommModule mModule;
      
          /**
          * 创建Native Module
          * @param reactContext
          * @return
          */
          @Override
          public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
              List<NativeModule> modules = new ArrayList<>();
              mModule = new CommModule(reactContext);
              modules.add(mModule);
              return modules;
          }
      
          @Override
          public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
              return Collections.emptyList();
          }
      }
      
    • module类

      import android.app.Activity;
      import android.content.Intent;
      import android.net.Uri;
      import android.support.annotation.Nullable;
      import android.util.Log;
      
      import com.facebook.react.bridge.Arguments;
      import com.facebook.react.bridge.Callback;
      import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
      import com.facebook.react.bridge.Promise;
      import com.facebook.react.bridge.ReactApplicationContext;
      import com.facebook.react.bridge.ReactContextBaseJavaModule;
      import com.facebook.react.bridge.ReactMethod;
      import com.facebook.react.bridge.ReadableMap;
      import com.facebook.react.bridge.ReadableNativeMap;
      import com.facebook.react.bridge.WritableMap;
      import com.facebook.react.modules.core.DeviceEventManagerModule;
      
      import java.util.HashMap;
      import java.util.Iterator;
      import java.util.Map;
      import java.util.Random;
      
      /**
      * Created by jhjr on 18-4-24.
      */
      
      public class CommModule extends ReactContextBaseJavaModule {
      
          private ReactApplicationContext mContext;
          public static final String MODULE_NAME = "commModule";
          public static final String EVENT_NAME = "nativeCallRn";
          public static final String EVENT_NAME1 = "getPatchImgs";
      
          /**
          * 构造方法必须实现
          * @param reactContext
          */
          public CommModule(ReactApplicationContext reactContext) {
              super(reactContext);
              this.mContext = reactContext;
          }
      
          /**
          * 在rn代码里面是需要这个名字来调用该类的方法
          * @return
          */
          @Override
          public String getName() {
              return MODULE_NAME;
          }
      
          /**
          * Native调用RN
          * @param msg
          */
          public void nativeCallRn(String msg) {
              mContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
                      .emit(EVENT_NAME,msg);
          }
      
          /**
          * Callback 方式
          * rn调用Native,并获取返回值
          * @param msg
          * @param callback
          */
          @ReactMethod
          public void rnCallNativeFromCallback(String msg, Callback callback) {
      
              // 1.处理业务逻辑...
              String result = "处理结果:" + msg;
              // 2.回调RN,即将处理结果返回给RN
              callback.invoke(null, result);
          }
      
          /**
          * Promise
          * @param msg
          * @param promise
          */
          @ReactMethod
          public void rnCallNativeFromPromise(String msg, Promise promise) {
      
              Log.e("---","adasdasda");
              // 1.处理业务逻辑...
              String result = "处理结果:" + msg;
              // 2.回调RN,即将处理结果返回给RN
              promise.resolve(result);
          }
      
          /**
          * 向RN传递常量
          */
          @Nullable
          @Override
          public Map<String, Object> getConstants() {
              Map<String,Object> params = new HashMap<>();
              Random rand = new Random();
              int i = rand.nextInt(100);
      
              Object o = new Object();
              o = (Object)(new Integer(i));
      
              params.put("test",o);
              return params;
          }
      
          @ReactMethod
          public void startActivityFromJS(String path, ReadableMap params){
              try{
                  Activity currentActivity = getCurrentActivity();
      
                  if(null!=currentActivity){
                      Map intentParams = ((ReadableNativeMap) params).toHashMap();
      
                      // 路由跳转
                  }
              }catch(Exception e){
                  throw new JSApplicationIllegalArgumentException(
                          "不能打开Activity : "+e.getMessage());
              }
          }
      
          @ReactMethod
          public void activityFinish() {
              try{
                  Activity currentActivity = getCurrentActivity();
                  currentActivity.finish();
              }catch(Exception e){
                  throw new JSApplicationIllegalArgumentException(
                          "不能打开Activity : "+e.getMessage());
              }
          }
      
          /**
          * Promise
          * @param promise
          */
          @ReactMethod
          public void getUserInfo(Promise promise) {
              WritableMap map = Arguments.createMap();
              map.putString("name", "Android");
              promise.resolve(map);
          }
      
      }
      

    最后在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首页,返回原生上个页面。
         }
        
    • 原生见本篇finishActivity方法。

    部分代码参考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失败。问题与解决方法如下:

    • React Native iOS: Could not build module 'yoga': 'algorithm' file not found

    • RCTReconnectingWebSocket.h文件的#import <fishhook/fishhook.h> 显示error: 'fishhook/fishhook.h' file not found

      "scripts": {
          "postinstall": "sed -i '' 's#<fishhook/fishhook.h>#\"fishhook.h\"#g' ./node_modules/react-native/Libraries/WebSocket/RCTReconnectingWebSocket.m"
      }
      
    • resolveRCTValueAnimatedNode
      sed -i '' 's/#import <RCTAnimation\\/RCTValueAnimatedNode.h>/#import \"RCTValueAnimatedNode.h\"/' ./node_modules/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h

    这样配个正常的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)。

    遗留问题

    • android项目依赖maven { url "$rootDir/../node_modules/react-native/android" }
    • iOS项目依赖怎么合并到打包脚本中
    • iOS编译问题如何在流程中解决
    • 登陆验证问题

    相关文章

      网友评论

          本文标题:React-Native混编学习

          本文链接:https://www.haomeiwen.com/subject/nebglftx.html