这个世界会好吗

Tutorial: GraphQL输入类型和自定义解析器

原文链接: dev-blog.apollodata.com

这是我们全栈GraphQL + React教程的第5部分,它指导您创建消息传递应用程序。 每个部分都是独立的,并且引入了新的关键概念,这意味着在执行此操作之前,您不必完成所有其他部分。 但万一你好奇,以下是我们到目前为止的内容:


在第4部分中,我们介绍了如何使用store更新和良好的用户界面来处理示例应用的channel列表视图中的网络延迟。

在这一部分中,我们将构建一个频道详情视图,显示channel中的所有消息并允许您发布新消息。 到最后,你会知道如何:

  • 在查询中使用字段参数
  • 充分利用Apollo的标准缓存
  • 使用GraphQL输入类型

在开始之前,我们先克隆git仓库并安装依赖关系:

git clone https://github.com/apollographql/graphql-tutorial.gitcd graphql-tutorialgit checkout t5-startcd server && npm installcd ../client && npm install

为了确保它的工作,让我们启动服务器和客户端,每个都在一个单独的终端中:

cd servernpm start

在另一个终端:

cd clientnpm start

您现在可以导航到localhost:3000并浏览channel详情视图的当前状态。 我们已经为你做了一些工作,所以它应该看起来像这样:

由于本教程主要关注GraphQL,因此我们已经为您制作了channel详细信息视图的路由和模板。 您不需要知道它如何在本教程中使用,但是如果您好奇,我们会使用react-router(查看react-router tutorialdocumentation).

看起来我们已经完成了所有的工作,但新视图目前只是一个存根。 为了使其真正起作用,您需要编写一个GraphQL查询来获取来自服务器的channel名称和消息,并且您需要创建一个mutation来添加新消息。

添加channel详细信息视图

channel详情视图应显示channel名称,其信息和新的信息输入。 首先,我们修改架构并编写查询以显示当前channel中的消息。

在schema中(在服务器上),我们需要创建一个消息类型,将消息字段添加到channel类型,并提供一种通过向根查询类型添加channel字段来获取单个channel的方法。 在进行这些更改之后,在schema.js中输入Define应如下所示:

//server/src/schema.js
const typeDefs = type Channel {  id: ID!  name: String  messages: [Message]!}
type Message {  id: ID!  text: String}
# This type specifies the entry points into our APItype Query {  channels: [Channel]  channel(id: ID!): Channel}
# The mutation root type, used to define all mutationstype Mutation {  addChannel(name: String!): Channel};
const schema = makeExecutableSchema({ typeDefs, resolvers });export { schema };

请注意,我们获取单个channel的方式是将id添加为字段参数。 这是GraphQL中非常常见的模式,您可能会在您的应用程序中使用它。 参数可以是任何标量或输入类型,我们将在本教程后面详细介绍。

接下来,新查询需要由解析器支持,该解析器返回适当的channel。 为此,将加粗的查询添加到resolvers.js中:

//server/src/resolvers.js
const channels = [{  id: '1',  name: 'baseball',  messages: [{    id: '2',    text: 'baseball is life',  }]}];let nextId = 3;
export const resolvers = {  Query: {    channels: () => { ... },    channel: (root, { id }) => {      return channels.find(channel => channel.id === id);
},  },};

注意:我们为channel创建了一个预填充消息的数组。 如果你没有检查t5-start分支,你必须自己创建这个数组.

现在服务器支持查询特定channel,客户端 - 特别是ChannelDetails组件 - 需要执行查询。 GraphQL中的最佳做法是使用查询变量作为参数(在这种情况下,为$id)。 GraphQL规范要求我们在查询关键字后面定义我们使用的变量。 如果我们不这样做,服务器会抱怨我们使用了一个变量而没有定义它。 该定义必须与参数期望的类型相匹配。 在这种情况下,它是ID。

在channelDetails.js中写入以下查询:

//client/src/components/channelDetails.js
export const channelDetailsQuery = gql  query ChannelDetailsQuery($channelId : ID!) {    channel(id: $channelId) {      id      name      messages {        id        text      }    }  };

在ChannelDetails React组件中,将存根替换为呈现实际数据的代码。 首先检查查询是否正在加载(data.loading),然后检查以确保没有错误(data.error),并最终呈现channel名称和MessagesList。

如果你这样做了,你应该得到一个看起来像这样的组件:

//client/src/components/ChannelDetails.js
const ChannelDetails = ({ data: {loading, error, channel }, match }) => {  if (loading) {    return Loading...;
}  if (error) {    return {error.message};
}  if(channel === null){    return   }
  return (              {channel.name}                );}
//export const channelDetailsQuery = gql...;
// ...

现在,您只需使用之前编写的查询来打包组件,然后将其导出即可。

//client/src/components/ChannelDetails.js (at the bottom)
export default (graphql(channelDetailsQuery, {  options: (props) => ({    variables: { channelId: props.match.params.channelId },  }),})(ChannelDetails));

我们正在通往正常运行的可交流的应用程序,亲自尝试一下! 它应该是这样的:

现在我们有一个channel名称和消息流,让我们添加消息mutation来发布新消息。

发布新消息

创建函数AddMessage与第三部分添加频道非常相似, 所以首先建议使用带字段的mutation来显示消息文本和channel ID。 但是在将来,我们可能想要关联用户名,时间戳,文本编码,图片,提到的用户或其他元信息。 将这些添加到Mutation的签名中很快变得笨拙和不灵活。 为了保持整洁,我们将使用GraphQL输入类型, 这是一个只能包含基本标量类型,列表类型和其他输入类型的对象。 输入类型允许客户端mutation签名保持不变并在模式中提供更好的可读性。

