九、Apollo 与 GraphQL

本章将介绍以下配方:

  • 创建我们的第一个 GraphQL 服务器
  • 使用 Apollo 和 GraphQL 创建 Twitter 时间线

介绍

GraphQL 是一种应用层查询语言,可用于任何数据库。它也是开源的(MIT 许可证),由 Facebook 创建。它与 REST 的主要区别在于 GraphQL 不使用端点,而是使用查询,大多数服务器语言都支持它,如 JavaScript(Node.js)、Go、Ruby、PHP、Java 和 Python。

现在我们来看看 GraphQL 和 REST 之间的主要区别。

图 ql:

  • 查询是可读的
  • 您可以在没有版本的情况下改进 API
  • 类型系统
  • 您可以避免多次往返以获取相关数据
  • 很容易限制我们需要的数据集

其余:

  • 在 REST 中,一切都是资源
  • 休息是无计划的
  • 您需要版本来改进 API
  • 很难限制我们需要的数据集
  • 如果您需要来自不同资源的数据,则需要发出多个请求

创建我们的第一个 GraphQL 服务器

对于这个食谱,我们将创建一个联系人列表,在其中保存朋友的姓名、电话和电子邮件地址

准备

我们需要做的第一件事是为我们的项目创建一个目录,并初始化一个新的package.json文件安装expressgraphqlexpress-graphql

 mkdir contacts-graphql
 cd contacts-graphql
 npm init --yes
 npm install express graphql express-graphql babel-preset-env
 npm install -g babel-cli

我们需要安装babel-preset-envbabel-cli才能在节点中使用 ES6 语法。另外,我们需要创建一个.babelrc文件:

  {
    "presets": ["env"]
  }

File: .babelrc

怎么做。。。

让我们创建第一个 GraphQL 服务器:

  1. 首先,我们需要为我们的 Express 服务器创建一个index.js文件:
  import express from 'express';

  const app = express();

  app.listen(3000, () => console.log('Running server on port 3000'));

File: index.js

  1. 如果您在终端中运行babel-node index.js,您应该能够看到节点服务器在端口 3000 上运行:

  1. 现在我们需要包括我们的express-graphql库,并从graphql导入buildSchema方法:
    import express from 'express';
    import expressGraphQL from 'express-graphql';
    import { buildSchema } from 'graphql';

    const app = express();

    app.listen(3000, () => console.log('Running server on port 3000'));

File: index.js

  1. 有了expressGraphQLbuildSchema之后,让我们用第一个查询创建第一个 GraphQL 服务器:
  // Dependencies
  import express from 'express';
  import expressGraphQL from 'express-graphql';
  import { buildSchema } from 'graphql';

  // Express Application
  const app = express();

  // Creating our GraphQL Schema
  const schema = buildSchema(`
    type Query {
      message: String
    }
  `);

  // Root has the methods we will execute to get the data
  const root = {
    message: () => 'First message'
  };

  // GraphQL middleware
  app.use('/graphql', expressGraphQL({
    schema,
    rootValue: root,
    graphiql: true // This enables the GraphQL browser's IDE
  }));

  // Running our server
  app.listen(3000, () => console.log('Running server on port 3000'));

File: index.js

  1. 现在,让我们为联系人列表创建数据文件。我们可以制作一个数据目录和一个contacts.json文件:
    {
      "contacts": [
        {
          "id": 1,
          "name": "Carlos Santana",
          "phone": "281-323-4146",
          "email": "carlos@milkzoft.com"
        },
        {
          "id": 2,
          "name": "Cristina",
          "phone": "331-251-5673",
          "email": "cristina@gmail.com"
        },
        {
          "id": 3,
          "name": "John Smith",
          "phone": "415-307-4382",
          "email": "john.smith@gmail.com"
        },
        {
          "id": 4,
          "name": "John Brown",
          "phone": "281-323-4146",
          "email": "john.brown@gmail.com"
        }
      ]
    }

