十四、与 React Native 协作

本章将介绍以下配方:

  • 创建第一个 React 本机应用
  • 使用 React Native 创建待办事项列表
  • 实现 React 导航 V2

介绍

React Native 是一个使用 JavaScript 和 React 构建移动应用的框架。许多人认为,使用 React Native,您可以制作一些“移动 web 应用”或“混合应用”(如 Ionic、PhoneGap 或 Sencha),但您可以构建一个本机应用,因为 React Native 将您的 React 代码转换为适用于 Android 的 Java 或适用于 iOS 应用的 Objective-C。React Native 使用大多数 React 概念,例如组件、道具、状态和生命周期方法。

React Native的优点:

  • 您只需编写一次代码,就可以获得两个本机应用(Android 和 iOS)
  • 您不需要有 Java、Objective-C 或 Swift 方面的经验
  • 更快的发展
  • 麻省理工学院许可证(开源)

对 Windows的要求:

  • 安卓工作室
  • Android SDK(>=7.0 牛轧糖)
  • 安卓 AVD

Mac的要求:

  • XCode(>=9)
  • 模拟机

创建第一个 React 本机应用

在此配方中,我们将构建一个 React-Native 应用,并了解 React 和 React-Native 之间的主要区别。

准备

要创建新的 React 本机应用,我们需要安装react-native-cli包:

 npm install -g react-native-cli

怎么做。。。

现在,要创建我们的第一个应用:

  1. 让我们使用以下命令执行此操作:
    react-native init MyFirstReactNativeApp
  1. 在构建 React Native 应用之后,我们需要安装 Watchman,这是 React Native 所需的文件监视服务。要安装它,请转到https://facebook.github.io/watchman/docs/install.html 下载操作系统(Windows、Mac 或 Linux)的最新版本。
  2. 在这种情况下,我们将使用自制软件为 Mac 安装它。如果您没有自制软件,可以使用以下命令安装:
    /usr/bin/ruby -e "$(curl -fsSL 
  https://raw.githubusercontent.com/Homebrew/install/master/install)"
  1. 要安装 Watchman,您需要运行:
    brew update 
    brew install watchman
  1. 要启动 React Native 项目,我们需要使用:
    react-native start
  1. 如果一切正常,您应该看到:

Sometimes you can get errors from Watchman, for example, Watchman error: too many pending cache jobs. Make sure watchman is running for this project.

If you get that error or another, you have to uninstall Watchman by doing:

brew unlink watchman

然后使用以下方法重新安装:

brew update && brew upgrade

