美文网首页WEB前端程序开发Hybrid开发Kevin的IOS开发专题
【Hybrid开发高级系列】ReactNative(五) ——

【Hybrid开发高级系列】ReactNative(五) ——

作者: Kevin_Junbaozi | 来源:发表于2018-05-22 23:33 被阅读13次

    1 高级开发技巧

    1.1 嵌入到现有原生应用

    1.1.1 核心概念

            如果你正准备从头开始制作一个新的应用,那么React Native会是个非常好的选择。但如果你只想给现有的原生应用中添加一两个视图或是业务流程,React Native也同样不在话下。只需简单几步,你就可以给原有应用加上新的基于React Native的特性、画面和视图等。

            把React Native组件植入到iOS应用中有如下几个要点:

        1、首先当然要了解你要植入的React Native组件。

        2、Create a Podfile with subspecs for all the React Native components you will need for your integration.

        3、Create your actual React Native components in JavaScript.

        4、Add a new event handler that creates a RCTRootView that points to your React Native

    component and its AppRegistry name that you defined in index.ios.js.

        5、Start the ReactNative server and run your native application.

        6、Optionally add more React Native components.

        7、调试

        8、Prepare for deployment(e.g., via the react-native-xcode.shscript).

        9、Deploy andProfit!

    1.1.2 开发环境准备(老版本构建方式)

    1.1.2.1 基础环境

            First, follow the 开发环境搭建教程 for your development environment and the iOS target platform to install the prerequisites for React Native.

    1.1.2.2 CocoaPods

            CocoaPods是针对iOS和Mac开发的包管理工具。我们用它来把React Native框架的代码下载下来并添加到你当前的项目中。

    $ sudo gem install cocoapods

            从技术上来讲,我们完全可以跳过CocoaPods,但是这样一来我们就需要手工来完成很多配置项。CocoaPods可以帮我们完成这些繁琐的工作。

    1.1.2.3 安装依赖包ReactJS、RN

            React Native integration requires both the React and React Native node modules. The ReactNative Framework will provide the code to allow your application integration to happen.

    package.json#

            我们把具体的依赖包记录在package.json文件中。

    file. Create this file in the root of your project if it does not exist.

            Normally with React Native projects, you will put files like package.json, index.ios.js, etc. in the root directory of your project and then have your iOS specific native code in a subdirectory like ios/ where your Xcode project is located (e.g., .xcodeproj).

            Below is an example of what your package.json file should minimally contain. Version numbers will vary according to your needs. Normally the latest versions for both React and React Native will be sufficient.

    {

      "name" : "NumberTileGame",

      "version" : "0.0.1",

      "private" : true,

      "scripts" : {

        "start" : "node node_modules/react-native/local-cli/cli.js start"

      },

      "dependencies": {

        "react" : "15.0.2",

        "react-native" : "0.26.1"

      }

    }

    安装依赖包

            使用npm(node包管理器,Node package manager)来安装React和React Native模块。modules via the Node package manager. The Node modules will be installed into anode_modules/directory in the root of your project.

    # From the directory containing package.json project, install the modules

    # The modules will be installed in node_modules/

    $ npm install

    1.1.2.4 CocoaPods安装

            React Native框架自身也是作为node模块安装到项目中的。Framework was installed as Node module in your project above. We will now install a CocoaPods Podfile with the components you want to use from the framework itself.

    Subspecs

            在你开始把React Native植入到你的应用中之前,首先要决定具体整合的是React Native框架中的哪些部分。而这就是subspec要做的工作。在创建Podfile文件的时候,就需要指定具体安装哪些React Native的依赖库。所指定的每一个库就都称为一个subspect。 

            into your application, you will want to decide what parts of the React Native Framework you would like to integrate. That is where subspecs come in. When you create your Podfile, you are going to specify React Native library dependencies that you will want installed so that your application can use those libraries. Each library will become a subspec in the Podfile.

            The list of supported subspecs are in node_modules/react-native/React.podspec. They are generally named by functionality. For example, you will generally always want theCore subspec. That will get you the AppRegistry, StyleSheet, View and other core React Native libraries. If you want to add the React Native Text library (e.g., for <Text> elements), then you will need the RCTText subspec. If you want the Image library (e.g., for <Image> elements),,then you will need the RCTImage subspec.

        Podfile

            After you have used Node to install the React and React Native frameworks into the node_modules directory, and you have decided on what React Native elements you want to integrate, you are ready to create your Podfile so you can install those components for use in your application.

            The easiest way to create a Podfile is by using the CocoaPods init command in the native iOS code directory of your project:

    ## In the directory where your native iOS code is located

    (e.g., where your `.xcodeproj` file is located)

    $ pod init

            Podfile will be created and saved in the iOS directory (e.g., ios/) of your current project and will contain a boilerplate setup that you will tweak for your integration purposes. In the end, Podfile should look something similar to this:

    # The target name is most likely the name of your project.

    target 'NumberTileGame' do

      # Your 'node_modules' directory is probably in the root of your project,

      # but if not, adjust the `:path` accordingly

      pod 'React', :path => '../node_modules/react-native', :subspecs=> [

        'Core',

        'RCTText',

        'RCTWebSocket', # needed for debugging

        # Add any other subspecs you want to use in your project

      ]

    end

    1.1.2.5 Pod安装

            创建好了Podfile后,就可以开始安装React Native pod了。

    $ pod install

            然后你应该可以看到类似下面的输出(译注:同样由于众所周知的网络原因,pod install的过程在国内非常不顺利,请自行配备稳定的翻墙工具,或是尝试一些镜像源):

    Analyzing dependencies

    Fetching podspec for`React` from `../node_modules/react-native`

    Downloading dependencies

    Installing React (0.26.0)

    Generating Pods project

    Integrating client project

    Sending stats

    Pod installation complete! There are 3 dependencies from the Podfile and 1 total pod installed.

    1.1.3 代码集成

            Now that we have a package foundation, we will actually modify the native application to integrate React Native into the application. For our 2048 app, we will add a"High Score" screen in React Native.

    1.1.3.1 React Native组件

            The first bit of code we will write is the actual React Native code for the new "HighScore" screen that will be integrated into our application

    1.1.3.2 创建一个index.ios.js文件

            首先创建一个空的index.ios.js文件。一般来说我们把它放置在项目根目录下。

    index.ios.js is the starting point for React Native applications on iOS. And it is always required. It can be a small file that requires other file that are part of your React Native component or application, or it can contain all the code that is needed for it. In our case, we will just put everything in index.ios.js

    # In root of your project

    $ touch index.ios.js

    1.1.3.3 添加你自己的ReactNative代码

            在index.ios.js中添加你自己的组件。, create your component. In our sample here, we will add simple component within a styled

    <View>

    'use strict';

    import React from 'react';

    import{

      AppRegistry,

      StyleSheet,

      Text,

      View

    } from 'react-native';

    class RNHighScores extends React.Component{

      render() {

        var contents = this.props["scores"].map(

          score =>{score.name}:{score.value}{"\n"}

        );

        return (

            <View style={styles.container} >

                <Text style={styles.highScoresTitle} >

                    2048 HighScores!

                </Text>

                <Text style={styles.scores}>

                   {contents}

                </Text>

            </View>

        );

      }

    }

    const styles = StyleSheet.create({

      container: {

        flex: 1,

        justifyContent: 'center',

        alignItems: 'center',

       backgroundColor: '#FFFFFF',

      },

      highScoresTitle:{

        fontSize: 20,

        textAlign: 'center',

        margin: 10,

      },

      scores: {

        textAlign: 'center',

        color: '#333333',

        marginBottom: 5,

      },

    });

    // Module name

    AppRegistry.registerComponent('RNHighScores', () =>RNHighScores);

            RNHighScores is the name of your module that will be used when you add a view to React Native from within your iOS application.

    1.1.3.4 The Magic: RCTRootView

            Now that your React Native component is created via index.ios.js, you need to add that component to a new or existing ViewController. The easiest path to take is to optionally create an event path to your component and then add that component to an existing ViewController.

            We will tie our React Native component with a new native view in the ViewController that will actually host it called RCTRootView.

    1.1.3.5 Create an Event Path

            You can add a new link on the main game menu to go to the "High Score" React Native page.

    1.1.3.6 事件处理

            We will now add an event handler from the menu link. A method will be added to the main ViewController of your application. This is where RCTRootView comes into play.

            When you build a React Native application, you use the React Native packager to create an index.ios.bundle that will be served by the React Native server. Inside index.ios.bundle will be our RNHighScore module. So, we need to point our RCTRootView to the location of the index.ios.bundle resource (via NSURL) and tie it to the module.

            We will, for debugging purposes, log that the event handler was invoked. Then, we will create a string with the location of our React Native code that exists inside the index.ios.bundle. Finally, we will create the main RCTRootView. Notice how we provide RNHighScores as the moduleName that we created above when writing the code for our React Native component.

            First import the RCTRootViewlibrary.

    #import "RCTRootView.h"

    The initialProperties are here for illustration purposes so we have some data for our high score screen. In our React Native component, we will use this.props to get access to that data.

    - (IBAction)highScoreButtonPressed:(id)sender {

        NSLog(@"High Score Button Pressed");

        NSURL *jsCodeLocation = [NSURL URLWithString: @"http://localhost:8081/index.ios.bundle?platform=ios"];

        RCTRootView*rootView = [[RCTRootView alloc] initWithBundleURL: jsCodeLocation moduleName: @"RNHighScores" initialProperties:

                                 @{

                                   @"scores": @[

                                     @{

                                       @"name" : @"Alex",

                                       @"value": @"42"

                                      },

                                     @{

                                       @"name" : @"Joel",

                                       @"value": @"10"

                                     }

                                   ]

                                 }

                               launchOptions: nil];

        UIViewController *vc = [[UIViewController alloc] init];

        vc.view =rootView;

        [self presentViewController: vc animated: YES completion: nil];

    }

            Note that RCTRootView initWithURL starts up a newJSC VM. To save resources and simplify the communication between RN views indifferent parts of your native app, you can have multiple views powered byReact Native that are associated with a single JS runtime. To do that, instead of using [RCTRootView alloc] initWithURL, use RCTBridge initWithBundleURL to create a bridge and then use RCTRootView initWithBridge.

            When moving your app to production, the NSURL can point to a pre-bundled file on disk via something like [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];. You can use the react-native-xcode.sh script in node_modules/react-native/packager/to generate that pre-bundled file.

    1.1.3.7 Wire Up

            Wire up the new link in the main menu to the newly added event handler method.

            One of the easier ways to do this is to open the view in the storyboard and right click on the new link. Select something such as the Touch Up Inside event, drag that to the storyboard and then select the created method from the list provided.

    1.1.4 TestYour Integration

            You have now done all the basic steps to integrate React Native with your current application. Now we will start the React Native packager to build the index.ios.bundle packager and the server running on localhost to serve it.

    1.1.5 AppTransport Security

            Apple has blocked implicit cleartext HTTP resource loading. So we need to add the following our project's Info.plist(or equivalent)file.

    1.1.6 运行Packager

    # From the root of your project, where the `node_modules` directory is located.

    $ npm start

    1.1.7 运行应用#

            If you are usingXcode or your favorite editor, build and run your native iOS application as normal. Alternatively, you can run the app from the command line using:

    # From the root of your project

    $ react-native run-ios

            In our sample application, you should see the link to the "High Scores" and then when you click on that you will see the rendering of your React Native component.

            Here is the native application home screen:

            Here is the React Native high score screen:

            If you are getting module resolution issues when running your application please see this GitHub issue for information and possible resolution.This comment seemed to be the latest possible resolution.

    1.1.8 Seethe Code#

            You can examine the code that added the React Native screen onGitHub.

    1.2 图片加载

    1.2.1 静态图片资源

            从0.14版本开始,React Native提供了一个统一的方式来管理iOS和Android应用中的图片。要往App中添加一个静态图片,只需把图片文件放在代码文件夹中某处,然后像下面这样去引用它:

    <Image source={require('./my-icon.png')} />

            图片文件的查找会和JS模块的查找方式一样。在上面的这个例子里,是哪个组件引用了这个图片,Packager就会去这个组件所在的文件夹下查找my-icon.png。并且,如果你有my-icon.ios.png和my-icon.android.png,Packager就会根据平台而选择不同的文件。

            你还可以使用@2x,@3x这样的文件名后缀,来为不同的屏幕精度提供图片。比如下面这样的代码结构:

    .

    ├── button.js

    └── img

       ├── check@2x.png

       └── check@3x.png

    并且button.js里有这样的代码:

    <Image source={require('./img/check.png')} />

            Packager会打包所有的图片并且依据屏幕精度提供对应的资源。譬如说,iPhone 5s会使用check@2x.png,而Nexus 5上则会使用check@3x.png。如果没有图片恰好满足屏幕分辨率,则会自动选中最接近的一个图片。

            注意:如果你添加图片的时候packager正在运行,则你需要重启packager以便能正确引入新添加的图片。

            这样会带来如下的一些好处:

        1、iOS和Android一致的文件系统。

        2、图片和JS代码处在相同的文件夹,这样组件就可以包含自己所用的图片而不用单独去设置。

        3、不需要全局命名。你不用再担心图片名字的冲突问题了。

        4、只有实际被用到(即被require)的图片才会被打包到你的app。

        5、现在在开发期间,增加和修改图片不需要重新编译了,只要和修改js代码一样刷新你的模拟器就可以了。

        6、与访问网络图片相比,Packager可以得知图片大小了,不需要在代码里再声明一遍尺寸。

        7、现在通过npm来分发组件或库可以包含图片了。

            注意:为了使新的图片资源机制正常工作,require中的图片名字必须是一个静态字符串。

    // 正确

    <Image source={require('./my-icon.png')} />

    // 错误

    var icon = this.props.active ? 'my-icon-active' : 'my-icon-inactive';

    <Image source={require('./' + icon + '.png')} />

    // 正确

    var icon = this.props.active ?require('./my-icon-active.png') : require('./my-icon-inactive.png');

    <Image source={icon} />

            本特性从0.14开始生效。请注意:新的资源系统依靠修改打包脚本来实现,react-native init创建的新工程已经包含了这些修改:XcodeGradle。如果你的工程是在0.13或者更早版本创建的,你可能需要自行添加对应的代码来支持新的图片资源系统。请参考文档升级版本文档中的升级操作说明。

    1.2.2 使用混合App的图片资源

            如果你在编写一个混合App(一部分UI使用React Native,而另一部分使用平台原生代码),也可以使用已经打包到App中的图片资源(通过Xcode的asset类目或者Android的drawable文件夹打包):

    <Image source={{uri: 'app_icon'}}style={{width: 40, height: 40}} />

            注意:这一做法并没有任何安全检查。你需要自己确保图片在应用中确实存在,而且还需要指定尺寸。

    1.2.3 网络图片

            很多要在App中显示的图片并不能在编译的时候获得,又或者有时候需要动态载入来减少打包后的二进制文件的大小。这些时候,与静态资源不同的是,你需要手动指定图片的尺寸。

    // 正确

    <Image source={{uri: 'https://facebook.github.io/react/img/logo_og.png'}} style={{width: 400, height: 400}} />

    // 错误

    <Image source={{uri: 'https://facebook.github.io/react/img/logo_og.png'}} />

    1.2.4 本地文件系统中的图片

            参考相册(CameraRoll)这个例子来看如何使用在Images.xcassets以外的本地资源。

    1.2.5 最合适的相册图片

            iOS会为同一张图片在相册中保存多个不同尺寸的副本。为了性能考虑,从这些副本中挑出最合适的尺寸显得尤为重要。对于一处200x200大小的缩略图,显然不应该选择最高质量的3264x2448大小的图片。如果恰好有匹配的尺寸,那么ReactNative会自动为你选好。如果没有,则会选择最接近的尺寸进行缩放,但也至少缩放到比所需尺寸大出50%,以使图片看起来仍然足够清晰。这一切过程都是自动完成的,所以你不用操心自己去完成这些繁琐且易错的代码。

    1.2.6 为什么不在所有情况下都自动指定尺寸呢?

            在浏览器中,如果你不给图片指定尺寸,那么浏览器会首先渲染一个0x0大小的元素占位,然后下载图片,在下载完成后再基于正确的尺寸来渲染图片。这样做的最大问题是UI会在图片加载的过程中上下跳动,使得用户体验非常糟糕。

            在React Native中我们有意避免了这一行为。如此一来开发者就需要做更多工作来提前知晓远程图片的尺寸(或宽高比),但我们相信这样可以带来更好的用户体验。然而,从已经打包好的应用资源文件中读取图片(使用require('image!x')语法)则无需指定尺寸,因为它们的尺寸在加载时就可以立刻知道。

            比如这样一个引用require('image!logo')的实际输出结果可能是:

    {"__packager_asset":true,"isStatic":true,"path":"/Users/react/HelloWorld/iOS/Images.xcassets/react.imageset/logo.png","uri":"logo","width":591,"height":573}

    1.2.7 资源属性是一个对象(object)#

            在React Native中,另一个值得一提的变动是我们把src属性改为了source属性,而且并不接受字符串,正确的值是一个带有uri属性的对象。

    <Image source={{uri: 'something.jpg'}} />

            深层次的考虑是,这样可以使我们在对象中添加一些元数据(metadata)。假设你在使用require('./my-icon.png'),那么我们就会在其中添加真实文件路径以及尺寸等信息(这只是举个例子,未来的版本中require的具体行为可能会变化)。此外这也是考虑了未来的扩展性,比如我们可能会加入精灵图(sprites)的支持:在输出{uri: ...}的基础上,我们可以进一步输出裁切信息{uri: ..., crop: {left: 10, top: 50, width: 20, height: 40}},这样理论上就可以在现有的代码中无缝支持精灵图的切分。

            对于开发者来说,则可以在其中标注一些有用的属性,例如图片的尺寸,这样可以使图片自己去计算将要显示的尺寸(而不必在样式中写死)。请在这一数据结构中自由发挥,存储你可能需要的任何图片相关的信息。

    1.2.8 通过嵌套来实现背景图片#

    return(

        <Image source={...}>

           <Text>Inside</Text>

        </Image>

    );

    1.2.9 在主线程外解码图片#

            图片解码有可能会需要超过一帧的时间。在web上这是页面掉帧的一大因素,因为解码是在主线程中完成的。然而在React Native中,图片解码则是在另一线程中完成的。在实际开发中,一般对图片还没下载完成时的场景都做了处理(添加loading等),而图片解码时显示的占位符只占用几帧时间,并不需要你改动代码去额外处理。

    1.3 RN调用Native模块(iOS)

            Native模块只是一个Objectve-C类,实现了RCTBridgeModule协议。ReactNative不会向JavaScript公开任何CalendarManager方法,而是通过RCT_EXPORT()方法向js端暴露方法。

    // CalendarManager.h

    #import "RCTBridgeModule.h"

    #import "RCTLog.h"

    @interface CalendarManager : NSObject<RCTBridgeModule>

    @end

    // CalendarManager.m

    @implementation CalendarManager

    - (void)addEventWithName:(NSString *)name location:(NSString*)location

    {

          RCT_EXPORT();

          RCTLogInfo(@"Pretending to create an event %@ at %@", name, location);

    }

    @end

            现在从你的JavaScript文件中,你可以像这样调用方法:

    var CalendarManager =require('NativeModules').CalendarManager;

    CalendarManager.addEventWithName('Birthday Party', '4 PrivetDrive, Surrey');

            注意,导出的方法名称是从Objective-C选择器的第一部分中生成的。有时它会产生一个非惯用的JavaScript名称(就像在我们的例子中的那个)。你可以通过为 RCT_EXPORT 提供一个可选参数更改名字,如RCT_EXPORT(addEvent)。 方法返回的类型应该是void。React Native 桥是异步的,所以向JavaScript传递结果的唯一方法是使用回调 或emitting事件(见下文)。

    1.3.1 支持传递的参数类型

            React Native支持多种参数类型,可以从JavaScript代码传递到native模块:

        · 字符串型(NSString )

        · 数字型(NSInteger,float,double,CGFloat,NSNumber )

        · 布尔型(BOOL,NSNumber)

        · 这个列表中任何类型的数组(NSArray)

        · 这个列表中任何类型的字符串键和值的映射(NSDictionary)

        · 函数(RCTResponseSenderBlock)

            注意:关于数组和映射 

            React Native没有为这些结构中值的类型提供任何担保。你的native模块可能期望一个字符串数组,但如果JavaScript调用你的包含数字和字符串数组的方法,你会得到带有NSNumber和NSString的NSArray。检查数组/映射值类型是开发人员的责任(助手方法见RCTConvert)。

    1.3.2 回调

            Native模块还支持一种特殊的参数——回调。在大多数情况下它是用来向JavaScript提供函数调用结果的。

    - (void)findEvents:(RCTResponseSenderBlock)callback

    {

        RCT_EXPORT();

        NSArray *events =...

        callback(@[[NSNull null], events]);

    }

    CalendarManager.findEvents((error, events) =>

    {

      if (error) {

        console.error(error);

      } else {

        this.setState({events: events});

      }

    })

            Native模块应该只调用它的回调一次。然而,它可以将回调作为ivar存储并稍后调用回调。这种模式通常用于包装需要委托的iOS的APIs。请看RCTAlertManager。

            如果你想向JavaScript传递error——如对象,使用RCTUtils.h的RCTMakeError。

    1.3.3 实现native模块

            React Native在一个单独的串行GCD队列中调用native模块方法,但这是一个实现细节,可能会改变。如果native模块需要调用main-thread-only iOS API,它应该在主队列安排操作:

    - (void) addEventWithName: (NSString *)name callback: (RCTResponseSenderBlock)callback

    {

        RCT_EXPORT(addEvent);

        dispatch_async(dispatch_get_main_queue(), ^{

            // Call iOS API on main thread

            ...

            // You can invoke callback from any thread/queue

            callback(@[...]);

        }); 

    }

    1.3.4 同步导出常量到JS端

            Native模块可以在运行时向JavaScript导出立即可用的常量。导出一些初始数据是有用的,否则这些初始数据需要往返的桥梁。

    - (NSDictionary *)constantsToExport

    {

        return @{@"firstDayOfTheWeek": @"Monday" };

    }

            JavaScript能够立即使用这些值:

    console.log(CalendarManager.firstDayOfTheWeek);

            注意,只有在初始化时常量才能被导出,所以如果你在运行时改变了 constantsToExport的值,它不会影响JavaScript环境。

    1.3.5 发送事件到JavaScript

            Native模块可以在不被直接调用的情况下向JavaScript发送事件信号。最简单的方法是使用eventDispatcher:

    #import "RCTBridge.h"

    #import "RCTEventDispatcher.h"

    @implementation CalendarManager

    @synthesize bridge = _bridge;

    - (void)calendarEventReminderReceived: (NSNotification*)notification

    {

         NSString* eventName = notification.userInfo[@"name"];

         [self.bridge.eventDispatcher sendAppEventWithName: @"EventReminder"];

    @end

    body:@{@"name": eventName}];

            JavaScript代码可以订阅这些事件:

    var subscription = DeviceEventEmitter.addListener(

      'EventReminder',

      (reminder) =>console.log(reminder.name)

    );

    ...

    // Don't forget to unsubscribe

    subscription.remove();

            更多的向JavaScript发送事件的例子,请看RCTLocationObserver。

    1.4 RN调用Native模块(Android)

            一个原生模块是一个通常继承ReactContextBaseJavaModule类的Java类,并且实现了JavaScript需要实现的方法。

    package com.facebook.react.modules.toast;

    import android.widget.Toast;

    import com.facebook.react.bridge.NativeModule;

    import com.facebook.react.bridge.ReactApplicationContext;

    import com.facebook.react.bridge.ReactContext;

    import com.facebook.react.bridge.ReactContextBaseJavaModule;

    import com.facebook.react.bridge.ReactMethod;

    import java.util.Map;

    public class ToastModule extends ReactContextBaseJavaModule

    {

      private static final StringDURATION_SHORT_KEY = "SHORT";

      private static final StringDURATION_LONG_KEY = "LONG";

      public ToastModule(ReactApplicationContext reactContext)

      {

        super(reactContext);

      }

      @Override

      public String getName() {

        return "ToastAndroid";

      }

     @Override

     public Map getConstants() {

        finalMap 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();

      }

    }

            ReactContextBaseJavaModule需要一个叫做getName的方法被实现。这个方法的目的就是返回在JavaScript里面表示这个类的叫做NativeModule的字符串的名字。在这里我们调用ToastAndroid因此我们可以在JavaScript里面使用React.NativeModules.ToastAndroid来得到它。

            一个可选的叫做getConstants的方法会将传递给JavaScript的常量返回。这个方法的实现并不是必须的,但是却对在JavaScript和Java中同步的预定义的关键字的值非常重要。

            给JavaScript暴露一个方法,一个Java方法需要使用@ReactMethod来注解。桥接方法的返回值类型总是void。ReactNative的桥接是异步的,因此将一个结果传递给JavaScript的唯一方式就是使用回调函数或者调用事件(见上面)。

    1.4.1 参数类型

            下面的参数类型是被使用@ReactMethod注解的方法支持的,并且它们直接对应JavaScript中对应的值。

    Boolean -> Bool

    Integer -> Number

    Double -> Number

    Float -> Number

    String -> String

    Callback -> function

    ReadableMap -> Object

    ReadableArray -> Array

    1.4.2 注册模块

            在使用Java的最后一步就是注册这个模块,这将在你的应用包中的createNativeModules发生。如果一个模块没有被注册,那么它在JavaScript是不可用的。

    class AnExampleReactPackage implements ReactPackage {

      ...

     @Override

     public List createNativeModules (ReactApplicationContext reactContext)

      {

        List modules = new ArrayList<>();

        modules.add(new ToastModule(reactContext));

        return modules;

      }

    }

            当包被创建的时候,它需要提供给ReactInstanceManager。可以看 UIExplorerActivity.java这个例子。当你初始化一个新工程的时候默认的包是 MainReactPackage.java。

    mReactInstanceManager = ReactInstanceManager.builder()

       .setApplication(getApplication())

       .setBundleAssetName("AnExampleApp.android.bundle")

       .setJSMainModuleName("Examples/AnExampleApp/AnExampleApp.android")

       .addPackage(new AnExampleReactPackage())

       .setUseDeveloperSupport(true)

      .setInitialLifecycleState(LifecycleState.RESUMED)

       .build();

            为了能让你更加方便的从JavaScript访问你的新功能的时候,通常会将原生模块包裹在一个JavaScript模块里面。这不是必须的,但是节省了你的类库的使用者每次都要pullNativeModules的不便。这个JavaScript文件也为你增加任何JavaScript端功能提供了方便。

    /**

      * @providesModule ToastAndroid

      */

     'use strict';

     /**

      * This exposes the native ToastAndroid moduleas a JS module. This has a function 'showText'

      * which takes the following parameters:

      *

      * 1. String message: A string with the textto toast

      * 2. int duration: The duration of the toast.May be ToastAndroid.SHORT or ToastAndroid.LONG

      */

     var { NativeModules } = require('react-native');

     module.exports = NativeModules.ToastAndroid;

            现在,在你的JavaScript文件里面你可以像下面这样调用方法:

    var ToastAndroid = require('ToastAndroid')

    ToastAndroid.show('Awesome',ToastAndroid.SHORT);

    // Note: Werequire ToastAndroid without any relative filepath because

    // of the@providesModule directive. Using @providesModule is optional.

    1.4.3 回调

            原生模块也提供了一种特殊的参数——回调。在大多数情况下这是给JavaScript返回结果使用的。

    public class UIManagerModule extends ReactContextBaseJavaModule{

      ...

      @ReactMethod

      public void measureLayout(

          int tag,

          int ancestorTag,

          Callback errorCallback,

          Callback successCallback) {

        try{

          measureLayout(tag, ancestorTag, mMeasureBuffer);

          float relativeX = PixelUtil.toDIPFromPixel(mMeasureBuffer[0]);

          float relativeY = PixelUtil.toDIPFromPixel(mMeasureBuffer[1]);

          float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]);

          float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]);

          successCallback.invoke(relativeX, relativeY, width, height);

        }catch (IllegalViewOperationException e) {

          errorCallback.invoke(e.getMessage());

    } }

    ...

            使用以下方法可以来访问在JavaScript里面可以使用:

    UIManager.measureLayout(

       100,

       100,

       (msg) => {

           console.log(msg);

       },

       (x, y, width, height) => {

           console.log(x + ':' + y + ':' + width +':' + height);

        } 

    );

            一个原生模块支持只调用一次它的回调。它可以保存这个回调,并且在以后调用。有一点需要强调的就是在原生方法完成之后这个回调并不是立即被调用的-请记住桥接通信是异步的,因此这个也在运行时循环里面。

    1.4.4 线程

            原生模块不应该设想有它们将在哪些线程里面被调用,因为目前的任务在以后改变是主要的。如果一个块调用是必须的,那么耗时操作将会被分配到间歇性的工作线程中,并且任何回调将会从这里开始。

    1.4.5 给JavaScript传递事件

            原生模块如果不需要立即被调用就可以给JavaScript发送事件。最简单的方式就是使用从ReactContext获得RCTDeviceEventEmitter,就像下面的代码片段:

      ...

     private void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) {

       reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)

           .emit(eventName, params);

     }

     ...

     WritableMap params= Arguments.createMap();

     ...

     sendEvent(reactContext, "keyboardWillShow", params);

            JavaScript模块在那时可以通过使用Subscribable的addListenerOn来注册并且接收事件。

    var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter');

    ...

     var ScrollResponderMixin = {

       mixins: [Subscribable.Mixin],

       componentWillMount: function() {

         ...

         this.addListenerOn(RCTDeviceEventEmitter,

            'keyboardWillShow',

            this.scrollResponderKeyboardWillShow);

            ... 

       },

       scrollResponderKeyboardWillShow: function(e:Event) {

           this.keyboardWillOpenTo = e;

           this.props.onKeyboardWillShow && this.props.onKeyboardWillShow(e);

       },

    1.5 调用Native UI组件(iOS)

            Native视图是通过RCTViewManager的子类创建和操做的。这些子类的功能与视图控制器很相似,但本质上它们是单例模式——桥只为每一个子类创建一个实例。它们将native视图提供给RCTUIManager,它会传回到native视图来设置和更新的必要的视图属性。RCTViewManager通常也是视图的代表,通过桥将事件发送回JavaScript。

            发送视图是很简单的:

        • 创建基本的子类。

        • 添加标记宏RCT_EXPORT_MODULE()。

        • 实现-(UIView *)view方法。

    //RCTMapManager.m

     #import <MapKit/MapKit.h>

     #import "RCTViewManager.h"

    @interface RCTMapManager : RCTViewManager

    @end

    @implementation RCTMapManager

    RCT_EXPORT_MODULE()

    -(UIView *)view

    {

        return [[MKMapView alloc] init];

    }

    @end

            然后你需要一些JavaScript使之成为有用的React组件:

    // MapView.js

     var { requireNativeComponent } = require('react-native');

     module.exports = requireNativeComponent('RCTMap', null);

            现在这是JavaScript中一个功能完整的native map视图组件了,包括pinch-zoom和其他native手势支持。但是我们还不能用JavaScript来真正的控制它。

    1.5.1 属性

            连接一些native属性。

    // RCTMapManager.m

    RCT_EXPORT_VIEW_PROPERTY(pitchEnabled, BOOL)

            注意我们显式的指定类型为BOOL——当谈到连接桥时,React Native使用hood下的RCTConvert来转换所有不同的数据类型,且错误的值会显示明显的 “RedBox”错误使你知道这里有ASAP问题。当一切进展顺利时,这个宏就会为你处理整个实现。

    //MyApp.js

    <MapView pitchEnabled={false} />

            但是这不是很好记录——为了知道哪些属性可用以及它们接收了什么值,你的新组件的客户端需要挖掘objective-C代码。为了更好的实现这一点,让我们做一个包装器组件并用React PropTypes记录接口:

    // MapView.js

    var React = require('react-native');

    var { requireNativeComponent } = React;

    class MapView extends React.Component {

        render() {

            return <RCTMap {...this.props}  />;

        } 

    }

    var RCTMap = requireNativeComponent('RCTMap',MapView);

    MapView.propTypes = {

    /**

     * When this propertyis set to `true` and a valid camera is associated

     * with the map, thecamera’s pitch angle is used to tilt the plane

     * of the map. Whenthis property is set to `false`, the camera’s pitch

     * angle is ignoredand the map is always displayed as if the user

     * is looking straightdown onto it.

    */

       pitchEnabled: React.PropTypes.bool,

     };

     module.exports = MapView;

            现在我们有一个很不错的已记录的包装器组件,它使用非常容易。注意我们为新的 MapView包装器组件将第二个参数从null改为MapView。这使得基础框架验证了propTypes匹配native工具来减少ObjC和JS代码之间的不匹配的可能。

            接下来,让我们添加更复杂的region工具。从添加native代码入手:

    // RCTMapManager.m

     RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap)

     {

           [view setRegion: json ? [RCTConvert MKCoordinateRegion: json] : defaultView.regionanimated: YES];

     }

            现在我们有一个MKCoordinateRegion类型,该类型需要一个转换函数,并且我们有自定义的代码,这样当我们从JS设置区域时,视图可以产生动画效果。还有一个defaultView,如果JS发送给我们一个null标记,我们使用它将属性重置回默认值。

            当然你可以为你的视图编写任何你想要的转换函数——下面是通过 RCTConvert的两类来实现MKCoordinateRegion的例子:

    @implementation RCTConvert(CoreLocation)

     RCT_CONVERTER(CLLocationDegrees, CLLocationDegrees, doubleValue);

     RCT_CONVERTER(CLLocationDistance, CLLocationDistance, doubleValue);

     +(CLLocationCoordinate2D)CLLocationCoordinate2D: (id)json

     {

       json = [self NSDictionary: json];

       return (CLLocationCoordinate2D){

           [self CLLocationDegrees: json[@"latitude"]],

           [self CLLocationDegrees: json[@"longitude"]]

       };

     }

     @end

     @implementation RCTConvert(MapKit)

     +(MKCoordinateSpan)MKCoordinateSpan: (id)json

     {

         json = [self NSDictionary: json];

         return (MKCoordinateSpan){

             [self CLLocationDegrees: json[@"latitudeDelta"]],

             [self CLLocationDegrees: json[@"longitudeDelta"]]

         };

     }

     +(MKCoordinateRegion)MKCoordinateRegion: (id)json

     {

       return (MKCoordinateRegion){

         [self CLLocationCoordinate2D: json],

         [self MKCoordinateSpan: json]

       };

    }

            这些转换函数是为了安全地处理任何JSON而设计的,当出现丢失的键或开发人员错误操作时,JS可能向它们抛出“RedBox”错误并返回标准的初始化值。

            为完成对region工具的支持,我们需要把它记录到propTypes中(否则我们将得到一个错误,即native工具没有被记录),然后我们就可以按照设置其他工具的方式来设置它:

    // MapView.js

     MapView.propTypes = {

       pitchEnabled: React.PropTypes.bool,

        region: React.PropTypes.shape({

          latitude: React.PropTypes.number.isRequired,

          longitude: React.PropTypes.number.isRequired,

          latitudeDelta: React.PropTypes.number.isRequired,

          longitudeDelta: React.PropTypes.number.isRequired,

       }),

     };

     // MyApp.js

       render() {

         var region = {

           latitude: 37.48,

           longitude: -122.16,

           latitudeDelta: 0.1,

           longitudeDelta: 0.1,

       };

       return <MapView region={region} />;

    }

            在这里你可以看到该区域的形状在JS文档中是显式的——理想情况下我们可以生成一些这方面的东西,但是这没有实现。

    1.5.2 事件

            让js端接收用户事件,关键是要使RCTMapManager成为它发送的所有视图的代表,并把事件通过事件调度器发送给JS。

    // RCTMapManager.m

     #import "RCTMapManager.h"

     #import <MapKit/MapKit.h>

     #import "RCTBridge.h"

     #import "RCTEventDispatcher.h"

     #import "UIView+React.h"

    @interface RCTMapManager()

    @end

    @implementation RCTMapManager

    RCT_EXPORT_MODULE()

    - (UIView *)view

    {

      MKMapView *map = [[MKMapView alloc] init];

      map.delegate = self;

      return map;

    }

      #pragma mark MKMapViewDelegate

     -(void)mapView: (RCTMap *)mapView regionDidChangeAnimated: (BOOL) animated

     {

       MKCoordinateRegion region = mapView.region;

       NSDictionary *event = @{

         @"target": mapView.reactTag,

         @"region": @{

           @"latitude": @(region.center.latitude),

           @"longitude": @(region.center.longitude),

           @"latitudeDelta": @(region.span.latitudeDelta),

           @"longitudeDelta": @(region.span.longitudeDelta),

        }

       };

       [self.bridge.eventDispatcher sendInputEventWithName: @"topChange" body: event];

    }

    // MapView.js

    class MapView extends React.Component {

       constructor() {

         this._onChange = this._onChange.bind(this);

       }

       _onChange(event: Event) {

         if(!this.props.onRegionChange) {

           return;

         }

         this.props.onRegionChange(event.nativeEvent.region);

       }

       render() {

         return ;

       }

    }

    MapView.propTypes = {

       onRegionChange: React.PropTypes.func,

       ...

    };

    1.5.3 样式

            由于我们所有的nativereact视图是UIView的子类,大多数样式属性会像你预想的一样存在不足。一些组件需要默认的样式,例如UIDatePicker,大小固定。为了达到预期的效果,默认样式对布局算法来说是非常重要的,但是我们也希望在使用组件时能够覆盖默认的样式。DatePickerIOS通过包装一个额外的视图中的native组件实现这一功能,该额外的视图具有灵活的样式设计,并在内部native组件中使用一个固定的样式(用从native传递的常量生成):

    // DatePickerIOS.ios.js

    var RCTDatePickerIOSConsts = require   ('NativeModules').UIManager.RCTDatePicker.Constants;

    ...

      render: function() {

        return (

            <View style={this.props.style}>

                <RCTDatePickerIOS ref={DATEPICKER} style={styles.rkDatePickerIOS} 

                    ...

                />

            </View>

        );

      }

    });

    var styles = StyleSheet.create({

        rkDatePickerIOS: {

            height: RCTDatePickerIOSConsts.ComponentHeight,

            width: RCTDatePickerIOSConsts.ComponentWidth,

        },

    });

    RCTDatePickerIOSConsts常量是通过抓取native组件的实际框架从native中导出的,如下所示:

    // RCTDatePickerManager.m

    - (NSDictionary *)constantsToExport

    {

        UIDatePicker *dp =[[UIDatePicker alloc] init];

        [dp layoutIfNeeded];

        return @{

           @"ComponentHeight": @(CGRectGetHeight(dp.frame)),

           @"ComponentWidth": @(CGRectGetWidth(dp.frame)),

           @"DatePickerModes": @{

               @"time":@(UIDatePickerModeTime),

               @"date": @(UIDatePickerModeDate),

               @"datetime": @(UIDatePickerModeDateAndTime),

           }

       };

    }

            本指南涵盖了衔接自定义native组件的许多方面,但有你可能有更多需要考虑的地方,如自定义hooks来插入 和布局子视图。如果你想了解更多,请在源代码中查看实际的RCTMapManager和其他组件。

    1.6 调用Native UI组件(Android)

            本地原生视图是由扩展ViewManager或者更普遍的SimpleViewManager所创建和操纵的。在大部分情况下SimpleViewManager是很方便的,因为它适用于普遍的属性,比如背景颜色、不透明度和Flexbox布局。不过也有其他例子,当您在使用FrameLayout进行包装组件的时候,那您需要使用ViewManager,比如ProgressBar。

            这些子类在本质上是单例— —每个子类之中只有一个实例是通过这个桥接器创建的。他们将本地视图传递到了NativeViewHierarchyManager之中,这代表回到了通过使用它们原始的方法来设置并更新这些必要的视图 的属性。ViewManagers通常也是这些视图的代表,它通过桥接器将事件发送回JavaScript。

            传递一个视图很简单:

        1. 创建ViewManager子类;

        2. 使用@UIProp注释视图属性;

        3. 执行createViewInstance;

        4. 执行updateView方法;

        5. 在应用程序软件包中的createViewManagers中注册管理器;

        6. 执行JavaScript模块

    1.6.1 封装一个原生视图

    1.6.1.1 1.创建ViewManager子类

            在本示例中,我们通过继承ReactImageView类型的SimpleViewManager来创建的视图管理器类ReactImageManager。它是由管理器管理的对象类型,这将成为一个本地视图。通过getName返回的名字将被用来从Javascript中引用本地视图类型。

    ...

    public class ReactImageManager extends SimpleViewManager

    {

        public static final StringREACT_CLASS = "RCTImageView";

        @Override

        public String getName() {

            return REACT_CLASS;

        }

    1.6.1.2 2.注释视图属性

            在Java中使用@UIProp来注释需要被反映出来的属性。目前支持的类型有 BOOLEAN, NUMBER, STRING, MAP和ARRAY。每个属性都被声明为公共静态最终字符串常量,并且给它们分配的值在JavaScript中都会成为属性的名称。

    @UIProp (UIProp.Type.STRING)

    public static final String PROP_SRC = "src";

    @UIProp(UIProp.Type.NUMBER)

    public static final String PROP_BORDER_RADIUS = "borderRadius";

    @UIProp(UIProp.Type.STRING)

    public static final String PROP_RESIZE_MODE = ViewProps.RESIZE_MODE;

    1.6.1.3 3.执行createViewInstance方法

            使用CreateViewInstance方法来创建视图,视图应将其自身初始化到默认状态,然后任何属性都会通过后续调用updateView来进行设置。

    @Override

    public ReactImageView createViewInstance(ThemedReactContext context) {

        return newReactImageView(context, Fresco.newDraweeControllerBuilder(), mCallerContext);

    }

    1.6.1.4 4.执行updateView方法

            和iOS中有些不同的是在Android中,不是通过自动调用setter方法来给一个视图的属性进行赋值;对于Android而言,你需要通过您的ViewManager中的updateView方法手动调用setter。从Map中提取出来值,并且传递给视图实例。它是通过updateView和视图类的组合来检查属性的有效性,并采取相应的行动。

    @Override

    public void updateView(final ReactImageView view, final CatalystStylesDiffMap props) {

        super.updateView(view, props);

        if (props.hasKey(PROP_RESIZE_MODE)) {

            view.setScaleType(ImageResizeMode.toScaleType(props.getString(PROP_RESIZE_MODE)));

        }

        if(props.hasKey(PROP_SRC)) {

            view.setSource(props.getString(PROP_SRC));

        }

        if (props.hasKey(PROP_BORDER_RADIUS)) {

             view.setBorderRadius(props.getFloat(PROP_BORDER_RADIUS, 0.0f));

        }

        view.maybeUpdateView();

      }

    }

    1.6.1.5 5.注册ViewManager

            在Java中的最后一步是通过应用程序包的成员函数createViewManagers在应用程序中注册ViewManager,这恰巧和Native Modules有些相似。

    @Override

    public List createViewManagers(ReactApplicationContext reactContext) {

        return Arrays.asList(

            newReactImageManager()

        );

    }

    1.6.1.6 6.执行JavaScript模块

            最后一步就是创建JavaScript模块来为您的新视图的用户定义Java和JavaScript之间的连接层。大量工作都是由Java和JavaScript中的React代码所完成,那么所有留给你的工作就是去描述propTypes。

    //ImageView.js

    var {requireNativeComponent } = require('react-native');

    var iface ={

        name: 'ImageView',

        propTypes: {

            src: PropTypes.string,

            borderRadius: PropTypes.number,

            resizeMode: PropTypes.oneOf(['cover','contain', 'stretch']),

        },

    };

    module.exports= requireNativeComponent('RCTImageView',iface);

            requireNativeComponent 通常具有两个参数,第一个是本地原生视图的名称,第二个是描述组件接口的对象。组件接口应该声明一个友好的名称在调试消息中使用,并且必须声明本地视图所反映的propTypes。PropTypes用于检查用户使用本地视图的有效性。

    1.6.2 事件

            当本地事件发生的时候,本地代码应该把事件传递给视图中的JavaScript代表,并且这两个视图都与getId()方法返回的值相连接。

    class MyCustomViewextends View {

       ...

       public void onReceiveNativeEvent() {

          WritableMap event = Arguments.createMap();

          event.putString("message", "MyMessage");

          ReactContext reactContext= (ReactContext)getContext();

          reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(

              getId(),

              "topChange",

              event);

          }

    }

    // MyCustomView.js

    class MyCustomView extends React.Component {

      constructor() {

          this._onChange = this._onChange.bind(this);

      }

      _onChange(event: Event) {

          if (!this.props.onChange) {

              return;

          }

          this.props.onChange(event.nativeEvent.message);

      }

      render() {

          return ;

      }

    }

    MyCustomView.propTypes= {

      /**

       * Callback that is called continuously whenthe user is dragging the map.

       */

      onChange: React.PropTypes.func,

      ...

    };

    1.7 链接库 (IOS)

    linking-libraries

    步骤 2

            点击你的主项目文件(代表.xcodeproj的文件)选择Build Phases,从你正在导入Link Binary With Libraries的库中的Products文件夹中,拖动静态库。

    linking-libraries

        步骤 3

            不是每个库都需要这一步,你需要考虑的是:

            我在编译时需要知道库的内容吗?

            这意味着,你是在native网站中使用库还是只是在JavaScript中使用库呢?如果你只是在JavaScript中使用它,这样做很好!

            对于我们用除了PushNotificationIOS和LinkingIOS的React Native推出的库来说,这个步骤是不必要的。

            以PushNotificationIOS为例,每次你收到一个新的pushnotifiation,你必须从AppDelegate的库中调用方法。

            为此,我们需要知道库的头。为了实现这个,你必须在你的项目文件中选择Build Settings,搜索HeaderSearch Paths。你应该包括通往库的路径(如果有相关文件的子目录,记得使它recursive,如例子中的React)。

    linking-libraries

    1.8 调试ReactNative应用

            访问应用程序内开发者菜单:

        1.在iOS中摇动设备或在虚拟机里按组合键control + ? + z.

        2.在Android中摇动设备或按硬件菜单按钮(旧的设备中以及大多数虚拟机中都有效,例如, 在 genymotio n 中,你可以按组合键? + m来模拟点击硬件菜单按钮)

    1.8.1 重加载

            选择Reload (或者在iOS虚拟机中按组合键? + r )将会重新加载作用于你的应用程序中的JavaScript。 如果你增加了新的资源(例如,将一幅图添加到iOS中的Images.xcassets,或Android中的文件夹)或者对任何本地代码进行修改(iOS中的Objective-C/Swift代码或Android中的Java/C++代 码),你将需要重新生成该应用程序以使更改生效。

    1.8.2 Chrome开发工具

            在Chrome中调试JavaScript代码,在开发者菜单选择Debug in Chrome。将打开一个新的标签http://localhost:8081/debugger-ui。在Chrome中,按下组合键? + option + i或选择View→Developer→ Developer Tools切换开发工 具控制台。 启用捕获异常时暂停以获得更佳的调试体验。在实际设备上进行调试:

        1、在iOS中,-打开文件RCTWebSocketExecutor.m并更改localhost为你的电脑IP地址。摇动设备打开开发菜单,选择启动调试。

        2、在Android中,如果你正在运行通过USB连接的Android5.0+设备,您可以使用adb命令行工具来从设备到您的计算机设置端口转发。运行:adbreverse 8081 8081 (参阅此链接以获得adb命令详情)。或者,你可以打开设备上开发菜单并选择开发设置,然后为设备设置更新调试服务器主机到您的计算机的IP地址。

    1.8.3 React开发工具(可选)

            安装React Developer Tools作为谷歌浏览器的扩展。这将允许您通过React在开发工具中导航组件层次结构

    (更多详情参阅facebook/react-devtools)。

    1.8.4 Live Reload

            这个选项可触发JS在连接设备/模拟器上自动刷新。启用此选项:

        1、在iOS中,通过开发者菜单选择Enable Live Reload,当JavaScript有任何改动时,应用程序会自动重新加载。

        2、在Android中,启动开发菜单(页0),进入Dev Settings并选择Autoreload on JS change选项。

    1.8.5 FPS (每秒帧数)显示器

            在0.5.0-rc以及更高的版本,为了帮助调试性能问题,你可以在开发者菜单启用FPS图形叠置。

    1.9 测试

    1.9.1 运行测试和贡献

            React Native回购有几个你可以运行的测试,来验证你没有用PR引起拟合。这些测试是用Travis持续集成系统运行的,并自动的向你的PR发布结果。你也可以在IntegrationTest和在Xcode中的UIExplorer应用中,使用cmd+U本地运行。您可以通过在命令行的 运行jest测试。但是我们目前还没有很大的测试覆盖率,所以大多数的变化仍将需要大量手工验证,但如果你想帮助我们提高我们的测试覆盖率,我们是非常欢迎的!

    1.9.2 Jest测试

            Jest测试是JS-only测试,运行在节点命令行上。测试位于它们测试的文件 目录中,还有一个对不是位于故障隔离和最大速度测试下的积极模拟功能的强调。你可以用来自react-native根的运行现有的React Native jest测试,并且我们鼓励你为你想做出贡献的任何组件添加你自己的测试。基本示例请看getImageSource-test.js。

    1.10 在设备上运行

    1.10.1 从设备访问开发服务器

            你可以使用开发服务器在设备中快速迭代。要做到这一点,你的笔记本电脑和你的手机必须处于相同的wifi网络中。

        1.打开iOS/AppDelegate.m;

        2.更改URL中的IP,从Localhost改成你的笔记本电脑的IP;

        3.在Xcode中,选择你的手机作为构建目标,并按“构建和运行”;

    1.10.2 使用离线包

            你也可以将应用程序本身的所有JavaScript代码打包。这样你可以在开发服务器没有运行时测试它,并把应用程序提交到到AppStore。

        1.打开iOS / AppDelegate.m

        2.遵循“选项2”的说明:

            • 取消 jsCodeLocation = [[NSBundle mainBundle]...

            • 在你应用程序的根目录的终端运行给定curl命令Packager支持几个选项:

            •dev (默认的true)——设置了__DEV__变量的值。当是true时,它会打开一堆有用的警告。对于产 品,它建议使用dev = false。

            •minify (默认的false)——只要不通过UglifyJS传输JS代码。

    1.10.3 故障排除

            如果curl命令失败,确保packager在运行。也尝试在它的结尾添加 ——ipv4标志。

            如果你刚刚开始了你的项目,main.jsbundle可能不会被包含到Xcode项目中。要想添加它,右键单击你的项目目录,然后单击“添加文件......”——选择生成的main.jsbundle文件。

    1.11 在设备上运行(Android)

    1.11.1 USB调试

            在设备上开发最简单的方式就是使用USB调试。首先请确保你有USB debugging enabled on your device。一旦在设备上调试是被允许的,在连接的设备上你可以以同样的方式在模拟器里面使用来安装并且运行你的React Native应用。

    1.11.2 从设备上获取开发服务器

            你也可以在设备上使用开发服务器快速集成。照着下面的描述的步骤之一来给你的设备构建在你的电脑上运行的开发者服务器。

        注意

          现在绝大多数的安卓设备没有一个我们来触发开发者模式的硬件按钮键。如果是那样的话,你可以通过摇动来开启开发者模式(重新加载,调试等)。

    1.11.3 使用adb反转

            请注意这个选项只支持运行在安卓5.0+ (API 21)上面的设备。

            使用USB将你的设备连接,并开启调试模式(可以看看上面如何在你的设备上面允许USB调试模式)。

        1. 运行adb reverse tcp:8081 tcp:8081;

        2. 你可以使用Reload JS和其他开发者参数,而不需要额外的配置;

            通过Wi-Fi来配置设备并且连接上你的开发者服务器,要做到这一点,你的电脑和你的手机必须在同一个wifi网络下。

        1. 打开震动菜单(摇动设备);

        2. 前往 Dev Settings;

        3. 前往 Debug server host for device;

        4. 输入该设备的IP和 Reload JS;

    1.12 JavaScript环境

    1.12.1 JavaScript运行时间

            当使用React Native时,你将会在两个环境中运行JavaScript代码:

        · 在模拟器和电话中:JavaScriptCore 是JavaScript的引擎,能够驱动Safari和web视图。由于在iOS应用程序中没有可写的可执行的内存,它不用JIT运行。

        · 使用Chrome调试时,它在Chrome本身中运行所有JavaScript代码,并且通过WebSocket与Objectiv e-C交互。所以你正在使用 V8。

            虽然两个环境很相似,但是你可能会以触及一些矛盾而结束。将来我们很可能去尝试其他JS引擎,所以最好避免依赖任何运行时的细节。

    1.12.2 JavaScript转换

           ReactNative附带许多JavaScript转换,使编写代码更愉快。如果你好奇的话,你可以查看所有这些转换的实现。这是完整的列表:

    ES5

        • 关键字: promise.catch(function(){ });

    ES6

        • 箭头函数: this.setState({pressed: true})}

        • 调用传播: Math.max(...array);

        • 类: class Cextends React.Component { render() { return ; } }

        • 解构: var{isActive, style} = this.props;

        • 迭代: for (var element of array) { }

        • 计算属性: var key ='abc'; var obj = {[key]: 10};

        • 对象Consise方法:var obj = { method() { return 10; } };

        • 对象short表示法:var name = 'vjeux'; var obj = { name };

        • 其他参数: function(type, ...args) { }

    1.13 已知Issues

    1.13.1 使用透明度为0来覆视图不能被点击

            在iOS和Android之间使用透明度为0来处理视图有一个明显的差异。虽然在iOS上面允许这些视图被点击 并且在这些视图下面的视图也将会接收到触摸输入,但是在Android上面这个触摸输入则会被阻塞。这一点可以 用下面的一个只能在iOS上面点击的例子来演示。

    <View style={{flex: 1}}>

        <TouchableOpacity onPress={() => alert('hi!')}>

            <Text>HELLO!</Text>

        </TouchableOpacity>

        <View style={{

          position:'absolute',

          top: 0,

          left: 0,

          bottom: 0,

          right: 0,

          opacity: 0}}

        />

    </View>

    1.13.2 在Android上面的单独布局节点

            在React Native的安卓版本一个优化的特征就是对于只能贡献布局的视图不能有原生视图,只有它们的布局属 性能够传递到它们的子视图。这个优化是为了给更深层次的ReactNative视图层提供支持,因此默认是开启 的。如果你需要一个视图被呈现,或者间歇性的测试检测一个视图是不是只是布局,关闭这个行为就很有必要 了。为了做到这一点,你只需要设置collapsable为false即可:

    <View collapsable={false}>

        ...

    </View>

    1.14 异步存储

            异步存储是一个简单的、异步的、持久的、全局的、键-值存储系统。它应该会代替本地存储被使用。由于异步存储是全局性的,建议您在异步存储之上使用抽象体,而不是对任何轻微用法直接使用异步存储。

            在本地iOS实现上JS代码是一个简单的外观模式,用来提供一个清晰的JS API,真正的错误对象,和简单的 非多元化功能。每个方法返回一个Promise对象。

    1.14.1 方法

    static **getItem**(key: string, callback: (error: ?Error, result: ?string) => void)

           如果有任何一个错误,获取key并传递callback的结果,返回一个Promise对象。

    static **setItem**(key: string, value: string, callback:?(error: ?Error) => void)

            如果有任何一个错误,获取key并在结束时调用callback函数,返回一个Promise对象。

    static **removeItem**(key: string, callback: ?(error: ?Error)=> void)

            返回一个Promise对象。

    static **mergeItem**(key: string, value: string, callback:?(error: ?Error) => void)

            将现有值与输入值进行合并,假设它们是stringifiedjson,返回一个Promise对象。 所有本地实现不支持。

    static **clear**(callback: ?(error: ?Error) => void)

            为所有客户、函数库等清除所有的异步存储。你可能不想调用这个-使用removeItem或者multiRemove来清除只属于你的键值。返回一个Promise对象。

    static **getAllKeys**(callback: (error: ?Error) => void)

            为调用者、函数库等获取系统已知的所有键值。返回一个Promise对象。

    static **multiGet**(keys: Array, callback:(errors: ?Array, result: ?Array>) =>void)

            multiGet利用一个键值对的数组调用回调函数来获取multiSet的输入格式。返回一个`Promise`对象。

            multiGet(['k1', 'k2'], cb) -> cb([['k1', 'val1'], ['k2','val2']])

    static **multiSet**(keyValuePairs:Array>, callback: ?(errors: ?Array)=> void)

            multiSet和multiMerge利用键值对的数组匹配multiGet的输出。返回一个Promise对象。例如, multiSet([['k1','val1'], ['k2', 'val2']], cb);

    static **multiRemove**(keys: Array, callback:?(errors: ?Array) => void)

            删除键值数组中所有的键值。返回一个Promise对象。

    static **multiMerge**(keyValuePairs: Array>,callback: ?(errors: ?Array)=> void)

            将现有值与输入值进行合并,假设它们是stringified json,返回一个Promise对象。 所有本地实现不支持。

    1.15 iOS链接

            LinkingIOS给你提供了一个通用接口,用来连接接收和发送应用程序的链接。

    1.15.1 基本用法

        处理深度链接

            如果你的应用程序是从一个外部链接启动的,并且这个外部链接是注册到你的应用程序里的,那么你就可以利用任意你想要的组件去访问并且处理它。

    componentDidMount() {

        var url = LinkingIOS.popInitialURL();

    }

            在你的应用程序运行期间,如果你也想监听传入应用程序的链接,那么你需要将以下几行添加到你的*AppDelegate.m

    - (BOOL) application:(UIApplication *)applicationopenURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication

        return [RCTLinkingManager application: application openURL: url sourceApplication: sourceApplication annotation: annotation

    }

            那么,在你的React组件中,你可以监听LinkingIOS上的事件,如下所示:

    componentDidMount() {

        LinkingIOS.addEventListener('url', this._handleOpenURL);

    },

    componentWillUnmount() {

        LinkingIOS.removeEventListener('url', this._handleOpenURL);

    },

    _handleOpenURL(event) {

        console.log(event.url);

    }

        触发应用程序链接

            为了触发一个应用程序的链接(浏览器,电子邮件,或者自定义模式),你需要调用

    LinkingIOS.openURL(url)

            如果你想要检查一个已经安装的应用程序是否可以提前处理一个给定的链接,你可以调用

         LinkingIOS.canOpenURL(url,(supported) => {

           if (!supported){

                AlertIOS.alert('Can\'t handle url: ' + url);

           } else {

                LinkingIOS.openURL(url);

           }

    });

    1.15.2 方法

    static addEventListener(type: string, handler: Function)

            通过监听事件类型和提供处理程序,将一个处理程序添加到LinkingIOS changes

    static removeEventListener(type: string, handler: Function)

            通过传递事件类型和处理程序,删除一个处理程序

    static openURL(url: string)

            尝试通过任意已经安装的应用程序打开给定的url

    static canOpenURL(url: string, callback: Function)

            决定一个已经安装的应用程序是否可以处理一个给定的url,该方法中回调函数将被调用,并且仅通过一个bool supported的参数。

    static popInitialURL()

            如果应用程序启动是通过一个应用程序链接触发的,那么它将弹出这个链接的url,否则它将返回null。

    1.16 全景响应器

            PanResponder将几个触发调节成一个单一的触发动作。该方法可以使单一触发动作对额外的触发具有弹性,可以用来识别简单的多点触发动作。

            它为响应处理程序提供了一个可预测包,这个相应处理程序是由动作应答系统提供的。对每一个处理程序,在正常事件旁提供了一个新的gestureState对象。 一个gestureState对象有以下属性:

        1、sateID -gestureState的ID-在屏幕上保持至少一个触发动作的时间

        2、moveX-最近动态触发的最新的屏幕坐标

        3、x0-应答器横向的屏幕坐标

        4、y0-应答器纵向的屏幕坐标

        5、dx-触发开始后累积的横向动作距离

        6、dy-触发开始后累积的纵向动作距离

        7、vx-当前手势的横向速度

        8、vy-当前手势的纵向速度

        9、numberActiveTouch-屏幕上当前触发的数量

    1.16.1 基本用法

     componentWillMount: function() {

        this._panGesture =PanResponder.create({

             // Ask to be the responder:

             onStartShouldSetPanResponder: (evt, gestureState) => true,

             onStartShouldSetPanResponderCapture: (evt, gestureState) => true,

             onMoveShouldSetPanResponder: (evt, gestureState) => true,

             onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,

             onPanResponderGrant: (evt, gestureState) => {

             // The guesture has started. Show visual feedback so the user knows

            // what ishappening!

            // gestureState.{x,y}0 will be set tozero now

          },

         onPanResponderMove: (evt, gestureState) => {

             // The most recent move distance is gestureState.move{X,Y}

             // The accumulated gesture distance since becoming responder is

             // gestureState.d{x, y}

         },

         onResponderTerminationRequest: (evt, gestureState) => true,

              onPanResponderRelease: (evt, gestureState) => {

                  // The user has released all touches while this view is the

                  // responder.This typically means a gesture has succeeded

               },

              onPanResponderTerminate: (evt, gestureState) => {

                 //Another component has become the responder, so this gesture

                 // should be cancelled

              }, 

         });

     },

     render: function() {

             return (

                <View {...this._panResponder.panHandlers} />

            ); 

    },

    1.16.2 方法

    static create(config: object)

        · onMoveShouldSetPanResponder: (e, gestureState)=> {...}

        · onMoveShouldSetPanResponderCapture: (e, gestureState) => {...}

        · onStartShouldSetPanResponder: (e, gestureState)=> {...}

        · onStartShouldSetPanResponderCapture: (e, gestureState) => {...}

        · onPanResponderReject: (e, gestureState) =>{...}

        · onPanResponderGrant: (e, gestureState) =>{...}

        · onPanResponderStart: (e, gestureState) =>{...}

        · onPanResponderEnd: (e, gestureState) => {...}

        · onPanResponderRelease: (e, gestureState) =>{...}

        · onPanResponderMove: (e, gestureState) =>{...}

        · onPanResponderTerminate: (e, gestureState) =>{...}

        · onPanResponderTerminationRequest: (e, gestureState) => {...}

            一般来说,对于那些捕获的等价事件,我们在捕获阶段更新一次gestureState,并且也可以在冒泡阶段使用。在onStartShould*回调时需要注意一点。在对节点的捕获/冒泡阶段的开始/结束事件中,它们只对更新后的g estureState做出反应。一旦节点成为应答器,你可以依靠每一个被动作和gestureState处理后相应更新的开始/结束事件。(numberActiveTouches)可能不完全准确,除非你是应答器。

    2 参考链接

    React Native之调用安卓原生控件

    http://blog.csdn.net/jj120522/article/details/51968278

    React-Native之Android:原生界面与React界面的相互调用

    http://www.jianshu.com/p/f1b265e80317

    react-native调用原生模块详解

    http://blog.csdn.net/woaini705/article/details/50899946

    使用React-Native Code push热更新 增量更新 动态修复bug移动开发

    http://www.jianshu.com/p/ec8d64681e53

    React Native官方文档中文版

    http://wiki.jikexueyuan.com/project/react-native/native-ui-components.html

    React中文版

    http://wiki.jikexueyuan.com/project/react/

    React Native中文网

    http://reactnative.cn

    React Native中调用原生android模块Toast例子及说明

    http://www.tuicool.com/articles/ayyQbyz

    React Native教程第一部分:Hello, React

    http://www.tuicool.com/articles/MJZ3ym

    相关文章

      网友评论

        本文标题:【Hybrid开发高级系列】ReactNative(五) ——

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