File: data/contacts.json

  1. 我们现在需要添加获取数据的方法(getContactgetContacts
      // Dependencies
      import express from 'express';
      import expressGraphQL from 'express-graphql';
      import { buildSchema } from 'graphql';

      // Contacts Data
      import { contacts } from './data/contacts';

      // Express Application
      const app = express();

      // Creating our GraphQL Schema
      const schema = buildSchema(`
        type Query {
          contact(id: Int!): Contact
          contacts(name: String): [Contact]
        }

        type Contact {
          id: Int
          name: String
          phone: String
          email: String
        }
      `);

      // Data methods
      const methods = {
        getContact: args => {
          const { id } = args;

          return contacts.filter(contact => contact.id === id)[0];
        },
        getContacts: args => {
          const { name = false } = args;

          // If we don't get a name we return all contacts
          if (!name) {
            return contacts;
          }

          // Returning contacts with same name...
          return contacts.filter(
            contact => contact.name.includes(name)
          );
        }
      };

      // Root has the methods we will execute to get the data
      const root = {
        contact: methods.getContact,
        contacts: methods.getContacts
      };

      // GraphQL middleware
      app.use('/graphql', expressGraphQL({
        schema,
        rootValue: root,
        graphiql: true // This enables the GraphQL GUI
      }));

      // Runnign our server
      app.listen(3000, () => console.log('Running server on port 3000'));

File: index.js

它是如何工作的。。。

如果您运行服务器并转到 URLhttp://localhost:3000/graphql,您将看到 GraphiQL IDE,默认情况下,您将看到消息查询,如果您单击播放按钮,您将看到带有消息“First message”的数据:

现在在 GraphiQL IDE 中,我们需要为我们的contactId创建一个查询并添加一个查询变量,以获取单个联系人:

现在对于我们的getContacts查询,我们需要传递contactName变量:

如您所见,如果我们将 John 作为contactName发送,查询将返回我们的两行,分别是 John Smith 和 John Brown。此外,如果我们发送一个空值,我们将获得所有联系人:

此外,我们可以开始使用片段,用于在queriesmutationssubscriptions之间共享字段:

如您所见,我们使用想要获取的字段定义片段,然后在两个查询(contact1contact2中,我们重复使用相同的片段(contactFields。在查询变量中,我们传递要获取数据的联系人的值

还有更多。。。

突变也很重要,因为它们帮助我们修改数据。让我们实现一个变异,并通过传递 ID 和要更改的字段来更新联系人。

我们需要添加突变定义并创建更新联系人的功能;我们的代码应该如下所示:

  // Dependencies
  import express from 'express';
  import expressGraphQL from 'express-graphql';
  import { buildSchema } from 'graphql';

  // Contacts Data
  import { contacts } from './data/contacts';

  // Express Application
  const app = express();

  // Creating our GraphQL Schema
  const schema = buildSchema(`
    type Query {
      contact(id: Int!): Contact
      contacts(name: String): [Contact]
    }

    type Mutation {
      updateContact(
 id: Int!, 
 name: String!, 
 phone: String!, 
 email: String!
      ): Contact
    }

    type Contact {
      id: Int
      name: String
      phone: String
      email: String
    }
  `);

  // Data methods
  const methods = {
    getContact: args => {
      const { id } = args;

      return contacts.filter(contact => contact.id === id)[0];
    },
    getContacts: args => {
      const { name = false } = args;

      // If we don't get a name we return all contacts
      if (!name) {
        return contacts;
      }

      // Returning contacts with same name...
      return contacts.filter(contact => contact.name.includes(name));
    },
    updateContact: ({ id, name, phone, email }) => {
      contacts.forEach(contact => {
        if (contact.id === id) {
          // Updating only the fields that has new values...
          contact.name = name || contact.name;
          contact.phone = phone || contact.phone;
          contact.email = email || contact.email;
        }
      });

      return contacts.filter(contact => contact.id === id)[0];
    }
  };

  // Root has the methods we will execute to get the data
  const root = {
    contact: methods.getContact,
    contacts: methods.getContacts,
    updateContact: methods.updateContact
  };

  // GraphQL middleware
  app.use('/graphql', expressGraphQL({
    schema,
    rootValue: root,
    graphiql: true // This enables the GraphQL GUI
  }));

  // Running our server
  app.listen(3000, () => console.log('Running server on port 3000'));

File: index.js

现在,让我们在 GraphiQL 中创建突变并更新联系人:

使用 Apollo 和 GraphQL 创建 Twitter 时间线

Apollo 是 GraphQL 的开源基础设施。还有其他用于处理 GraphQL 的库,例如中继和通用反应查询库(URQL)。这些库的主要问题是它们主要用于 React 应用,而 Apollo 可以与任何其他技术或框架一起工作。

准备

对于此配方,我们将使用create-react-app创建一个新的 React 应用:

 create-react-app apollo

我们需要通过执行以下命令弹出配置:

 npm run eject

eject命令将把react-scripts的所有配置带到本地项目(Webpack 配置)。

现在我们需要安装以下软件包:

 npm install apollo-boost graphql graphql-tag moment mongoose react-
  apollo

我们还需要安装这些开发包:

 npm install --save-dev babel-preset-react babel-preset-stage-0

然后我们需要添加一个resolutions节点来指定我们将要使用的 GraphQL 的确切版本。这是为了避免版本冲突。graphql的当前版本为0.13.2。当然,在阅读本文时,您需要指定 GraphqQL 的最新版本:

 "resolutions": {
   "graphql": "0.13.2"
 }

另外,我们需要删除package.json中的babel节点。

 "babel": {
   "presets": [
     "react-app"
   ]
 }

File: package.json

然后,最后,我们需要创建一个具有以下内容的.babelrc文件:

 {
   "presets": ["react", "stage-0"]
 }

File: .babelrc

在开始实际操作之前,我们需要先创建 GraphQL 后端服务器,以创建完成此项目所需的所有查询和变体。我们将在下面几节中看到如何做到这一点。

创建 GraphQL 后端服务器

让我们从后端服务器开始:

  1. 首先,在apollo项目中(我们用create-react-app创建的项目),我们需要创建一个名为backend的新目录,初始化一个package.json文件,并在src文件夹中创建:
 cd apollo
 mkdir backend
 cd backend
 npm init -y
 mkdir src
  1. 现在我们需要安装这些依赖项:
 npm install cors express express-graphql graphql graphql-tools 
      mongoose nodemon babel-preset-es2015

 npm install -g babel-cli
  1. 在我们的package.json文件中,我们需要修改启动脚本以使用nodemon
      "scripts": {
        "start": "nodemon src/app.js --watch src --exec babel-node 
        --presets es2015"
      }

File: package.json

  1. 然后我们需要创建我们的app.js文件,我们将在其中创建我们的 GraphQL 中间件:
  // Dependencies
  import express from 'express';
  import expressGraphQL from 'express-graphql';
  import cors from 'cors';
  import graphQLExpress from 'express-graphql';
  import { makeExecutableSchema } from 'graphql-tools';

  // Query
  import { typeDefs } from './types/Query';
  import { resolvers } from './types/Resolvers';

  // Defining our schema with our typeDefs and resolvers
  const schema = makeExecutableSchema({
    typeDefs,
    resolvers
  });

  // Intializing our express app
  const app = express();

  // Using cors
  app.use(cors());

  // GraphQL Middleware
  app.use('/graphiql', graphQLExpress({
    schema,
    pretty: true,
    graphiql: true
  }));

  // Listening port 5000
  app.listen(5000);

  console.log('Server started on port 5000');

File: src/app.js

  1. 如您所见,我们已经包含了来自types文件夹的 typeDefs 和 Resolver,因此让我们创建该目录并创建查询文件:
 export const typeDefs = [`
    # Scalar Types (custom type)
    scalar DateTime

    # Tweet Type (should match our Mongo schema)
    type Tweet {
      _id: String
      tweet: String
      author: String
      createdAt: DateTime
    }

    # Query
    type Query {
      # This query will return a single Tweet
      getTweet(_id: String): Tweet 

      # This query will return an array of Tweets
      getTweets: [Tweet]
    }

    # Mutations
    type Mutation {
      # DateTime is a custom Type
      createTweet(
        tweet: String,
        author: String,
        createdAt: DateTime 
      ): Tweet

      # Mutation to delete a Tweet
      deleteTweet(_id: String): Tweet

      # Mutation to update a Tweet (! means mandatory).
      updateTweet(
        _id: String!,
        tweet: String!
      ): Tweet
    }

    # Schema
    schema {
      query: Query
      mutation: Mutation
    }
  `];

File: src/types/Query.js

  1. 创建查询文件后,我们需要添加解析程序。这些是为每个查询和变异执行的函数。我们还将使用GraphQLScalarType定义我们的自定义DateTime类型:
 // Dependencies
 import { GraphQLScalarType } from 'graphql';
 // TweetModel (Mongo Schema)
 import TweetModel from '../model/Tweet';
 // Resolvers
 export const resolvers = {
    Query: {
      // Receives an _id and returns a single Tweet.
      getTweet: _id => TweetModel.getTweet(_id),
      // Gets an array of Tweets.
      getTweets: () => TweetModel.getTweets()
    },
    Mutation: {
      // Creating a Tweet passing the args (Tweet object), the _ is    
      // the root normally is undefined
      createTweet: (_, args) => TweetModel.createTweet(args),
      // Deleting a Tweet passing in the args the _id of the Tweet 
      // we want to remove
      deleteTweet: (_, args) => TweetModel.deleteTweet(args),
      // Updating a Tweet passing the new values of the Tweet we 
      // want to update
      updateTweet: (_, args) => TweetModel.updateTweet(args)
    },
    // This DateTime will return the current date.
    DateTime: new GraphQLScalarType({
      name: 'DateTime',
      description: 'Date custom scalar type',
      parseValue: () => new Date(),
      serialize: value => value,
      parseLiteral: ast => ast.value
    })
  };

File: src/types/Resolvers.js

  1. 最后,我们需要创建我们的 tweet 模型:
  // Dependencies
  import mongoose from 'mongoose';

  // Connecting to Mongo
  mongoose.Promise = global.Promise;
  mongoose.connect('mongodb://localhost:27017/twitter', {
    useNewUrlParser: true
  });

  // Getting Mongoose Schema
  const Schema = mongoose.Schema;

  // Defining our Tweet schema
  const tweetSchema = new Schema({
    tweet: String,
    author: String,
    createdAt: Date,
  });

  // Creating our Model
  const TweetModel = mongoose.model('Tweet', tweetSchema);

  export default {
    // Getting all the tweets and sorting descending
    getTweets: () => TweetModel.find().sort({ _id: -1 }),
    // Getting a single Tweet using the _id
    getTweet: _id => TweetModel.findOne({ _id }),
    // Saving a Tweet
    createTweet: args => TweetModel(args).save(),
    // Removing a Tweet by _id
    deleteTweet: args => {
      const { _id } = args;

      TweetModel.remove({ _id }, error => {
        if (error) {
          console.log('Error Removing:', error);
        }
      });

      // Even when we removed a tweet we need to return the object 
      // of the tweet
      return args;
    },
    // Updating a Tweet (just the field tweet will be updated)
    updateTweet: args => {
      const { _id, tweet } = args;

      // Searching by _id and then update tweet field.
      TweetModel.update({ _id }, {
        $set: {
          tweet
        }
      },
      { upsert: true }, error => {
        if (error) {
          console.log('Error Updating:', error);
        }
      });

      // This is hard coded for now
      args.author = 'codejobs';
      args.createdAt = new Date();

      // Returning the updated Tweet 
      return args;
    }
  };

File: src/model/Tweet.js You need to have MongoDB installed and running to use this project. If you don't know how to do this, you can look at Chapter 8, Creating an API with Node.js Using MongoDB and MySQL.

  1. 现在是关键时刻!如果您正确地遵循了所有步骤,那么如果转到http://localhost:5000/graphiql,您应该会看到 GraphiQL IDE 正在工作,但您可能会遇到以下错误:

  1. 通常,这个错误意味着我们在两个项目(前端和后端)中使用graphql,npm 不知道哪个版本将使用哪个版本。这是一个棘手的错误,但我将向您展示如何修复它。首先,我们从两个项目(前端和后端)中删除node_modules文件夹。然后我们需要在两个package.json文件中添加一个resolutions节点:
  "resolutions": {
     "graphql": "0.13.2"
   }
  1. 同时,我们还需要从两个package.json文件的graphql版本中删除插入符号(^
  2. 现在我们必须删除package-lock.jsonyarn.lock文件(如果您有)。
  3. 在我们再次安装依赖项之前,最好将 npm 更新到最新版本:
     npm install -g npm
  1. 之后,为了安全起见,让我们删除 npm 缓存:
    npm cache clean --force
  1. 现在再次运行npm install(首先在后端),在使用npm start运行项目之后,如果一切正常,您应该看到 GraphiQL IDE 工作正常:

怎么做。。。

现在我们已经准备好后端,让我们开始处理前端:

  1. 我们需要修改的第一个文件是index.js文件:
    // Dependencies
    import React from 'react';
    import { render } from 'react-dom';
    import ApolloClient from 'apollo-boost';
    import { ApolloProvider } from 'react-apollo';

    // Components
    import App from './App';

    // Styles
    import './index.css';

    // Apollo Client
    const client = new ApolloClient({
      uri: 'http://localhost:5000/graphiql' // Backend endpoint
    });

    // Wrapping the App with ApolloProvider
    const AppContainer = () => (
      <ApolloProvider client={client}>
        <App />
      </ApolloProvider>
    );

    // Root
    const root = document.getElementById('root');

    // Rendering the AppContainer
    render(<AppContainer />, root);

File: src/index.js

  1. 我们将后端端点连接到ApolloClient,并将<App />组件包装为<ApolloProvider>(是的,这类似于 Redux 提供程序)。现在,让我们修改我们的App.js文件,以包括我们的主要组件(Tweets):
  // Dependencies
  import React, { Component } from 'react';

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

  // Styles
  import './App.css';

  class App extends Component {
    render() {
      return (
        <div className="App">
          <Tweets />
        </div>
      );
    }
  }

 export default App;

File: src/App.js

  1. 我们需要做的第一件事是创建 GraphQL 查询和变体。为此,我们需要创建一个名为graphql的新目录,其中还有两个目录,一个用于mutations,另一个用于queries
 // Dependencies
 import gql from 'graphql-tag';

 // getTweets query
 export const QUERY_GET_TWEETS = gql`
    query getTweets {
      getTweets {
        _id
        tweet
        author
        createdAt
      }
    }
  `;

File: src/graphql/queries/index.js

  1. 是的,你看得对,这不是打字错误!调用该函数时不带括号,只使用反勾号(gqlYOUR QUERY HERE`。getTweets查询已经在我们的后端定义。我们正在执行getTweets查询,我们将获得字段(_idtweetauthorcreatedAt`。现在让我们创建我们的突变:
  // Dependencies
  import gql from 'graphql-tag';

  // createTweet Mutation
  export const MUTATION_CREATE_TWEET = gql`
    mutation createTweet(
      $tweet: String,
      $author: String,
      $createdAt: DateTime
    ) {
      createTweet(
        tweet: $tweet,
        author: $author,
        createdAt: $createdAt
      ) {
        _id
        tweet
        author
        createdAt
      }
    }
  `;

  // deleteTweet Mutation
  export const MUTATION_DELETE_TWEET = gql`
    # ! means mandatory
    mutation deleteTweet($_id: String!) {
      deleteTweet(
        _id: $_id
      ) {
        _id
        tweet
        author
        createdAt
      }
    }
  `;

  // updateTweet Mutation
 export const MUTATION_UPDATE_TWEET = gql`
    mutation updateTweet(
      $_id: String!,
      $tweet: String!
    ) {
      updateTweet(
        _id: $_id,
        tweet: $tweet
      ) {
        _id
        tweet
        author
        createdAt
      }
    }
  `;

File: src/graphql/mutations/index.js

  1. 我总是喜欢进行重构和改进,这就是为什么我从react-apolloQueryMutation组件创建了两个助手。首先,让我们创建两个目录,sharedshared/components。首先,这是我们的查询组件:
  // Dependencies
  import React, { Component } from 'react';
  import { Query as ApolloQuery } from 'react-apollo';

  class Query extends Component {
    render() {
      const {
        query,
        render: Component
      } = this.props;

      return (
        <ApolloQuery query={query}>
          {({ loading, error, data }) => {
            if (loading) {
              return <p>Loading...</p>;
            }

            if (error) {
              return <p>Query Error: {error}</p>
            }

            return <Component data={data || false} />;
          }}
        </ApolloQuery>
      );
    }
  }

  export default Query;

File: src/shared/components/Query.js

  1. 我们的突变成分应该是这样的:
  // Dependencies
  import React, { Component } from 'react';
  import { Mutation as ApolloMutation } from 'react-apollo';

  class Mutation extends Component {
    render() {
      const {
        mutation,
        query,
        children,
        onCompleted
      } = this.props;

      return (
        <ApolloMutation
          mutation={mutation}
          update={(cache, { data }) => {
            // Getting the mutation and query name
            const { 
              definitions: [{ name: { value: mutationName } }] 
            } = mutation;
            const { 
              definitions: [{ name: { value: queryName } }] 
            } = query;

            // Getting cachedData from previous query
            const cachedData = cache.readQuery({ query });

            // Getting current data (result of the mutation)
            const current = data[mutationName];

            // Initializing our updatedData
            let updatedData = [];

            // Lower case mutation name
            const mutationNameLC = mutationName.toLowerCase();

            // If the mutation includes "delete" or "remove"
            if (mutationNameLC.includes('delete') 
              || mutationNameLC.includes('remove')) {
              // Removing the current tweet by filtering 
              // from the cachedData
              updatedData = cachedData[queryName].filter(
                row => row._id !== current._id
              );
            } else if (mutationNameLC.includes('create') 
 || mutationNameLC.includes('add')) {
              // Create or add action injects the current 
              // value in the array
              updatedData = [current, ...cachedData[queryName]];
            } else if (mutationNameLC.includes('edit') 
 || mutationNameLC.includes('update')) {
              // Edit or update actions will replace the old values 
              // with the new ones
              const index = cachedData[queryName].findIndex(
                row => row._id === current._id
              );
              cachedData[queryName][index] = current;
              updatedData = cachedData[queryName];
            }

            // Updating our data to refresh the tweets list
            cache.writeQuery({
              query,
              data: {
                [queryName]: updatedData
              }
            });
          }}
          onCompleted={onCompleted} 
        >
          {/** 
            * Here we render the content of the 
            * component (children) 
            */}
          {children}
        </ApolloMutation>
      );
    }
  }

  export default Mutation;

File: src/shared/components/Mutation.js

  1. 一旦我们的助手准备好了,让我们为 Tweet、Tweet 和 CreateTweet 创建组件。这是我们的Tweets组件:
 // Dependencies
  import React, { Component } from 'react';

  // Components
  import Tweet from './Tweet';
  import CreateTweet from './CreateTweet';
  import Query from '../shared/components/Query';

  // Queries
  import { QUERY_GET_TWEETS } from '../graphql/queries';

  // Styles
  import './Tweets.css';

  class Tweets extends Component {
    render() {
      return (
        <div className="tweets">
          {/* Rendering CreateTweet component */}
          <CreateTweet />

          {/** 
            * Executing QUERY_GET_TWEETS query and render our Tweet 
            * component 
            */}
          <Query query={QUERY_GET_TWEETS} render={Tweet} />
        </div>
      );
    }
  }

  export default Tweets;

File: src/components/Tweets.js

  1. 这是我们的Tweet组件:
    // Dependencies
    import React, { Component } from 'react';
    import moment from 'moment';

    // Components
    import Mutation from '../shared/components/Mutation';

 // Queries
    import { 
      MUTATION_DELETE_TWEET, 
      MUTATION_UPDATE_TWEET 
    } from '../graphql/mutations';

    import { QUERY_GET_TWEETS } from '../graphql/queries';

    // Images (those are temporary images and exists on the repository)
    import TwitterLogo from './twitter.svg';
    import CodejobsAvatar from './codejobs.png';

    class Tweet extends Component {
      // Local State
      state = {
        currentTweet: false
      };

      // Enabling a textarea for edit a Tweet
      handleEditTweet = _id => {
        const { data: { getTweets: tweets } } = this.props;

        const selectedTweet = tweets.find(tweet => tweet._id === _id);

        const currentTweet = {
          [_id]: selectedTweet.tweet
        };

        this.setState({
          currentTweet
        });
      }

      // Handle Change for textarea
      handleChange = (value, _id) => {
        const { currentTweet } = this.state;

        currentTweet[_id] = value;

        this.setState({
          currentTweet
        });
      }

      // Delete tweet mutation
      handleDeleteTweet = (mutation, _id) => {
        // Sending variables
        mutation({
          variables: {
            _id
          }
        });
      }

      // Update tweet mutation
      handleUpdateTweet = (mutation, value, _id) => {
        // Sending variables
        mutation({
          variables: {
            _id,
            tweet: value
          }
        });
      }

      render() {
        // Getting the data from getTweets query
        const { data: { getTweets: tweets } } = this.props;

        // currentTweet state
        const { currentTweet } = this.state;

        // Mapping the tweets
        return tweets.map(({
          _id,
          tweet,
          author,
          createdAt
        }) => (
          <div className="tweet" key={`tweet-${_id}`}>
            <div className="author">
              {/* Rendering our Twitter Avatar (this is hardcoded) */}
              <img src={CodejobsAvatar} alt="Codejobs" />

              {/* Rendering the author */}
              <strong>{author}</strong>
            </div>

            <div className="content">
              <div className="twitter-logo">
                {/* Rendering the Twitter Logo */}
                <img src={TwitterLogo} alt="Twitter" />
              </div>

              {/**
 * If there is no currentTweet being edited then  
                * we display the tweet as a text otherwise we 
                * render a textarea with the tweet to be edited
 */}
              {!currentTweet[_id]
                ? tweet
                : (
                  <Mutation
                    mutation={MUTATION_UPDATE_TWEET}
                    query={QUERY_GET_TWEETS}
                    onCompleted={() => {
                      // Once the mutation is completed we clear our 
                      // currentTweet state
                      this.setState({
                        currentTweet: false
                      });
                    }}
                  >
                    {(updateTweet) => (
                      <textarea
                        autoFocus
                        className="editTextarea"
                        value={currentTweet[_id]}
                        onChange={(e) => {
                          this.handleChange(
                            e.target.value, 
 _id                          ); 
                        }}
                        onBlur={(e) => {
                          this.handleUpdateTweet(
 updateTweet, 
                            e.target.value, 
 _id                          ); 
                        }}
                      />
                    )}
                  </Mutation>
                )
              }
            </div>

            <div className="date">
              {/* Rendering the createdAt date (MMM DD, YYYY) */}
              {moment(createdAt).format('MMM DD, YYYY')}
            </div>

            {/* Rendering edit icon */}
            <div 
              className="edit" 
 onClick={() => { 
                this.handleEditTweet(_id); 
              }}
            >
              <i className="fa fa-pencil" aria-hidden="true" />
            </div>

            {/* Mutation for delete a tweet */}
            <Mutation
              mutation={MUTATION_DELETE_TWEET}
              query={QUERY_GET_TWEETS}
            >
              {(deleteTweet) => (
                <div 
                  className="delete" 
 onClick={() => {
                    this.handleDeleteTweet(deleteTweet, _id); 
                  }}
                >
                  <i className="fa fa-trash" aria-hidden="true" />
                </div>
              )}
            </Mutation>
          </div>
        ));
      }
    }

 export default Tweet;

File: src/components/Tweet.js

  1. 我们的CreateTweet组件如下:
  // Dependencies
  import React, { Component } from 'react';
  import Mutation from '../shared/components/Mutation';

  // Images (this image is on the repository)
  import CodejobsAvatar from './codejobs.png';

  // Queries
  import { MUTATION_CREATE_TWEET } from '../graphql/mutations';
  import { QUERY_GET_TWEETS } from '../graphql/queries';

  class CreateTweet extends Component {
    // Local state
    state = {
      tweet: ''
    };

    // Handle change for textarea
    handleChange = e => {
      const { target: { value } } = e;

      this.setState({
        tweet: value
      })
    }

    // Executing createTweet mutation to add a new Tweet
    handleSubmit = mutation => {
      const tweet = this.state.tweet;
      const author = '@codejobs';
      const createdAt = new Date();

      mutation({
        variables: {
          tweet,
          author,
          createdAt
        }
      });
    }

    render() {
      return (
        <Mutation
          mutation={MUTATION_CREATE_TWEET}
          query={QUERY_GET_TWEETS}
          onCompleted={() => {
            // On mutation completed we clean the tweet state 
            this.setState({
              tweet: ''
            });
          }}
        >
          {(createTweet) => (
            <div className="createTweet">
              <header>
                Write a new Tweet
              </header>

              <section>
                <img src={CodejobsAvatar} alt="Codejobs" />

                <textarea
                  placeholder="Write your tweet here..."
                  value={this.state.tweet}
                  onChange={this.handleChange}
                />
              </section>

              <div className="publish">
                <button
                  onClick={() => {
                    this.handleSubmit(createTweet);
                  }}
                >
                  Tweet it!
                </button>
              </div>
            </div>
          )}
        </Mutation>
      );
    }
  }

 export default CreateTweet;

File: src/components/CreateTweet.js

  1. 最后,但同样重要的是,这是样式的文件:
  .tweet {
    margin: 20px auto;
    padding: 20px;
    border: 1px solid #ccc;
    height: 200px;
    width: 80%;
    position: relative;
  }

  .author {
    text-align: left;
    margin-bottom: 20px;
  }

  .author strong {
    position: absolute;
    top: 40px;
    margin-left: 10px;
  }

  .author img {
    width: 50px;
    border-radius: 50%;
  }

  .content {
    text-align: left;
    color: #222;
    text-align: justify;
    line-height: 25px;
  }

  .date {
    color: #aaa;
    font-size: 12px;
    position: absolute;
    bottom: 10px;
  }

  .twitter-logo img {
    position: absolute;
    right: 10px;
    top: 10px;
    width: 20px;
  }

  .createTweet {
    margin: 20px auto;
    background-color: #F5F5F5;
    width: 86%;
    height: 225px;
    border: 1px solid #AAA;
  }

  .createTweet header {
    color: white;
    font-weight: bold;
    background-color: #2AA3EF;
    border-bottom: 1px solid #AAA;
    padding: 20px;
  }

  .createTweet section {
    padding: 20px;
    display: flex;
  }

  .createTweet section img {
    border-radius: 50%;
    margin: 10px;
    height: 50px;
  }

  textarea {
    border: 1px solid #ddd;
    height: 80px;
    width: 100%;
  }

  .publish {
    margin-bottom: 20px;
  }

  .publish button {
    cursor: pointer;
    border: 1px solid #2AA3EF;
    background-color: #2AA3EF;
    padding: 10px 20px;
    color: white;
    border-radius: 20px;
    float: right;
    margin-right: 20px;
  }

  .delete {
    position: absolute;
    right: 10px;
    bottom: 10px;
    cursor: pointer;
  }

  .edit {
    position: absolute;
    right: 30px;
    bottom: 10px;
    cursor: pointer;
  }

File: src/components/Tweets.css

它是如何工作的。。。

如果所有操作都正确,并且运行前端和后端(每个都在不同的终端上),则可以在http://localhost:3000处运行项目,您应该会看到以下视图:

现在我们可以创建新的推文,将它们写在文本区域并点击推文!按钮:

如你所见,tweet 的顺序在下降。这意味着最新的 tweet 发布在顶部。如果要编辑推文,可以单击编辑图标(铅笔):

保存更改的方法是删除文本区域上的焦点(onBlur),现在我们可以看到更新的 tweet:

最后,如果您想删除一条推文,请单击垃圾箱图标(我已删除第二条推文):

正如您所看到的,突变非常容易实现,有了助手,我们简化了这个过程。

You're probably thinking that there's some way to use Redux with GraphQL, but let me tell you that it is possible that GraphQL will replace Redux because we have access to the data through the ApolloProvider.