搭建一个待办事项应用程序所涉及的工作涵盖了搭建数据驱动应用程序的所有重要步骤,包括创建读取更新删除(CRUD)操作。我将在这个案例里面使用最流行的移动框架之一 React Native 搭建一个待办事项应用程序。

我将使用 ReactiveSearch Native,这是一个提供 React Native UI 组件并快捷搭建数据驱动应用程序的开源库。

我会在这个案例中搭建以下待办事项应用程序

image-21
Todo App

你可以在 snack 或者 expo 上了解这个待办事项应用程序。

什么是 React Native?

以下是文档的描述:

React Native 允许仅使用 JavaScript 搭建移动应用程序,它在设计原理上和 React 一致,通过声明式的组件机制来搭建丰富多彩的用户界面。

即使你刚开始使用 React 或 React Native,你应该也能够跟着这篇文章来搭建自己的实时待办事项应用程序。

为什么我们要使用 ReactiveSearch⚛

ReactiveSearch 是一款我和一群很棒的伙伴合作为 Elasticsearch 开发的 React 和 React Native UI 开源组件库,它提供了各种可以连接到任何的 Elasticsearch 集群的 React Native 组件。

我写了另一篇文章,介绍如何使用 React 和 Elasticsearch 搭建一个 GitHub 仓库浏览器。你可以在那篇文章里查看关于 Elasticsearch 的简要介绍。即使你没有 Elasticsearch 的相关经验,你应该也能够很好地理解这篇文章。

先做一些设置准备⚒

我们将在这里使用的 React Native 版本库。

在开始搭建 UI 之前,我们需要在 Elasticsearch 中创建一个数据存储区。ReactiveSearch 可以与任何 Elasticsearch 索引一起使用,你可以轻松地将它与你自己的数据集一起使用

image-3

此处查看我的应用数据集,你也可以将其克隆到你自己的应用中。

为了简洁起见,你可以直接使用我的数据集或者使用可以让你创建一个 Elasticsearch 索引数据集(也称为应用程序)的 appbase.io

所有待办事项的结构都采用以下格式:

{
  "title": "react-native",
  "completed": true,
  "createdAt": 1518449005768
}

启动项目

在开始之前,我建议安装 yarn。 在 Linux 上,只需添加 yarn 存储库并通过包管理器运行 install 命令即可完成。 在 Mac 上,你需要首先安装 Homebrew 以使事情变得更简单。 这里是 yarn 详细的安装文档。 接下来你需要安装 watchman,它是一个文件监听服务,它将帮助 react-native 包顺利运行。

我在这里使用 GitHub 分支中的 create-react-native-app 设置了启动项目。 你可以通过运行以下命令来下载 zip 或克隆基础分支:

git clone -b base https://github.com/appbaseio-apps/todos-native

接下来安装依赖项并启动包:

cd todos-native && yarn && yarn start

在包启动后,你可以使用 Expo 应用程序或使用 Android 或 IOS 模拟器在手机上运行这个应用程序:

image-22
所有选项卡的基本设置,请从这里克隆

嵌入代码

基础分支克隆代码后,你应该看到如下目录结构:

navigation
├── RootComponent.js         // Root component for our app
├── MainTabNavigator.js      // Tab navigation component
screens
├── TodosScreen.js           // Renders the TodosContainer
components        
├── Header.js                // Header component         
├── AddTodo.js               // Add todo input        
├── AddTodoButton.js         // Add todo floating button
├── TodoItem.js              // The todo item         
├── TodosContainer.js        // Todos main container api
├── todos.js                 // APIs for performing writes
constants                    // All types of constants used in app
types                        // Todo type to be used with prop-types
utils                        // Streaming logic goes here

让我们来解析一下基本的设置。

导航

  • 连接到 Elasticsearch 的所有必要配置都在 constants / Config.js 中。
  • 我们使用来自 react-navigationTabNavigator 显示 todos 的 AllActiveCompleted 界面通过 navigation / RootComponent.js 渲染。 你会注意到 RootComponent[ReactiveBase] 组件中的所有内容封装在 ReactiveSearch 中。 此组件为子 ReactiveSearch 组件提供所有必需的数据。 你可以通过更新 constants / Config.js 中的配置来连接你自己的 Elasticsearch 索引。

