本章将介绍以下配方:
- 创建我们的第一个 GraphQL 服务器
- 使用 Apollo 和 GraphQL 创建 Twitter 时间线
GraphQL 是一种应用层查询语言,可用于任何数据库。它也是开源的(MIT 许可证),由 Facebook 创建。它与 REST 的主要区别在于 GraphQL 不使用端点,而是使用查询,大多数服务器语言都支持它,如 JavaScript(Node.js)、Go、Ruby、PHP、Java 和 Python。
现在我们来看看 GraphQL 和 REST 之间的主要区别。
图 ql:
- 查询是可读的
- 您可以在没有版本的情况下改进 API
- 类型系统
- 您可以避免多次往返以获取相关数据
- 很容易限制我们需要的数据集
其余:
- 在 REST 中,一切都是资源
- 休息是无计划的
- 您需要版本来改进 API
- 很难限制我们需要的数据集
- 如果您需要来自不同资源的数据,则需要发出多个请求
对于这个食谱,我们将创建一个联系人列表,在其中保存朋友的姓名、电话和电子邮件地址
我们需要做的第一件事是为我们的项目创建一个目录,并初始化一个新的package.json
文件安装express
、graphql
和express-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-env
和babel-cli
才能在节点中使用 ES6 语法。另外,我们需要创建一个.babelrc
文件:
{
"presets": ["env"]
}
File: .babelrc
让我们创建第一个 GraphQL 服务器:
- 首先,我们需要为我们的 Express 服务器创建一个
index.js
文件:
import express from 'express';
const app = express();
app.listen(3000, () => console.log('Running server on port 3000'));
File: index.js
- 如果您在终端中运行
babel-node index.js
,您应该能够看到节点服务器在端口 3000 上运行:
- 现在我们需要包括我们的
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
- 有了
expressGraphQL
和buildSchema
之后,让我们用第一个查询创建第一个 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
- 现在,让我们为联系人列表创建数据文件。我们可以制作一个数据目录和一个
contacts.json
文件:
{
"contacts": [
{
"id": 1,
"name": "Carlos Santana",
"phone": "281-323-4146",
"email": "[email protected]"
},
{
"id": 2,
"name": "Cristina",
"phone": "331-251-5673",
"email": "[email protected]"
},
{
"id": 3,
"name": "John Smith",
"phone": "415-307-4382",
"email": "[email protected]"
},
{
"id": 4,
"name": "John Brown",
"phone": "281-323-4146",
"email": "[email protected]"
}
]
}
File: data/contacts.json
- 我们现在需要添加获取数据的方法(
getContact
和getContacts
:
// 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。此外,如果我们发送一个空值,我们将获得所有联系人:
此外,我们可以开始使用片段,用于在queries
、mutations
和subscriptions
之间共享字段:
如您所见,我们使用想要获取的字段定义片段,然后在两个查询(contact1
和contact2
中,我们重复使用相同的片段(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 的开源基础设施。还有其他用于处理 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 后端服务器,以创建完成此项目所需的所有查询和变体。我们将在下面几节中看到如何做到这一点。
让我们从后端服务器开始:
- 首先,在
apollo
项目中(我们用create-react-app
创建的项目),我们需要创建一个名为backend
的新目录,初始化一个package.json
文件,并在src
文件夹中创建:
cd apollo
mkdir backend
cd backend
npm init -y
mkdir src
- 现在我们需要安装这些依赖项:
npm install cors express express-graphql graphql graphql-tools
mongoose nodemon babel-preset-es2015
npm install -g babel-cli
- 在我们的
package.json
文件中,我们需要修改启动脚本以使用nodemon
:
"scripts": {
"start": "nodemon src/app.js --watch src --exec babel-node
--presets es2015"
}
File: package.json
- 然后我们需要创建我们的
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
- 如您所见,我们已经包含了来自
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
- 创建查询文件后,我们需要添加解析程序。这些是为每个查询和变异执行的函数。我们还将使用
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
- 最后,我们需要创建我们的 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.
- 现在是关键时刻!如果您正确地遵循了所有步骤,那么如果转到
http://localhost:5000/graphiql
,您应该会看到 GraphiQL IDE 正在工作,但您可能会遇到以下错误:
- 通常,这个错误意味着我们在两个项目(前端和后端)中使用
graphql
,npm 不知道哪个版本将使用哪个版本。这是一个棘手的错误,但我将向您展示如何修复它。首先,我们从两个项目(前端和后端)中删除node_modules
文件夹。然后我们需要在两个package.json
文件中添加一个resolutions
节点:
"resolutions": {
"graphql": "0.13.2"
}
- 同时,我们还需要从两个
package.json
文件的graphql
版本中删除插入符号(^
。 - 现在我们必须删除
package-lock.json
和yarn.lock
文件(如果您有)。 - 在我们再次安装依赖项之前,最好将 npm 更新到最新版本:
npm install -g npm
- 之后,为了安全起见,让我们删除 npm 缓存:
npm cache clean --force
- 现在再次运行
npm install
(首先在后端),在使用npm start
运行项目之后,如果一切正常,您应该看到 GraphiQL IDE 工作正常:
现在我们已经准备好后端,让我们开始处理前端:
- 我们需要修改的第一个文件是
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
- 我们将后端端点连接到
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
- 我们需要做的第一件事是创建 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
- 是的,你看得对,这不是打字错误!调用该函数时不带括号,只使用反勾号(
gql
YOUR QUERY HERE``。getTweets
查询已经在我们的后端定义。我们正在执行`getTweets`查询,我们将获得字段(`_id`、`tweet`、`author`和`createdAt`。现在让我们创建我们的突变:
// 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
- 我总是喜欢进行重构和改进,这就是为什么我从
react-apollo
为Query
和Mutation
组件创建了两个助手。首先,让我们创建两个目录,shared
和shared/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
- 我们的突变成分应该是这样的:
// 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
- 一旦我们的助手准备好了,让我们为 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
- 这是我们的
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
- 我们的
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
- 最后,但同样重要的是,这是样式的文件:
.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.