Native应用集成ReactNative实践

ReactNative

去年Facebook发布了ReactNative之后, 各大厂也都反应积极, 今天就来抛抛砖。
ReactNative介绍在这里就不赘述了, 这里是官方介绍, 民间版在这里.
大家一般肯定有是有现成项目的, 完全新开一个项目可能性比较小, 所以集成就是重中之重了。

目标

在已有项目中我们使用ReactNative实现一个功能模块, 使其可以与Native代码进行通讯, 使其可以从ReactNative跳到Native

实践

Facebook官方教程里面有关于如何集成的教程, 安卓版, iOS版
我们主要描述一下iOS版的过程, 必要条件请移步这里
大概熟悉一下之后, 进行如下步骤

  • 首先建一个文件夹, 我们就叫ReactNativeDemo, 之后

    1
    2
    3
    4
    cd ReactNativeDemo
    npm init # 会问些问题什么的, 基本上测试一路回车就行了, 第一步的name不能有大些字母
    npm install --save react-native
    curl -o .flowconfig https://raw.githubusercontent.com/facebook/react-native/master/.flowconfig
  • 都完成之后, 向package.json里的scripts里面添加
    "start": "node_modules/react-native/packager/packager.sh"

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 完成之后 package.json 看起来大概是这个样子
    {
    "name": "react-native-test",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node_modules/react-native/packager/packager.sh"
    },
    "author": "",
    "license": "ISC",
    "dependencies": {
    "react-native": "^0.21.0"
    }
    }
  • 再添加一个文件叫index.ios.js, 内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    'use strict';

    import React, {
    TouchableHighlight,
    Text,
    View
    } from 'react-native';

    var styles = React.StyleSheet.create({
    container: {
    flex: 1,
    backgroundColor: 'orange'
    }
    });

    class SimpleApp extends React.Component {
    constructor(props) {
    super(props);
    }

    render() {
    return (
    <View style={styles.container}>
    <Text>This is a simple application.</Text>
    <TouchableHighlight onPress={ this._onPressButton.bind(this) }>
    <Text>Tap me!</Text>
    </TouchableHighlight>
    </View>
    )
    }

    _onPressButton(event) {
    console.log("Press action");
    }
    }

    React.AppRegistry.registerComponent('SimpleApp', () => SimpleApp);

    到这里javascript部分就差不多了, 我们开始建iOS项目

  • 新建一个工程叫ReactNativeTest, 放到我们刚建的文件夹ReactNativeDemo里面, 因为iOS 9增强了安全性, 不允许HTTP连接, 编辑工程的Info.plist, 追加如下内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <key>NSAppTransportSecurity</key>
    <dict>
    <key>NSExceptionDomains</key>
    <dict>
    <key>localhost</key>
    <dict>
    <key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
    <true/>
    </dict>
    </dict>
    </dict>
  • 在工程根目录里新建一个文件Podfile

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # Depending on how your project is organized, your node_modules directory may be
    # somewhere else; tell CocoaPods where you've installed react-native from npm
    pod 'React', :path => '../node_modules/react-native', :subspecs => [
    'Core',
    'RCTImage',
    'RCTNetwork',
    'RCTText',
    'RCTWebSocket',
    # Add any other subspecs you want to use in your project
    ]
  • 在终端中执行

    1
    pod install
  • 完成之后在storyboard里面拖大概这么个东西Storyboard

  • 结构大概是UITabBarController管着两个UIViewController, 上面那个我们用来显示ReactNative内容, 下面那个是Native的内容, 并且都用UINavigationController包起来方便页面跳转

  • 再建一个RNNotificationManager, 语言选Objective-C, 留空就好, 现在建这么个文件是为了生成swiftObjective-C通讯的头文件

  • 之后Xcode会提示生成一个头文件, 在那个文件里添加#import "RCTRootView.h", 因为刚才我建的项目是swift版的, 所以需要加这么一句以便swift调用, 接着

  • 新建一个ReactView.swift内容如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    import UIKit

    class ReactView: UIView {
    override init(frame: CGRect) {
    super.init(frame: frame)
    setup()
    }
    required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    }
    override func awakeFromNib() {
    setup()
    }
    private func setup() -> () {
    let jsCodeLocation = NSURL(string: "http://localhost:8081/index.ios.bundle?platform=ios")
    let rootView = RCTRootView(
    bundleURL: jsCodeLocation
    , moduleName: "SimpleApp"
    , initialProperties: .None
    , launchOptions: .None
    )
    rootView.frame = bounds
    addSubview(rootView)
    rootView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
    }
    }
  • 然后在storyboard上面那个ViewControllerViewclass写成ReactView

  • 新建一个文件NativeViewController内容如下, 并且在storyboard中把下面的那个ViewControllerclass指定为NativeViewController, 再拖一个UILabel放进来, 连到NativeViewController

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    // ViewController.swift
    import UIKit

    class ViewController: UIViewController {

    override func viewDidLoad() {
    super.viewDidLoad()
    edgesForExtendedLayout = .Bottom // 上方不要扩展, 否则就看不到最上面那行了
    }

    func notificationHandler(notification: NSNotification) -> () {
    // 留着后面用
    }

    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if let vc = segue.destinationViewController as? NativeViewController {
    vc.messageText = sender?.description
    }
    }
    }

    // NativeViewController.swift
    import UIKit

    class NativeViewController: UIViewController {
    var messageText: String!
    @IBOutlet weak var label: UILabel!

    override func viewDidLoad() {
    super.viewDidLoad()
    if messageText == .None {
    messageText = "Native Message"
    }
    label.text = messageText
    }
    }
  • ReactNativeDemo文件夹里面执行

    1
    npm start

    之后, 在Xcode运行iOS程序, 是这么个样子的东西模拟器运行图
    Tap me!控制台应该输出Press Action, 说明一切正常了