导航的逻辑放在 navigation / MainNavigator.js 中。让我们来看看它是如何工作的。如果你想要引用任何内容,这个是选项卡导航的文档。

import React from 'react';
import { MaterialIcons } from '@expo/vector-icons';
import { TabNavigator, TabBarBottom } from 'react-navigation';

import Colors from '../constants/Colors';
import CONSTANTS from '../constants';
import TodosScreen from '../screens/TodosScreen';

const commonNavigationOptions = ({ navigation }) => ({
    header: null,
    title: navigation.state.routeName,
});

// we just pass these to render different routes
const routeOptions = {
    screen: TodosScreen,
    navigationOptions: commonNavigationOptions,
};

// different routes for all, active and completed todos
const TabNav = TabNavigator(
    {
        [CONSTANTS.ALL]: routeOptions,
        [CONSTANTS.ACTIVE]: routeOptions,
        [CONSTANTS.COMPLETED]: routeOptions,
    },
    {
        navigationOptions: ({ navigation }) => ({
            // this tells us which icon to render on the tabs
            tabBarIcon: ({ focused }) => {
                const { routeName } = navigation.state;
                let iconName;
                switch (routeName) {
                    case CONSTANTS.ALL:
                        iconName = 'format-list-bulleted';
                        break;
                    case CONSTANTS.ACTIVE:
                        iconName = 'filter-center-focus';
                        break;
                    case CONSTANTS.COMPLETED:
                        iconName = 'playlist-add-check';
                }
                return (
                    <MaterialIcons
                        name={iconName}
                        size={28}
                        style={{ marginBottom: -3 }}
                        color={focused ? Colors.tabIconSelected : Colors.tabIconDefault}
                    />
                );
            },
        }),
        // for rendering the tabs at bottom
        tabBarComponent: TabBarBottom,
        tabBarPosition: 'bottom',
        animationEnabled: true,
        swipeEnabled: true,
    },
);

export default TabNav;

TabNavigator 函数接受两个参数,第一个是路由配置,第二个是TabNavigator 配置。在上面的代码片段中,我们传递的配置是在底部显示选项卡导航栏并为每个选项卡设置不同的图标。

TodosScreen 和 TodosContainer

screens / TodosScreen.js 中的 TodosScreen 组件将我们的主要 TodosContainer 组件包装在 components / TodosContainer.js中,我们将为应用程序添加各种组件。TodosContainer 将根据我们是否在 AllActive 或者 Completed 选项卡上来显示已过滤的数据。

用于创建,更新和删除待办事项的 API

用于 Elasticsearch 上的 CUD 操作的 API 存储在 api / todos.js 中,它包含三个简单的方法 addupdatedestroy,它们与 constants / Config.js 中指定的任何 Elasticsearch 索引一起使用。需要记住的一点是,我们创建的每个待办事项都将具有唯一的 _id 字段。我们可以使用此 _id 字段来更新或删除现有的待办事项。

对于我们的 app,我们只需要添加、创建或删除待办事项这三个方法。但是,你可以在文档中找到有关 API 方法的详细说明。

搭建组件和 UI

让我们开始添加一些组件来完成应用程序的功能。

添加 Todos

我们将使用 [native-base][Fab] 来渲染用于添加待办事项的浮动按钮。

image-23
const AddTodoButton = ({ onPress }) => (
  <Fab
      direction="up"
      containerStyle={{}}
      style={{ backgroundColor: COLORS.primary }}
      position="bottomRight"
      onPress={onPress}
  >
      <Icon name="add" />
  </Fab>
);

现在,你可以在 components / TodosContainer.js 中使用此组件。

import AddTodoButton from './AddTodoButton';
...
export default class TodosContainer extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        ...
        <AddTodoButton />
      </View>
    );
  }
}

添加按钮之后,我们就会看到如下内容:

image-24
添加了 AddTodoButton 之后

现在,当有人点击此按钮时,我们需要显示添加待办事项的输入框。让我们在 components / AddTodo.js 中添加这个代码。

class AddTodo extends Component {
  constructor(props) {
    super(props);
    const { title, completed, createdAt } = this.props.todo;
    this.state = {
      title,
      completed,
      createdAt,
    };
  }

  onSubmit = () => {
    if (this.state.title.length > 0) this.props.onAdd(this.state);
    return null;
  };

  setStateUtil = (property, value = undefined) => {
    this.setState({
      [property]: value,
    });
  };

  render() {
    const { title, completed } = this.state;
    const { onBlur } = this.props;
    return (
      <View
        style={{
          flex: 1,
          width: '100%',
          flexDirection: 'row',
          alignItems: 'center',
          paddingRight: 10,
          paddingBottom: 5,
          paddingTop: 5,
        }}
      >
        <CheckBox checked={completed} onPress={() => this.setStateUtil('completed', !completed)} />
        <Body
          style={{
            flex: 1,
            justifyContent: 'flex-start',
            alignItems: 'flex-start',
            paddingLeft: 25,
          }}
        >
          <TextInput
            style={{ width: '90%' }}
            placeholder="What needs to be done?"
            autoFocus
            underLineColorAndroid="transparent"
            underlineColor="transparent"
            blurOnSubmit
            onSubmitEditing={this.onSubmit}
            onChangeText={changedTitle => this.setStateUtil('title', changedTitle)}
            value={title}
            autoCorrect={false}
            autoCapitalize="none"
            onBlur={onBlur}
          />
        </Body>
        <TouchableOpacity
          onPress={() => this.props.onCancelDelete}
          style={{ paddingLeft: 25, paddingRight: 15 }}
        >
          <Ionicons
            name="ios-trash-outline"
            color={`${title.length > 0 ? 'black' : 'grey'}`}
            size={23}
          />
        </TouchableOpacity>
      </View>
    );
  }
}

