二十六、构建 RelayReact 应用

在上一章中,您对 Relay/GraphQL 进行了一万英尺的介绍,并了解了为什么应该在 React 应用中使用该方法。有了这些,我们就可以构建 Todo React 本机应用,边走边谈代码。在本章结束时,您应该对数据如何在以 GraphQL 为中心的体系结构中移动感到满意。走吧。

TodoMVC 和 Relay

我原本计划在本章中扩展我们之前开发的 Neckbeard 新闻应用。相反,我决定将 TodoMVC 示例用于 Relay(https://github.com/relayjs/relay-examples 是一个强大而简洁的例子,我很难打败它。

所以,计划是这样的:我将带您了解 Todo 应用的本机实现示例。关键是它将使用与 web UI 相同的 GraphQL 后端。我认为这是 React 开发者的一个绝对胜利,他们希望同时构建应用的 web 版本和本机版本;它们可以共享相同的模式!

我已经在本书附带的代码中包含了 TodoMVC 应用的 web 版本,但我不会详细介绍它的工作原理。如果您在过去 5 年中从事过 web 开发,您可能会遇到一个示例 Todo 应用。以下是 web 版本的外观:

TodoMVC and Relay

这一功能是不言自明的,因此,即使您以前没有使用过任何 TodoMVC 应用,我建议您在尝试实现本机版本之前使用这一应用,这是我们在本章剩余部分将要做的。

我们将要实现的本机版本的目标不是功能对等。事实上,我们正在寻找一个非常小的 todo 功能子集。目的是向您展示 Relay 在本机平台上的工作原理与在 web 平台上的工作原理基本相同,并且 GraphQL 后端可以在 web 和本机应用之间共享。

GraphQL 模式

模式是 GraphQL 后端服务器和前端的 Relay 组件使用的词汇表。GraphQL 类型系统使模式能够描述可用的数据,以及在查询请求到来时如何将所有数据放在一起。这正是使整个方法如此可伸缩的原因,GraphQL 运行时解决了如何将数据放在一起的问题。我们需要提供的只是告诉 GraphQL 数据在哪里的函数。例如,在数据库或某个远程服务端点中。

让我们看看 ToDoVC 应用的图形化模式中使用的类型:

import { 
  GraphQLBoolean, 
  GraphQLID, 
  GraphQLInt, 
  GraphQLList, 
  GraphQLNonNull, 
  GraphQLObjectType, 
  GraphQLSchema, 
  GraphQLString, 
} from 'graphql'; 

import { 
  connectionArgs, 
  connectionDefinitions, 
  connectionFromArray, 
  cursorForObjectInConnection, 
  fromGlobalId, 
  globalIdField, 
  mutationWithClientMutationId, 
  nodeDefinitions, 
  toGlobalId, 
} from 'graphql-relay'; 

import { 
  Todo, 
  User, 
  addTodo, 
  changeTodoStatus, 
  getTodo, 
  getTodos, 
  getUser, 
  getViewer, 
  markAllTodos, 
  removeCompletedTodos, 
  removeTodo, 
  renameTodo, 
} from './database'; 

const { nodeInterface, nodeField } = nodeDefinitions( 
  (globalId) => { 
    const { type, id } = fromGlobalId(globalId); 

    if (type === 'Todo') { 
      return getTodo(id); 
    } else if (type === 'User') { 
      return getUser(id); 
    } 

    return null; 
  }, 
  (obj) => { 
    if (obj instanceof Todo) { 
      return GraphQLTodo; 
    } else if (obj instanceof User) { 
      return GraphQLUser; 
    } 

    return null; 
  } 
); 

const GraphQLTodo = new GraphQLObjectType({ 
  name: 'Todo', 
  fields: { 
    id: globalIdField('Todo'), 
    text: { 
      type: GraphQLString, 
      resolve: ({ text }) => text, 
    }, 
    complete: { 
      type: GraphQLBoolean, 
      resolve: ({ complete }) => complete, 
    }, 
  }, 
  interfaces: [nodeInterface], 
}); 

const { 
  connectionType: TodosConnection, 
  edgeType: GraphQLTodoEdge, 
} = connectionDefinitions({ 
  name: 'Todo', 
  nodeType: GraphQLTodo, 
}); 

const GraphQLUser = new GraphQLObjectType({ 
  name: 'User', 
  fields: { 
    id: globalIdField('User'), 
    todos: { 
      type: TodosConnection, 
      args: { 
        status: { 
          type: GraphQLString, 
          defaultValue: 'any', 
        }, 
        ...connectionArgs, 
      }, 
      resolve: (obj, { status, ...args }) => 
        connectionFromArray(getTodos(status), args), 
    }, 
    totalCount: { 
      type: GraphQLInt, 
      resolve: () => getTodos().length, 
    }, 
    completedCount: { 
      type: GraphQLInt, 
      resolve: () => getTodos('completed').length, 
    }, 
  }, 
  interfaces: [nodeInterface], 
}); 

const Root = new GraphQLObjectType({ 
  name: 'Root', 
  fields: { 
    viewer: { 
      type: GraphQLUser, 
      resolve: () => getViewer(), 
    }, 
    node: nodeField, 
  }, 
}); 

这里有很多东西需要导入,所以我们将从导入开始。我想包括所有这些导入,因为我认为它们与本次讨论相关。首先,我们有来自graphql库的原始 GraphQL 类型。接下来,我们有一组来自graphql-relay库的帮助程序,它们简化了 GraphQL 模式的定义。最后,我们从我们自己的database模块中导入。这不一定是一个数据库,事实上,在本例中,它只是模拟数据。例如,如果我们需要与远程 API 端点进行对话,我们可以将database替换为api,或者我们可以将两者结合起来;就 React 组件而言,这都是 GraphQL。

然后,我们定义一些我们自己的 GraphQL 类型。例如,GraphQLTodo类型有两个字段-textcomplete。一个是布尔值,一个是字符串。关于这些字段,需要注意的重要一点是resolve()函数。这就是我们告诉 GraphQL 运行时如何在需要时填充这些字段的方式。这两个字段只返回属性值。

然后是GraphQLUser型。此字段表示用户在 UI 中的整个范围,因此得名。例如,todos字段是我们从 Relay 组件查询 todo 项的方式。它是通过connectionFromArray()函数解决的,这是一个快捷方式,不需要更多详细的字段定义。然后是Root型。这有一个viewer字段,用作所有查询的根。

现在,让我们来看看加尾突变。为了空间的利益,我们不会对这个应用的网络版本使用的每一种变异都进行检查:

const GraphQLAddTodoMutation = mutationWithClientMutationId({ 
  name: 'AddTodo', 

  inputFields: { 
    text: { type: new GraphQLNonNull(GraphQLString) }, 
  }, 

  outputFields: { 
    todoEdge: { 
      type: GraphQLTodoEdge, 
      resolve: ({ localTodoId }) => { 
        const todo = getTodo(localTodoId); 

        return { 
          cursor: cursorForObjectInConnection( 
            getTodos(), 
            Todo 
          ), 
          node: todo, 
        }; 
      }, 
    }, 
    viewer: { 
      type: GraphQLUser, 
      resolve: () => getViewer(), 
    }, 
  }, 

  mutateAndGetPayload: ({ text }) => { 
    const localTodoId = addTodo(text); 
    return { localTodoId }; 
  }, 
}); 

const Mutation = new GraphQLObjectType({ 
  name: 'Mutation', 
  fields: { 
    addTodo: GraphQLAddTodoMutation, 
    ... 
  }, 
}); 

所有的突变都有一个mutateAndGetPayload()方法,这就是突变如何实际调用某些外部服务来更改数据。返回的有效负载可以是已更改的实体,但也可以包括作为副作用更改的数据。这就是outputFields发挥作用的地方。这是在浏览器中返回给 Relay 的信息,以便它有足够的信息根据突变的副作用正确更新组件。别担心,我们会从 Relay 的角度看一点。

我们在这里创建的突变类型用于保存所有应用突变;显然,我们在这里省略了其中一些。最后,以下是如何将整个模式组合在一起并从模块中导出:

export const schema = new GraphQLSchema({ 
  query: Root, 
  mutation: Mutation, 
}); 

我们现在不必担心这个模式是如何输入到 GraphQL 服务器的。如果你对它的工作原理感兴趣,可以看看server.js

自举 Relay

此时,我们已经启动并运行了 GraphQL 后端。现在,我们可以关注前端的 React 组件。特别是,我们将在 React 本地上下文中研究 Relay,它实际上只有细微的区别。例如,在 web 应用中,引导通常是react-routerRelay 的。在 React Native 中,有点不同。让我们看看作为本机应用入口点的索引文件:

import React from 'react'; 
import { AppRegistry } from 'react-native'; 
import Relay, { 
  DefaultNetworkLayer, 
  RootContainer, 
} from 'react-relay'; 

import viewerQueries from './queries/ViewerQueries'; 
import TodoApp from './TodoApp'; 

// Since this is a native app instead of a web 
// app, we have to tell Relay where to find 
// the GraphQL backend. 
Relay.injectNetworkLayer( 
  new DefaultNetworkLayer('http://localhost:8080') 
); 

AppRegistry.registerComponent( 
  'TodoRelayMobile', 
  () => () => ( 
    // The "<RootContainer>" component is the entry 
    // point for Relay in React Native. It takes the 
    // main component - "TodoApp" - and uses 
    // "viewerQueries" to kick-off communication 
    // with the backend. 
    <RootContainer 
      Component={TodoApp} 
      route={{ 
        name: 'viewer', 
        params: {}, 
        queries: viewerQueries, 
      }} 
    /> 
  ) 
); 

这里需要做一些额外的事情。有趣的是传递给<RootContainer>组件的queries道具。让我们看一下 ToeT2 模块:

import Relay from 'react-relay'; 

export default { 
  viewer: () => Relay.QL`query { viewer }`, 
}; 

酷,这意味着如果TodoApp组件需要数据;它的父组件知道viewer查询。请记住,在我们前面创建的 GraphQL 模式中,这是应用可用的数据范围。组件可能需要的任何内容都可以在此查询中找到。稍后将对此进行详细介绍。

增加待办事项

TodoApp组件中,我们将添加一个文本输入,允许用户输入新的 todo 项目。当他们输入完 todo 后,Relay 将需要向后端 GraphQL 服务器发送一个变异。以下是组件代码的外观:

import React, { Component, PropTypes } from 'react'; 
import { 
  View, 
  TextInput, 
} from 'react-native'; 
import Relay from 'react-relay'; 

import styles from './styles'; 
import AddTodoMutation from './mutations/AddTodoMutation'; 
import TodoList from './TodoList'; 

export class TodoRelayMobile extends Component { 
  static propTypes = { 
    viewer: PropTypes.any.isRequired, 
    relay: PropTypes.shape({ 
      commitUpdate: PropTypes.func.isRequired, 
    }), 
  } 

  // We need to keep track of new todo item text 
  // as the user enters them. 
  state = { 
    text: '', 
  } 

  // When the user creates the todo by pressing enter, 
  // the "AddTodoMutation" is sent to the backend, 
  // with the new "text" and the "viewer" as the 
  // arguments. 
  onSubmitEditing = ({ nativeEvent: { text } }) => { 
    this.props.relay.commitUpdate( 
      new AddTodoMutation({ 
        text, 
        viewer: this.props.viewer, 
      }) 
    ); 

    this.setState({ text: '' }); 
  } 

  onChangeText = text => this.setState({ text }) 

  render() { 
    return ( 
      <View style={styles.container}> 
        <TextInput 
          style={styles.textInput} 
          placeholder="What needs to be done?" 
          onSubmitEditing={this.onSubmitEditing} 
          onChangeText={this.onChangeText} 
          value={this.state.text} 
        /> 
        <TodoList viewer={this.props.viewer} /> 
      </View> 
    ); 
  } 
} 

它看起来与典型的 React 本机组件没有多大区别。最引人注目的是变异-AddTodoMutation。这就是我们告诉 GraphQL 后端我们想要创建一个新的todo节点的方式。此时,TodoApp组件仍然只是一个普通的 React 组件。这是我们创建 Relay 容器并从模块导出它的方式:

// Turns the "TodoApp" component into a Relay 
// container component. This is where the data 
// dependency for "TodoApp" is declared. We tell 
// the "queries" value that was passed to "RootContainer" 
// that we want a fragment of fields from the "User" type. 
export default Relay.createContainer(TodoRelayMobile, { 
  fragments: { 
    viewer: variables => Relay.QL` 
      fragment on User { 
        totalCount, 
        ${AddTodoMutation.getFragment('viewer')}, 
        ${TodoList.getFragment('viewer', ...variables)}, 
      } 
    `, 
  }, 
}); 

这就是我们告诉 Relay 其数据依赖关系的方式,包括如果用户决定添加新 todo,我们可能发送的AddTodoMutation。关于这个片段需要注意的另一点是,它正在传递来自TodoListviewer片段。这是因为即使TodoApp没有直接使用TodoList所需的数据,它仍然需要告知 Relay,以便TodoList组件渲染时,它可以从其父级获得它所需的数据。让我们看看目前为止应用的外观:

Adding todo items

用于添加新 todo 项目的文本框位于 todo 项目列表的正上方。现在,我们来看看TodoList组件,它负责呈现 todo 项目列表。

呈现待办事项

TodoList组件的任务是呈现待办事项列表。当AddTodoMutation发生时,TodoList组件需要能够呈现这个新项目。Relay 负责更新所有 GraphQL 数据所在的内部数据存储。下面再次查看项目列表,并添加了更多待办事项:

Rendering todo items

以下是TodoList组件本身:

import React, { PropTypes } from 'react'; 
import Relay from 'react-relay'; 
import { View } from 'react-native'; 

import Todo from './Todo'; 

// The list component itself is quite simple. Note the 
// property that we're using to iterate over - there's 
// "edges" and "nodes". This is reflective of a GraphQL 
// collection. 
const TodoList = ({ viewer }) => ( 
  <View> 
    {viewer.todos.edges.map(edge => ( 
      <Todo 
        key={edge.node.id} 
        todo={edge.node} 
        viewer={viewer} 
      /> 
    ))} 
  </View> 
); 

TodoList.propTypes = { 
  viewer: PropTypes.any.isRequired, 
}; 

export default Relay.createContainer(TodoList, { 
  initialVariables: { 
    status: null, 
  }, 

  // Variables that are sent along with the query. These 
  // can come from UI elements. In this case, we want every 
  // item, so we're providing a static value. 
  prepareVariables() { 
    return { 
      status: 'any', 
    }; 
  }, 

  // The fragments used by this component. Notice the 
  // arguments that are passed to the "todos" query - 
  // "status" and "first". We're also traversing the 
  // structure of the graph using "edges" and "node", 
  // so that we can tell the backend exactly what 
  // data this component needs. 
  fragments: { 
    viewer: () => Relay.QL` 
      fragment on User { 
        todos( 
          status: $status, 
          first: 2147483647  # max GraphQLInt 
        ) { 
          edges { 
            node { 
              id, 
              ${Todo.getFragment('todo')}, 
            }, 
          }, 
        }, 
        ${Todo.getFragment('viewer')}, 
      } 
    `, 
  }, 
}); 

如您所见,fragments属性是我们编写相关 GraphQL 以获取所需数据的地方。这是组件的声明性数据依赖项。此外,用于 todo 项目的特定 GraphQL 片段来自Todo.getFragment('todo')。因此,我们在组件之间共享数据依赖关系。当我们渲染<Todo>组件时,我们正在传递edge.todo数据。现在,让我们看看Todo组件本身是什么样子。

完成待办事项

此应用的最后一部分是呈现每个 todo 项,并提供更改 todo 状态的功能。让我们看看这个代码:

import React, { Component, PropTypes } from 'react'; 
import Relay from 'react-relay'; 
import { 
  Text, 
  View, 
  Switch, 
} from 'react-native'; 

import styles from './styles'; 
import ChangeTodoStatusMutation from 
  './mutations/ChangeTodoStatusMutation'; 

// How to style the todo text, based on the 
// boolean value of the "completed" property. 
const completeStyleMap = new Map([ 
  [true, { textDecorationLine: 'line-through' }], 
  [false, {}], 
]); 

class Todo extends Component { 
  static propTypes = { 
    relay: PropTypes.any.isRequired, 
    viewer: PropTypes.any.isRequired, 
    todo: PropTypes.shape({ 
      text: PropTypes.string.isRequired, 
      complete: PropTypes.bool.isRequired, 
    }), 
  } 

  // Handles the "switch" button click. The "complete" 
  // argument is the value of the switch UI control, 
  // which is sent to the "ChangeTodoStatusMutation". 
  onValueChange = complete => 
    this.props.relay.commitUpdate( 
      new ChangeTodoStatusMutation({ 
        complete, 
        todo: this.props.todo, 
        viewer: this.props.viewer, 
      }) 
    ) 

  render() { 
    // The "todo" is passed in from the "TodoList" 
    // component. 
    const { 
      props: { 
        todo: { 
          text, 
          complete, 
        }, 
      }, 
      onValueChange, 
    } = this; 

    // The actual todo is a "<Switch>" component, 
    // and the todo item text, styled based on it's 
    // "complete" value. 
    return ( 
      <View style={styles.todoItem}> 
        <Switch 
          value={complete} 
          onValueChange={onValueChange} 
        /> 
        <Text style={completeStyleMap.get(complete)}> 
          {text} 
        </Text> 
      </View> 
    ); 
  } 
} 

// The fragments defined here are actually used 
// in the "TodoList" component when it runs the 
// "todos" query. We also have to tell it about 
// the fragments defined by "ChangeTodoStatusMutation". 
export default Relay.createContainer(Todo, { 
  fragments: { 
    todo: () => Relay.QL` 
      fragment on Todo { 
        complete, 
        id, 
        text, 
        ${ChangeTodoStatusMutation.getFragment('todo')}, 
      } 
    `, 
    viewer: () => Relay.QL` 
      fragment on User { 
        ${ChangeTodoStatusMutation.getFragment('viewer')}, 
      } 
    `, 
  }, 
}); 

渲染的实际组件非常简单—一个开关控件和项文本。当用户将 todo 标记为“完成”时,项目文本的样式为“已删除”。用户也可以取消选中项目。ChangeTodoStatusMutation变异将请求发送到 GraphQL 后端以更改todo状态。GraphQL 后端然后与任何需要实现这一点的微服务进行对话。然后,它用该组件所依赖的字段进行响应。

我想指出的代码的重要部分是 Relay 容器中使用的片段。这个容器实际上并不直接使用它们。相反,它们被TodoList组件(Todo.getFrament()中的todos查询使用。这是很有用的,因为这意味着我们可以在另一个上下文中使用Todo组件和另一个查询,并且它的数据依赖关系将始终得到满足。

总结

在本章中,我们实现了一些特定的 Relay 和 GraphQL 思想。从 GraphQL 模式开始,您学习了如何声明应用使用的数据,以及这些数据类型如何解析为特定的数据源,例如 microservice 端点。然后,在 React 本机应用中,我们从 Relay 开始引导 GraphQL 查询。接下来,我们详细介绍了添加、更改和列出 todo 项的细节。该应用本身使用与 Todo 应用的 web 版本相同的模式,这使您在开发 web 和本机 React 应用时更加容易。

嗯,这是这本书的结尾。我们一起浏览了很多资料,我希望你从阅读中学到的东西和我从写作中学到的一样多。如果这本书中有一个主题你应该放弃,那就是 React 只是一个渲染抽象。随着新的渲染目标的出现,新的 React 库也将出现。当开发人员想到处理大规模状态的新方法时,您将看到新技术和库的发布。我希望你们现在已经做好了在这个快速发展的生态系统中工作的准备。