ReactNative设计与实现之二:实践

ReactNative设计与实现之二:实践

在上一篇文章《ReactNative设计与实现之一:背景》,重点介绍了RN跨平台方案的思路,并将其与其他几种跨平台方案做了一些简单的对比。本文则主要以例子的形式来介绍RN是Android、iOS原生开发的异同点。

一、一个React-Native工程的目录结构

我们按照React-Native官网的Getting Started的指引,借助Create React Native App创建了一个名为NavigatorTest的RN工程。然后借助tree命令,分析了这个RN工程的目录结构,如下图所示(略去了部分不相关文件):

由上图可知,一个RN工程中包含一个完整的Android和一个完整的iOS工程,它们分别位于android和ios目录下。当我们使用react-native run-android命令时,它最终会启动位于android目录下的Android工程,并根据manifest加载主Activity。这里,主Activity的定义如下:

public class MainActivity extends ReactActivity

其中ReactActivity由RN提供,它继承自Android的Activity。

二、React-Native与原生开发的对比

首先让我们来看两段Android代码:

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  ...
}

上述代码是Android Activity中onCreate方法的常见写法,它通过setContentView方法来指定Activity将要渲染的UI,一般是一个Android的layout xml文件。

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);

  mReactRootView = new ReactRootView(this);
  mReactInstanceManager = initReactInstanceManager();
  mReactRootView.startReactapplication(mReactInstanceManager, "demo", null);

  setContentView(mReactRootView)
}

这段代码是RN的ReactActivity中的onCreate方法。可以看到,它同样是通过setContentView方法来指定Activity的UI。不同的是,第一段代码指定的是一个layout文件,第二段代码指定的是ReactRootView的一个实例。熟悉Android的同学都知道,setContentView方法也可以接收一个view的实例来作为它的参数。

在这里,ReactRootView继承SizeMonitoringFrameLayout,而SizeMonitoringFrameLayout则继承Android的FrameLayout。所有与JS的交互都发生在ReactRootView内。

三、在原生APP中集成React-Native

关于如何在一个已有的Native APP中继承RN,RN官方文档的Integration with Existing Apps这一小节有详细的介绍,简而言之,可分为以下五步:

  1. 设置好React-Native依赖项和目录结构,将已有工程根目录下的文件全部拷贝到android文件夹下
  2. 用JavaScript开发React-Native组件
  3. 新建一个activity,并将ReactRootView设为它的ContentView
  4. 在工程根目录下通过npm start启动React-Native服务,随后运行Android工程
  5. 验证APP的React-Native部分是否按预期工作

感兴趣的同学可以按照官方教程去实验一下,这里我就不再赘述了。这里需要注意一点:我们需要在app/build.gradle的defaultConfig中添加 ndk { abiFilters “armeabi-v7a”, “x86” },否则可能会报下面的错误:

java.lang.UnsatisfiedLinkError:
    dlopen failed: "libgnustl_shared.so" is 32-bit instead of 64-bit

四、如何开发一个NativeModule

前面我们已经提到,RN借助NativeModule来做Native扩展,且官方提供的与native相关的功能与组件,都是通过这一机制来实现的。其实,我们借助NativeModule来扩展的native功能和组件与RN官方提供的无本质上的差别。

为了实现一个native功能或组件,我们需要在Android与iOS上分别开发,官方文档的native-modules-androidnative-modules-ios这两小节分别有详细的介绍。在上一篇文章《ReactNative设计与实现之一:背景》中,我们提到了中间转换系统的概念,添加新的NativeModule其实就是扩展我们的中间转换系统,使得它能够转换更多的JS指令。

一个NativeModule具有以下三个要素:

  1. NativeModule的名字,由getName方法返回;JS侧直接通过这个名字来调用该Module提供的功能
  2. NativeModule暴露给JS侧的常量:由getConstants方法返回
  3. NativeModule暴露给JS侧的方法:由@ReactNative注解修饰的方法

下面我们以Android端的Toast为例,来展示如何开发一个NativeModule。

1. 定义一个NativeModule

public class ToastModule extends ReactContextBaseJavaModule {
  private static final String DURATION_SHORT_KEY = "SHORT";
  private static final String DURATION_LONG_KEY = "LONG";

  public ToastModule(ReactApplicationContext reactContext) {
    super(reactContext);
  }

  @Override
  public String getName() { return "ToastExample"; }

  @Override
  public Map<String, Object> getConstants() {
    final Map<String, Object> constants = new HashMap<>();
    constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT);
    constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG);
    return constants;
  }

  @ReactMethod
  public void show(String message, int duration) {
    Toast.makeText(getReactApplicationContext(), message, duration).show();
  }
}

上述代码中,我们通过getName()方法定义了一个名为ToastExample的NativeModule;并通过getConstants()方法对JS侧暴露了两个常量,用来控制Toast的显示时间;最后我们通过@ReactMethod注解向JS侧暴露了一个show方法,JS侧将通过调用这个show方法来展示Toast。

2. 定义一个ReactPackage,并注册ToastModule

public class CustomToastPackage implements ReactPackage {
  @Override
  public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
    return Collections.emptyList();
  }

  @Override
  public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
    List<NativeModule> modules = new ArrayList<>();
    modules.add(new ToastModule(reactContext));
    return modules;
  }
}

上述代码定义了一个名为CustomToastPackage的ReactPackage,通过ReactPackage,我们可以将多个功能相近的NativeModule聚合在一起。在createNativeModules方法中,我们定义了一个NativeModule的List,并将上文中定义的ToastModule添加到这个List中,最后返回这个List。

3. 注册ReactPackage

这需要通过ReactNativeHost的getPackages方法来完成。以上文中提到的NavigatorTest工程为例,我们需要在android/app/src/main/java/com/navigatortest/MainApplication.java内的getPackages()方法中注册CustomToastPackage。代码如下:

public class MainApplication extends Application implements ReactApplication {

  private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
    ...

    protected List<ReactPackage> getPackages() {
      return Arrays.<ReactPackage>asList(
        new MainReactPackage(),
        new CustomToastPackage()); // <-- Add this line with your package name.
      }
    };
  }

  @Override
  public ReactNativeHost getReactNativeHost() {
    return mReactNativeHost;
  }
  ...
}

4. 在JS侧使用ToastModule

// ToastExample.js
import { NativeModules } from 'react-native';
module.exports = NativeModules.ToastExample;

// Other.js
import ToastExample from './ToastExample';
ToastExample.show('Awesome', ToastExample.SHORT);

上述代码中,我们通过NativeModules.ToastExample来获得上文中定义的ToastModule,请注意,ToastExample与ToastModule中的getName()方法的返回值一致。在JS侧的常见做法是先将NativeModule做一层封装,如上述代码中的ToastExample.js,然后在其他JS文件中引用这个封装,如上述代码中的Other.js。

五、小结

本文我们将RN与原生开发做了一些简单的对比,并着重介绍了如何开发一个NativeModule。下面我们将讨论RN的整体架构

编辑于 2018-10-09

文章被以下专栏收录