这里使用的主要组件是 [TextInput][Checkbox] 和 `[Ionicons]和 props 属性。我们通过 state 使用 titlecompleted。 我们将从 components / TodosContainer.js 传递 props 属性 todoonAddonCancelDeleteonBlur。 这些将有助于添加新待办事项,或在取消添加待办事项的时候重置视图。

现在我们可以更新 components / TodosContainer.js

...
import AddTodoButton from './AddTodoButton';
import AddTodo from './AddTodo';
import TodoModel from '../api/todos';
...

// will render todos based on the active screen: all, active or completed
export default class TodosContainer extends React.Component {
  state = {
    addingTodo: false,
  };

  componentDidMount() {
    // includes the methods for creation, updation and deletion
    this.api = new TodoModel('react-todos');
  }

  render() {
    return (
      <View style={styles.container}>
        <Header />
        <StatusBar backgroundColor={COLORS.primary} barStyle="light-content" />
        <ScrollView>
          {this.state.addingTodo ? (
            <View style={styles.row}>
              <AddTodo
                onAdd={(todo) => {
                  this.setState({ addingTodo: false });
                  this.api.add(todo);
                }}
                onCancelDelete={() => this.setState({ addingTodo: false })}
                onBlur={() => this.setState({ addingTodo: false })}
              />
            </View>
          ) : null}
        </ScrollView>
        <AddTodoButton onPress={() => this.setState({ addingTodo: true })} />
      </View>
    );
  }
}

AddTodo 组件在 ScrollView 组件中渲染。 我们还将一个 onPress 传递给 AddTodoButton 来切换状态并根据 this.state.addingTodo 显示 AddTodo 组件。传递给 AddTodoonAdd 还使用 api / todos.js 中的 add API 创建了一个新的待办事项。

单击添加按钮后,我们将看到添加这样的待办事项输入框:

image-25
添加一个待办事项

显示待办事项

添加待办事项后,将其添加到 Elasticsearch 中。可以使用 ReactiveSearch Native 组件实时查看所有这些数据。

库提供了超过 10 个本地 UI 组件。 对于我们的待办事项应用程序,我们将主要使用 ReactiveList 组件来显示待办事项的状态。

添加 ReactiveList 组件以显示待办事项。 我们将在 components / TodosContainer.js 中添加此组件。 以下是 ReactiveList 的使用方法:

...
import { ReactiveList } from '@appbaseio/reactivesearch-native';
...

export default class TodosContainer extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Header />
        <StatusBar backgroundColor={COLORS.primary} barStyle="light-content" />
        <ScrollView>
          <ReactiveList
            componentId="ReactiveList"
            defaultQuery={() => ({
              query: {
                match_all: {},
              },
            })}
            stream
            onAllData={this.onAllData}
            dataField="title"
            showResultStats={false}
            pagination={false}
          />
          ...
        </ScrollView>
        <AddTodoButton onPress={() => this.setState({ addingTodo: true })} />
      </View>
    );
  }
}

我们还没有添加 onAllData 方法,先来了解一下这里使用的 props:

  • componentId:组件的唯一标识符。
  • defaultQuery:最初应用于列表的查询。 我们将使用 match_all 显示默认情况下的所有待办事项。
  • stream:是否流式传输新结果更新或仅显示历史结果。 通过将此设置为 true,我们现在还可以实时监听 Todo 的更新。 稍后会添加与流相关的逻辑。
  • onAllData - 一个回调函数,它接收当前待办事项列表和数据流(新的待办事项和任何更新),并返回一个 React 组件或 JSX 进行渲染。 这是语法大概的样子:
<ReactiveList
  onAllData(todos, streamData) {
    // return the list to render
  }
  ...
/>

你可以在 ReactiveList 的文档页面上详细了解所有这些 props。

要查看内容,我们需要从 onAllData 返回 JSX 或 React 组件。为此,我们将使用由 Text 组件组成的 React Native 的 FlatList 。在下一步中,我们将添加自定义的 TodoItem 组件。

...
import { ScrollView, StyleSheet, StatusBar, FlatList, Text } from 'react-native';
import CONSTANTS from '../constants';
...

export default class TodosContainer extends React.Component {
  ...
  onAllData = (todos, streamData) => {
    // filter data based on "screen": [All | Active | Completed]
    const filteredData = this.filterTodosData(todos);

    return (
      <FlatList
        style={{ width: '100%', top: 15 }}
        data={filteredData}
        keyExtractor={item => item._id}
        renderItem={({ item: todo }) => (
            <Text>{todo.title}</Text>
        )}
      />
    );
  };

  filterTodosData = (todosData) => {
    const { screen } = this.props;

    switch (screen) {
      case CONSTANTS.ALL:
        return todosData;
      case CONSTANTS.ACTIVE:
        return todosData.filter(todo => !todo.completed);
      case CONSTANTS.COMPLETED:
        return todosData.filter(todo => todo.completed);
    }

    return todosData;
  };

  render() {
    ...
  }
}
image-26
集成 reactiveList 与 onalldata

添加 TodoItem(s)

接下来,我们将创建一个单独的组件 TodoItem,用于显示每个待办事项,其中包含 Todo 项目的所有必要标记,如 CheckBoxText 和一个删除 Icon。这包含在 components / TodoItem.js 中:

class TodoItem extends Component {
  onTodoItemToggle = (todo, propAction) => {
    propAction({
      ...todo,
      completed: !todo.completed,
    });
  };

  render() {
    const { todo, onUpdate, onDelete } = this.props;

    return (
      <View style={styles.row}>
        <View
          style={{
            flex: 1,
            width: '100%',
            flexDirection: 'row',
            alignItems: 'center',
            paddingRight: 10,
            paddingVertical: 5,
          }}
        >
          <TouchableOpacity
            onPress={() => this.onTodoItemToggle(todo, onUpdate)}
            style={{
              flex: 1,
              width: '100%',
              flexDirection: 'row',
            }}
          >
            <CheckBox
              checked={todo.completed}
              onPress={() => this.onTodoItemToggle(todo, onUpdate)}
            />
            <Body
              style={{
                flex: 1,
                justifyContent: 'flex-start',
                alignItems: 'flex-start',
                paddingLeft: 25,
              }}
            >
              <Text
                style={{
                  color: todo.completed ? 'grey' : 'black',
                  textDecorationLine: todo.completed ? 'line-through' : 'none',
                }}
              >
                {todo.title}
              </Text>
            </Body>
          </TouchableOpacity>
          <TouchableOpacity
            onPress={() => onDelete(todo)}
            style={{ paddingLeft: 25, paddingRight: 15 }}
          >
            <Ionicons
              name="ios-trash-outline"
              color={`${todo.title.length > 0 ? 'black' : 'grey'}`}
              size={23}
            />
          </TouchableOpacity>
        </View>
      </View>
    );
  }
}

该组件从其 props 获取 todo 以及用于更新和删除待办事项的 onDeleteonUpdate

接下来,我们可以在 components / TodosContainer.js 中的 onAllData 导入和使用 TodoItem 组件,把 todoupdatedestroy 作为 API 方法作为属性传递给 TodoItem 组件。

class TodosContainer extends Component {
  ...
  onAllData = (todos, streamData) => {
    ...
    return (
      <FlatList
        ...
        renderItem={({ item: todo }) => (
          <TodoItem 
            todo={todo}
            onUpdate={this.api.update} 
            onDelete={this.api.destroy}
          />
        )}
      />
    );
  }
}
image-27
在 ToDoContainer 中添加 TodoItem 后

流数据更新

你可能已经注意到 todos 显示正常,但你无法在不刷新 app 的情况下查看更新的待办事项。在最后一步中,我们将解决这个难题的缺失部分。

在上一节中,我们为 ReactiveListcomponent 添加了一个 onAllData 方法。 我们将利用接收的第二个参数 onAllData 结构更新流来保持待办事项的更新。以下是更新的 onAllData 方法在 components / TodosContainer.js 中的大概的样子。

import Utils from '../utils';
...

export default class TodosContainer extends React.Component {
  ...
  onAllData = (todos, streamData) => {
    // merge streaming todos data along with current todos
    const todosData = Utils.mergeTodos(todos, streamData);

    // filter data based on "screen": [All | Active | Completed]
    const filteredData = this.filterTodosData(todosData);

    return (
      <FlatList
        style={{ width: '100%', top: 15 }}
        data={filteredData}
        keyExtractor={item => item._id}
        renderItem={({ item: todo }) => (
            <TodoItem todo={todo} onUpdate={this.api.update} onDelete={this.api.destroy} />
        )}
      />
    );
  };
  ...
}

mergeTodos 方法存在于 utils / index.js 中,以下是它的工作原理:

class Utils {
  static mergeTodos(todos, streamData) {
    // generate an array of ids of streamData
    const streamDataIds = streamData.map(todo => todo._id);

    return (
      todos
        // consider streamData as the source of truth
        // first take existing todos which are not present in stream data
        .filter(({ _id }) => !streamDataIds.includes(_id))
        // then add todos from stream data
        .concat(streamData)
        // remove todos which are deleted in stream data
        .filter(todo => !todo._deleted)
        // finally sort on the basis of creation timestamp
        .sort((a, b) => a.createdAt - b.createdAt)
    );
  }
}

export default Utils;

streamData 在创建、删除或更新时接收待办事项对象的数组。如果更新了某个对象,则它包含一个设置为 true_updated 键。同样,如果删除了一个对象,则它包含一个设置为 true_deleted 键。如果创建了一个对象,则它不包含这两个。利用这些点,我们添加了 mergeTodos 函数。

有了这个,你应该能够实时看到 todo 项目的变化! 如果你有一个运行相同 app 的其他设备/模拟器,它们也将流式传输新的更新。

参考链接

  1. Todos app 演示expo 链接入门项目最终源代码
  2. ReactiveSearch GitHub repo⭐️
  3. ReactiveSearch 文档

Happy coding!

原文:How to build a real-time todo app with React Native,作者:Divyanshu Maithani