brew install watchman

  1. 打开一个新的终端(Cmd+T并运行此命令(取决于您要使用的设备):
    react-native run-ios 
    or
    react-native run-android
  1. 如果没有错误,您应该看到模拟器运行默认应用:

现在,我们已经运行了应用,让我们打开代码并稍作修改:

  1. 更改App.js文件:
  ...
  export default class App extends Component<Props> {
    render() {
      return (
        <View style={styles.container}>
          <Text style={styles.welcome}>
 This is my first React Native App!          </Text>
          <Text style={styles.instructions}>
            To get started, edit App.js
          </Text>
          <Text style={styles.instructions}>{instructions}</Text>
        </View>
      );
    }
  }
  ...

File: App.js

  1. 如果您再次进入模拟器,则需要按Cmd+R重新加载应用,以查看反映的新更改:

  1. 您可能想知道是否有一种方法可以自动重新加载,而不是手动执行此过程,当然,还有一种方法可以启用实时重新加载选项;您需要按Cmd+D打开开发菜单,然后选择启用实时重新加载选项:

  1. 另一个令人兴奋的选项是远程调试 JS。如果您单击该按钮,它将自动打开一个 Chrome 选项卡,在那里我们可以看到我们使用console.log添加到应用中的日志。例如,如果我在我的渲染方法中添加了console.log('==== Debugging my First React Native App! ====');,我应该看到如下所示:

  1. 让我们回到代码。也许你对你在App.js中看到的代码有点困惑,因为你没有看到<div>标记,或者更糟糕的是,样式是像对象一样创建的,而不是像我们在 React 中那样使用 CSS 文件。我有一些好消息和一些坏消息;坏消息是 React Native 不像 React 那样支持 CSS 和 JSX/HTML 代码。好消息是,一旦您了解到<View>组件相当于使用<div><Text>相当于使用<p>,并且样式类似于 CSS 模块(对象),其他所有功能都与 React(道具、状态、生命周期方法)相同
  2. 创建一个新组件(Home。为此,我们必须创建一个名为 components 的目录,然后将此文件保存为Home.js
  // Dependencies
  import React, { Component } from 'react';
  import { StyleSheet, Text, View } from 'react-native';

  class Home extends Component {
    render() {
      return (
        <View style={styles.container}>
          <Text style={styles.home}>Home Component</Text>
        </View>
      );
    }
  }

  const styles = StyleSheet.create({
    container: {
      flex: 1,
      justifyContent: 'center',
      alignItems: 'center',
      backgroundColor: '#F5FCFF',
    },
    home: {
      fontSize: 20,
      textAlign: 'center',
      margin: 10,
    }
  });

 export default Home;

File: components/Home.js

  1. App.js中,我们导入Home组件,并呈现它:
  // Dependencies
  import React, { Component } from 'react';
  import { StyleSheet, Text, View } from 'react-native';

  // Components
  import Home from './components/Home';

  class App extends Component {
    render() {
      return (
        <Home />
      );
    }
  }

  export default App;

File: App.js

它是如何工作的。。。

如您所见,创建一个新的 React 本机应用非常简单,但是 React(使用 JSX)和 React 本机(使用带有对象样式的特殊标记)之间存在一些关键区别,即使这些样式也有一些限制,例如,让我们创建一个 flex 布局:

    // Dependencies
    import React, { Component } from 'react';
    import { StyleSheet, Text, View } from 'react-native';

    class Home extends Component {
      render() {
        return (
          <View style={styles.container}>
            <View style={styles.header}>
              <Text style={styles.headerText}>Header</Text>
            </View>

            <View style={styles.columns}>
              <View style={styles.column1}>
                <Text style={styles.column1Text}>Column 1</Text>
              </View>

              <View style={styles.column2}>
                <Text style={styles.column2Text}>Column 2</Text>
              </View>

              <View style={styles.column3}>
                <Text style={styles.column3Text}>Column 3</Text>
              </View>
            </View>
          </View>
        );
      }
    }

    const styles = StyleSheet.create({
      container: {
        flex: 1,
        height: 100
      },
      header: {
        flex: 1,
        backgroundColor: 'green',
        justifyContent: 'center',
        alignItems: 'center'
      },
      headerText: {
        color: 'white'
      },
      columns: {
        flex: 1
      },
      column1: {
        flex: 1,
        alignItems: 'center',
        justifyContent: 'center',
        backgroundColor: 'red'
      },
      column1Text: {
        color: 'white'
      },
      column2: {
        flex: 1,
        alignItems: 'center',
        justifyContent: 'center',
        backgroundColor: 'blue'
      },
      column2Text: {
        color: 'white'
      },
      column3: {
        flex: 1,
        alignItems: 'center',
        justifyContent: 'center',
        backgroundColor: 'orange'
      },
      column3Text: {
        color: 'white'
      },
    });

    export default Home;

File: components/Home.js

您可能不喜欢查看大型文件(我也不喜欢),因此让我们将组件和样式分开:

  import { StyleSheet } from 'react-native';

  export default StyleSheet.create({
    container: {
      flex: 1,
      height: 100
    },
    header: {
      flex: 1,
      backgroundColor: 'green',
      justifyContent: 'center',
      alignItems: 'center'
    },
    headerText: {
      color: 'white'
    },
    columns: {
      flex: 1
    },
    column1: {
      flex: 1,
      alignItems: 'center',
     justifyContent: 'center',
      backgroundColor: 'red'
    },
    column1Text: {
      color: 'white'
    },
    column2: {
      flex: 1,
      alignItems: 'center',
      justifyContent: 'center',
      backgroundColor: 'blue'
    },
    column2Text: {
      color: 'white'
    },
    column3: {
      flex: 1,
      alignItems: 'center',
      justifyContent: 'center',
      backgroundColor: 'orange'
    },
    column3Text: {
      color: 'white'
    },
  });

File: components/HomeStyles.js

然后在我们的Home组件中,我们可以导入样式并以与之前相同的方式使用它们:

  // Dependencies
  import React, { Component } from 'react';
  import { StyleSheet, Text, View } from 'react-native';

  // Styles
  import styles from './HomeStyles';
  ...

File: components/Home.js

以下是代码的结果:

但是有一些不寻常的事情。

如您所见,我为<Text>组件(headerText、column1Text 等)创建了样式,这是因为视图组件中不允许使用某些样式。例如,如果您尝试将color: 'white'属性添加到<View>组件中,您将看到该属性不起作用,并且标题将包含黑色文本:

使用 React Native 创建待办事项列表

在本食谱中,我们将学习如何在 React Native 中处理事件,以及如何通过创建简单的 Todo 列表来处理状态。

怎么做。。。

对于这个配方,我创建了一个名为“MySecondReactNativeApp”的新 React 应用:

  1. 创建一个src文件夹并将App.js文件移到里面。另外,修改此文件以包括我们的待办事项列表:
  import React, { Component } from 'react';

  import Todo from './components/Todo';

  export default class App extends Component {
    render() {
      return (
        <Todo />
      );
    }
  }

File: src/App.js

  1. 我们的Todo组件将是:
  import React, { Component } from 'react';
  import { 
    Text, 
    View, 
    TextInput, 
    TouchableOpacity, 
    ScrollView 
  } from 'react-native';

  import styles from './TodoStyles';

  class Todo extends Component {
    state = {
      task: '',
      list: []
    };

    onPressAddTask = () => {
      if (this.state.task) {
        const newTask = this.state.task;
        const lastTask = this.state.list[0] || { id: 0 };
        const newId = Number(lastTask.id + 1);

        this.setState({
          list: [{ id: newId, task: newTask }, ...this.state.list],
          task: ''
        });
      }
    }

    onPressDeleteTask = id => {
      this.setState({
        list: this.state.list.filter(task => task.id !== id)
      });
    }

    render() {
      const { list } = this.state;
      let zebraIndex = 1;

      return (
        <View style={styles.container}>
          <ScrollView
            contentContainerStyle={{
              flexGrow: 1,
            }}
          >
            <View style={styles.list}>
              <View style={styles.header}>
                <Text style={styles.headerText}>Todo List</Text>
              </View>

              <View style={styles.add}>
                <TextInput
                  style={styles.inputText}
                  placeholder="Add a new task"
                  onChangeText={(value) => this.setState({ task: 
 value })}
                  value={this.state.task}
                />

                <TouchableOpacity
                  style={styles.button}
                  onPress={this.onPressAddTask}
                >
                  <Text style={styles.submitText}>+ Add Task</Text>
                </TouchableOpacity>
              </View>

              {list.length === 0 && (
                <View style={styles.noTasks}>
                  <Text style={styles.noTasksText}>
                    There are no tasks yet, create a new one!
 </Text>
                </View>
              )}

              {list.map((item, i) => {
                zebraIndex = zebraIndex === 2 ? 1 : 2;

                return (
                  <View key={`task${i}`} style=
                   {styles[`task${zebraIndex}`]}>
                    <Text>{item.task}</Text>
                    <TouchableOpacity onPress={() => { 
                     this.onPressDeleteTask(item.id) }}>
                      <Text style={styles.delete}>
                        X
                      </Text>
                    </TouchableOpacity>
                  </View>
                );
              })}
            </View>
 </ScrollView>
 </View>
      );
    }
  }

 export default Todo;

File: src/components/Todo.js

  1. 以下是样式:
  import { StyleSheet } from 'react-native';

 export default StyleSheet.create({
    container: {
      flex: 1,
      backgroundColor: '#F5FCFF',
      height: 50
    },
    list: {
      flex: 1
    },
    header: {
      backgroundColor: '#333',
      alignItems: 'center',
      justifyContent: 'center',
      height: 60
    },
    headerText: {
      color: 'white'
    },
    inputText: {
      color: '#666',
      height: 40,
      borderColor: 'gray',
      borderWidth: 1
    },
    button: {
      paddingTop: 10,
      paddingBottom: 10,
      backgroundColor: '#1480D6'
    },
    submitText: {
      color:'#fff',
      textAlign:'center',
      paddingLeft : 10,
      paddingRight : 10
    },
    task1: {
      flexDirection: 'row',
      height: 50,
      backgroundColor: '#ccc',
      alignItems: 'center',
      justifyContent: 'space-between',
      paddingLeft: 5
    },
    task2: {
      flexDirection: 'row',
      height: 50,
      backgroundColor: '#eee',
      alignItems: 'center',
      justifyContent: 'space-between',
      paddingLeft: 5
    },
    delete: {
      margin: 10,
      fontSize: 15
    },
    noTasks: {
      flex: 1,
      alignItems: 'center',
      justifyContent: 'center'
    },
    noTasksText: {
      color: '#888'
    }
  });

File: src/components/TodoStyles.js

它是如何工作的。。。

我们在组件中做的第一件事是设置状态。task状态用于输入创建新项,list状态用于保存所有任务项:

 state = {
      task: '',
      list: []
    };

TextInput组件创建了一个输入元素,与 React 中的输入主要区别在于,它没有使用onChange方法,而是使用onChangeText,默认获取值,我们可以直接更新我们的状态:

 <TextInput
    style={styles.inputText}
    placeholder="Add a new task"
    onChangeText={(value) => this.setState({ task: value })}
    value={this.state.task}
  />

TouchableOpacity组件用于处理点击事件(React Native 中的onPress,可以用作按钮。也许你想知道为什么我没有直接使用组件Button;这是因为在 iOS 上无法为按钮添加背景色,它只能在 Android 上使用背景色。使用TouchableOpacity(或TouchableHighlight)可以对样式进行个性化设置,它可以完美地作为一个按钮:

  <TouchableOpacity
    style={styles.button}
    onPress={this.onPressAddTask}
  >
    <Text style={styles.submitText}>+ Add Task</Text>
  </TouchableOpacity>

在任务的渲染中,我为任务实现了斑马风格(混合颜色)。此外,我们正在处理onPressDeleteTask通过点击 X 按钮删除每个项目:

    {list.map((item, i) => {
      zebraIndex = zebraIndex === 2 ? 1 : 2;

      return (
        <View key={`task${i}`} style={styles[`task${zebraIndex}`]}>
          <Text>{item.task}</Text>
          <TouchableOpacity onPress={() => { 
           this.onPressDeleteTask(item.id) }}>
            <Text style={styles.delete}>
              X
            </Text>
          </TouchableOpacity>
 </View>
      );
    })}

如果我们运行应用,首先会看到以下视图:

如果我们没有任何任务,我们将看到“还没有任务,创建一个新任务!”消息。

如您所见,顶部有一个输入,其中有“添加新任务”占位符。让我们添加一些任务:

最后,我们可以通过点击 X 来删除任务;我将删除“支付租金”任务:

从这个基本的 Todo 列表中可以看到,我们学习了如何使用本地状态以及如何在 React Native 中处理单击和更改事件。

还有更多。。。

如果要防止用户意外删除任务,可以添加警报,询问用户是否确实要删除所选任务。为此,我们需要从 react native 导入警报组件,并修改 onPressDeleteTask 方法:

  import { 
    Text, 
    View, 
    TextInput, 
    TouchableOpacity, 
    ScrollView, 
 Alert 
  } from 'react-native';

  ...

  onPressDeleteTask = id => {
    Alert.alert('Delete', 'Do you really want to delete this task?', [
      {
        text: 'Yes, delete it.',
        onPress: () => {
          this.setState({
            list: this.state.list.filter(task => task.id !== id)
          });
        }
      }, {
        text: 'No, keep it.'
      }
    ]);
  }

  ...

如果您运行应用并尝试立即删除任务,您将看到此本机警报:

实现 React 导航 V2

在本教程中,我们将学习如何在 React 本机应用中实现 React Navigation V2。我们将在各个部分之间创建一个简单的导航。

准备

我们需要安装react-navigation依赖项:

 npm install react-navigation

怎么做。。。

让我们实现 React Navigation v2:

  1. 包括 react navigation 中的createDrawerNavigationDrawerItems以及我们希望呈现为部分的组件(主页和配置):
  // Dependencies
  import React, { Component } from 'react';
  import { StyleSheet, View, ScrollView, Image } from 'react-
  native';

  // React Navigation
  import { createDrawerNavigator, DrawerItems } from 'react-
  navigation';

  // Components
  import Home from './sections/Home';
  import Configuration from './sections/Configuration';

File: App.js

  1. 在 CustomDrawerComponent 中,我们将呈现 Codejobs 徽标和菜单(您可以根据需要进行修改):
 // Custom Drawer Component
 // Here we are displaying the menu options 
  // and customizing our drawer
  const CustomDrawerComponent = props => (
    <View style={styles.area}>
      <View style={styles.drawer}>
        <Image
          source={require('./img/codejobs.jpeg')}
          style={styles.logo}>
        </Image>
      </View>

 <ScrollView>
        <DrawerItems {...props} />
 </ScrollView>
 </View>
  );

File: App.js

  1. 创建AppDrawerNavigator,指定要在菜单中显示为部分的组件(主页和配置)。另外,我们需要通过前面创建的contentComponentCustomDrawerComponent
 // The left Drawer navigation
 // The first object are the components that we want to display
 // in the Drawer Navigation.
  const AppDrawerNavigator = createDrawerNavigator({
    Home,
    Configuration
  },
  {
    contentComponent: CustomDrawerComponent
  });

File: App.js

  1. 创建 App 类并呈现AppDrawerNavigator组件:
  class App extends Component {
    render() {
      return (
        <AppDrawerNavigator />
      );
    }
  }

  // Styles for left Drawer
  const styles = StyleSheet.create({
    area: {
      flex: 1
    },
    drawer: {
      height: 150,
      backgroundColor: 'white',
      alignItems: 'center',
      justifyContent:'center'
    },
    logo: {
      height: 120,
      width: 120,
      borderRadius: 60
    }
  });

  export default App;

File: App.js

  1. 创建截面构件;第一个是 Home 组件:
  // Dependencies
  import React, { Component } from 'react';
  import { View, Text, Image, TouchableOpacity } from 'react-native';
  // Styles
  import styles from './SectionStyles';
  class Home extends Component {
    // Here we specify the icon we want to render
 // in the menu for this option
    static navigationOptions = {
      drawerIcon: () => (
        <Image
          style={styles.iconsItem}
          source={require('../img/home.png')}
        />
      )
    }
    render() {
      return(
        <View style={styles.container}>
          {/* Hamburger menu */}
          <TouchableOpacity 
            onPress={() => this.props.navigation.openDrawer()} 
            style={styles.iconMenu}
          >
            <Image
              style={styles.menu}
              source={require('../img/menu.png')}
            />
          </TouchableOpacity>

          {/* Here is the content of the component */}
          <Text style={styles.titleText}>I'm the home section</Text>
        </View>
      );
    }
  }
  export default Home;

File: sections/Home.js

  1. 以下是配置部分组件:
  // Dependencies
  import React, { Component } from 'react';
  import { View, Text, Image, TouchableOpacity } from 'react-native';

  // Styles
  import styles from './SectionStyles';

  class Configuration extends Component {
 // Here we specify the icon we want to render
 // in the menu for this option
    static navigationOptions = {
      drawerIcon: () => (
        <Image
          style={styles.iconsItem}
          source={require('../img/config.png')}
        />
      )
    };

    render() {
      return(
        <View style={styles.container}>
          {/* Hamburger menu */}
          <TouchableOpacity 
            onPress={() => this.props.navigation.openDrawer()} 
            style={styles.iconMenu}
          >
            <Image
              style={styles.menu}
              source={require('../img/menu.png')}
            />
          </TouchableOpacity>

          {/* Here is the content of the component */}
          <Text style={styles.titleText}>I'm the configuration 
          section</Text>
        </View>
      );
    }
  }

 export default Configuration;

File: sections/Configuration.js

  1. 您可能已经注意到,我们在两个组件上使用相同的样式,这就是为什么我为这些样式创建了一个单独的文件:
  import { StyleSheet } from 'react-native';

 export default StyleSheet.create({
    container: {
      flex: 1,
      backgroundColor: '#fff',
      alignItems: 'center',
      justifyContent: 'center',
    },
    iconMenu: {
      position: 'absolute',
      left: 0,
      top: 5
    },
    titleText: {
      fontSize: 26,
      fontWeight: 'bold',
    },
    menu: {
      width: 80,
      height: 80,
    },
    iconsItem: {
      width: 25,
      height: 25
    }
  });

File: sections/sectionStyles.js

  1. 您可以在存储库(Chapter14/Recipe3/ReactNavigation/assets中找到我们正在使用的资产。

它是如何工作的。。。

如果所有操作都正确,您应该看到:

正在渲染的第一个组件是Home组件。如果你点击汉堡包菜单,你会看到抽屉里有两个部分(HomeConfiguration,分别有各自的图标,顶部有 Codejobs 徽标:

最后,如果单击配置,您也将看到该组件:

如果您再次看到抽屉,您将注意到当前打开的部分在菜单中也处于活动状态(在本例中为“配置”)。