ReactNative与Native通讯

现在我们可以着手进行通讯了, ReactNative集成Native内容有两种类型, 分别是Native UI Component使用原生代码写的节目, 与Native Module使用原生代码写的非界面模块。
这里, 我希望通过点击Tap me!通过NavigationController push跳进Native的界面, 具体我想通过NSNotificationCenter来实现。我们在点击的时候, 给通知中心发消息, 然后让ViewController捕获消息进行页面跳转。

于是在刚才的RNNotificationManager两个文件写成这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// RNNotificationManager.h
#import <Foundation/Foundation.h>
#import "RCTBridgeModule.h"

@interface RNNotificationManager : NSObject <RCTBridgeModule>
@end

// RNNotificationManager.m
#import "RNNotificationManager.h"
#import "RCTLog.h"

@implementation RNNotificationManager

RCT_EXPORT_MODULE(); // 向ReactNative注册Native模块
RCT_EXPORT_METHOD(postNotification:(NSString *)name userInfo:(NSDictionary *)userInfo)
{
RCTLogInfo(@"postNotification name: %@ userInfo %@", name, userInfo);
[[NSNotificationCenter defaultCenter] postNotificationName:name object:self userInfo:userInfo];
}
@end

修改index.ios.js

1
2
3
4
5
6
7
8
9
10
11
12
13
// import 部分改为
import React, {
NativeModules, // ReactNative调用Native用的模块
TouchableHighlight,
Text,
View
} from 'react-native';

// constructor 追加
this.notificationCenter = NativeModules.RNNotificationManager; // 取得Native模块

// _onPressButton 追加
this.notificationCenter.postNotification("pushVC", {"k": "v"}); // 向Native发送消息

最后, 修改ViewController.swift

1
2
3
4
5
6
7
8
9
10
11
// viewDidLoad 追加
NSNotificationCenter.defaultCenter().addObserver(
self,
selector: "notificationHandler:",
name: "pushVC",
object: .None)

// notificationHandler 追加
dispatch_async(dispatch_get_main_queue()) { [weak self] _ in
self?.performSegueWithIdentifier("nativeSegue", sender: notification.userInfo?["k"])
}

为什么notificationHandler中要用GCD切换到主队列中执行performSegueWithIdentifier呢?
这是因为ReactNative很多操作都是异步执行的, 如果不切到主队列的话我们的界面是没有响应的, 因为“非主线程不能操作UI”嘛。

至此, 就大功告成了!

结尾

通过这个可行性实验, 我们可以结合具体的业务需求来具体实现。
假如我们已有的模块都可以拿出来使用的话, 设想我们所有的节目都有自己独立的identifier, 那么通过修改ReactNative发送的消息就可以随便进入希望进入的页面, 从而达成“即使不是纯ReactNative项目, 我们也可以在线更新”这个愿望。

源码在此