从服务器开始,我们定义MessageInput输入类型,并在schema.js中包含mutation,如下所示:

//server/src/schema.js
input MessageInput{  channelId: ID!  text: String}
type Mutation {  # A mutation to add a new channel to the list of channels  addChannel(name: String!): Channel  addMessage(message: MessageInput!): Message}

resolver.js中的addMessage解析器应该检查输入

//server/src/resolvers.js
Mutation: {  addChannel: {...},  addMessage: (root, { message }) => {    const channel = channels.find(channel => channel.id === message.channelId);
if(!channel)      throw new Error("Channel does not exist");
    const newMessage = { id: String(nextMessageId++), text: message.text };
channel.messages.push(newMessage);
return newMessage;
},},

接下来在客户端,我们需要完成AddMessage.js,从查询开始:

//client/src/components/AddMessage.js
const addMessageMutation = gql  mutation addMessage($message: MessageInput!) {    addMessage(message: $message) {      id      text    }  };

AddMessage组件主体将变量添加到AddChannel的基本代码中,其中包括我们在其中使用的相同的UI功能 last tutorial. 唯一不同的部分是变量。 我在下面以粗体突出显示了这些变化:

//client/src/components/AddMessage.js
const AddMessage = ({ mutate, match }) => {  const handleKeyUp = (evt) => {    if (evt.keyCode === 13) {      mutate({        variables: {          message: {            channelId: match.params.channelId,            text: evt.target.value          }        },        optimisticResponse: {          addMessage: {            text: evt.target.value,            id: Math.round(Math.random() * -1000000),            __typename: 'Message',          },        },        update: (store, { data: { addMessage } }) => {          // Read the data from the cache for this query.          const data = store.readQuery({            query: channelDetailsQuery,            variables: {              channelId: match.params.channelId,            }          });
// Add our Message from the mutation to the end.          data.channel.messages.push(addMessage);
// Write the data back to the cache.          store.writeQuery({            query: channelDetailsQuery,            variables: {              channelId: match.params.channelId,            },            data          });
},      });
evt.target.value = '';
}  };
  return (     ...  );};
//const addMessageMutation = gql...
const AddMessageWithMutation = graphql(  addMessageMutation,)(withRouter(AddMessage));
export default AddMessageWithMutation;

Note: match 是react-router提供的url属性的接口 withRouter

现在我们有一个功能齐全的消息channel! 但是,有一个小问题:如果网络速度较慢,用户必须等待channel名称和消息从服务器加载。 在加载所有数据之前,用户甚至不知道他们在哪个channel,这是坏的UX。 理想情况下,我们希望用户在加载邮件时看到良好的channel预览。 这就是我们在本教程的最后部分要做的。

从缓存中读取频channel名称

正如您可能已经注意到的,客户端已经知道channel名称,因为它在主页上加载了ChannelNameListQuery。 如果有方法让我们保留channel名称,我们可以在不向服务器提出其他请求的情况下显示它!

幸运的是,Apollo Client会自动将每个查询结果存储在normalized cache中,这意味着我们可以查询数据 我们希望让Apollo Client找出它是否可以从缓存中加载。 但是,有一个小问题:

默认情况下,Apollo客户端使用查询路径(例如/channel(id:5)/name)来确定对象是否被缓存。

由于channels和channel查询导致到同一对象的路径不同,Apollo Client不知道它们是相同的,除非您明确告诉它channel查询可能解析为channel查询检索到的对象。 我们可以通过向App.js中的ApolloClient构造函数添加一个自定义解析器来告诉Apollo客户端这种关系。 每当我们进行channel查询时,这个自定义解析器就会告诉Apollo客户端检查它的缓存中是否有ID $ channelId的Channel对象。 如果它在缓存中找到具有该ID的channel,则不会向服务器发出请求。

以下自定义解析器在App.js中创建此映射:

//client/src/App.js
//function dataIdFromObject (result) {...}
const client = new ApolloClient({  networkInterface,  customResolvers: {    Query: {      channel: (_, args) => {        return toIdValue(dataIdFromObject({ __typename: 'Channel', id: args['id'] }))      },    },  },  dataIdFromObject,});

ApolloClient uses dataIdFromObject to tag GraphQL objects in the cache and toIdValue ensures an ID type is returned.

现在您只需要像通常那样创建ChannelPreview组件:

//client/src/components/ChannelPreview.js
const ChannelPreview = ({ data: {loading, error, channel } }) => {  return (    
      Loading Messages      );};
export const channelQuery = gql  query ChannelQuery($channelId : ID!) {    channel(id: $channelId) {      id      name    }  };
export default (graphql(channelQuery, {  options: (props) => ({    variables: { channelId: props.channelId },  }),})(ChannelPreview));

最后,我们需要用ChannelPreview组件替换ChannelDetails组件的加载消息:

//client/src/components/ChannelDetails.js
const ChannelDetails = (...) => {  if (loading) {    return ;
}

通过从缓存中提取数据,我们创建了一个channel详细视图,可以在后台加载消息的同时立即显示channel名称。

总结

恭喜,您现在有一个可以提供channel标记的消息流的应用程序! 该服务几乎已经准备好用于生产,经过几次改进之后:首先,我们需要一种使用GraphQL订阅实时显示消息的方法,从下一部分的服务器端开始. 其次,我们希望分页消息,因为一次加载所有消息可能会很慢。 最后,我们还需要添加登录和身份验证,以确保我们知道消息来自哪里。

如果您喜欢本教程并希望继续学习Apollo和GraphQL,请务必点击下面的“关注”按钮,然后在Twitter上关注我们 @apollographql 以及作者 @evanshauser.

非常感谢Jonas Helfer 的指导!