React Native 整合指南

610 查看

React-Native

React-Native 目前是一个不错的多平台开发和快速开发的解决方案,它解决了很多开发上的痛点,并且作为 facebook 大厂出品,维护上有保障,因此学习 RN 是有好处的。当然,它目前还存在着很多的问题,但是都可以通过一些方案来规避和解决。RN 目前来说对 iOS 开发者比较友好,而 Android 开发环境则比较难以配置。

HelloWorld

学什么都得先来个 HelloWorld,RN 官方文档对 HelloWorld 已经讲的很详细了。

> brew install node
> brew install watchman
> npm install -g react-native-cli
> react-native init HelloWorld

然后就自动创建了对应的 iOS 和 Android 工程,笔者自己使用的是 node4-lts 版本,watchman 用于监视文件是否改变,不过笔者记得以前版本 watchman 是通过 npm 安装的,并且不是强制要求。可能现在版本改动了,不装就不能运行。

iOS
{
  "name": "HelloWorld",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node node_modules/react-native/local-cli/cli.js start"
  },
  "dependencies": {
    "react": "15.3.2",
    "react-native": "0.34.1"
  }
}

这是 package.json 文件内容,里面依赖了 react 和 react-native。打开 iOS 工程,里面基本和普通的 iOS 工程差不多,Libraries 目录包括了 node_modules 文件夹下的依赖工程,React.xcodeproj 工程内含两段脚本,一个是 RCTJSCProfiler 分析,一个是启动本地 node 服务器。AppDelegate.m 如下

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  NSURL *jsCodeLocation;

  jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil];

  RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                      moduleName:@"HelloWorld"
                                               initialProperties:nil
                                                   launchOptions:launchOptions];
  rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];

  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  UIViewController *rootViewController = [UIViewController new];
  rootViewController.view = rootView;
  self.window.rootViewController = rootViewController;
  [self.window makeKeyAndVisible];
  return YES;
}

主要的区别就是使用 RCTRootView 作为 RN 渲染视图添加到控制器上。并且指定了 bundleURL 和 moduleName,看过源码的应该知道,bundleURL 就是本地或者远程 jsbundle 路径,而 moduleName 就是 js 代码中注册的模块名,jsBundleURLForBundleRoot:fallbackResource 则是返回本地或者远程的 jsbundle 路径,这里就不细讲了,只要点开源码一看就懂。

Android

Android 环境配置比 iOS 麻烦多了,因为 iOS URL 等配置都是明文放在 AppDelegate 代码中的,哪里创建了 RootView 都很清楚,而安卓就比较麻烦,MainApplication 实现了 ReactApplication 接口,主要是生成了 mReactNativeHost 成员变量。mReactNativeHost 持有 ReactInstanceManager 类变量,初始化代码如下

ReactInstanceManager.Builder builder = ReactInstanceManager.builder()
      .setApplication(mApplication)
      .setJSMainModuleName(getJSMainModuleName())
      .setUseDeveloperSupport(getUseDeveloperSupport())
      .setRedBoxHandler(getRedBoxHandler())
      .setUIImplementationProvider(getUIImplementationProvider())
      .setInitialLifecycleState(LifecycleState.BEFORE_CREATE);

这里可以看到指定了 jsMainModuleName 和是否显示开发者菜单的参数,再来看 MainActivity,它继承了 ReactActivity,只重写了一个方法

/**
 * Returns the name of the main component registered from JavaScript.
 * This is used to schedule rendering of the component.
 */
@Override
protected String getMainComponentName() {
    return "HelloWorld";
}

再来看它的父类 ReactActivity,里面有一个成员变量 ReactActivityDelegate,它接受两个参数初始化

new ReactActivityDelegate(this, getMainComponentName());

这就变相的将组件名传入了 mDelegate。再进入 ReactActivityDelegate,就能找到

private @Nullable ReactRootView mReactRootView;

这就是 RootView,然后它的初始化如下

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());
    getPlainActivity().setContentView(mReactRootView);
}

这里的初始化实际上就是对应了 iOS 中 rootView 的初始化,只不过 Android 是 startReactApplication 而不是通过构造函数传递参数的方式。并且,主要的参数都是通过 ReactInstanceManager 来存储管理,appKey 实际上就是组件名,等同于 iOS 中的 moduleName,而 ReactInstanceManager 初始化中的 setJSMainModuleName 才是 jsbundle 名称。Android 甚至没有在代码中指定 remote server jsbundle 路径的地方,目前只能进入 app 然后打开 dev setting 来修改远程服务器地址,当然,也可能是笔者没有找到,如果有找到的朋友请在评论中告诉我。而且 ReactInstanceManager 初始化后面还有一小段代码

String jsBundleFile = getJSBundleFile();
if (jsBundleFile != null) {
    builder.setJSBundleFile(jsBundleFile);
} else {
    builder.setBundleAssetName(Assertions.assertNotNull(getBundleAssetName()));
}

发现了没,iOS 下远程 jsbundle 和本地 jsbundle 是通过一个方法统一处理的,而 Android 则需要两个参数维护。iOS 下 RN 只提供了 RootView,开发者只需要处理好 RootView 暴露的方法就行了,更加简单,而 Android 环境下则是提供了 ReactActivity 类来做封装,并且将参数拆成了好几个类,非常的麻烦。

Integration With Existing Apps

iOS

手动 iOS 整合步骤如下:

  1. package.json

  2. 创建带有 subspecs 的 Podfile 用于引入所需要的第三方库,这需要 CocoaPods,虽然也可以手动 link 第三方库,但是目前笔者推荐使用 CocoaPods

  3. index.ios.js

  4. RCTRootView

当然,还有其他步骤,比如 ATS 的问题,不过一个合格的开发者应当都能完成,看上面这些步骤,你会发现实际上在 iOS 下,手动整合和 HelloWorld 基本是一致的。

Android

手动整合步骤如下

  1. package.json

  2. 增加 com.facebook.react:react-native:+maven {} 到 build.gradle

  3. index.android.js

  4. ReactRootView

是否发现了,在 Android 环境下,HelloWorld 工程和手动整合指南是不一样的,最主要的区别在于 HelloWorld 工程是继承 ReactActivity 来产生 Activity 的,而手动整合则是可以随便做,甚至可以在 Fragment 上面创建 RootView。

总结

就目前而言,RN 在 iOS 下无论是表现还是编码难度,都优于 Android 环境,Android 官方甚至没有一个很好的启动 App 方式,只能 react-native run-android。而且在性能方面,iOS 经过良好的编码和优化,是能够达到原生代码的水平的,而 Android 则有不少问题,但是 RN 虽然有这样那样的问题,它对整个跨平台开发是有很大贡献的,让开发者有机会多端开发,能涉足其他平台,而且相信未来这些问题都是能解决的。