如何使用React、Prisma和GraphQL构建食谱应用程序

来自菜鸟教程
跳转至:导航、​搜索

介绍

GraphQL 在前端开发方面受到欢迎,因为它比 REST APIs 提供了各种优势。 但是,设置自己的 GraphQL 服务器既容易出错又复杂。 因此,已经使用 Prisma 等托管服务来管理您的 GraphQL 服务器,让您可以专注于应用程序的开发。

在本教程中,我们将使用 ReactPrisma 构建一个功能齐全的食谱应用程序来管理 GraphQL。

先决条件

  • Javascript和React的中级知识
  • GraphQL 基础知识
  • Docker 基础知识

第 1 步 — 安装依赖项

通过运行以下命令全局安装 Prisma CLI 客户端:

npm install -g prisma

我们将使用 create-react-app 来引导我们的 React 应用程序,因此运行以下命令以全局安装它:

npm install -g create-react-app

要在本地使用 Prisma,您需要在您的机器上安装 Docker。 如果您还没有 Docker,可以下载 Docker 社区版

第 2 步 — 设置 Prisma

要使用 Prisma CLI,您需要有一个 Prisma 帐户。 您可以在 Prisma 网站 创建一个帐户 ,然后通过运行以下命令登录到 Prisma CLI:

prisma login

现在我们有了所有必需的依赖项,为项目创建一个文件夹并通过运行以下命令导航到该文件夹:

mkdir recipe-app-prisma-react 
cd recipe-app-prisma-react

然后在文件夹中初始化您的 Prisma 服务器:

prisma init

将出现一个提示,其中包含一些选项,说明您要使用哪种方法来设置您的 prisma 服务器。 我们现在将在本地使用服务器,然后再部署它。 选择 Create new database 让 Prisma 使用 Docker 在本地创建数据库。

接下来,您将收到选择数据库的提示。 对于本教程,我们将使用 Postgres,因此选择 PostgreSQL

接下来我们必须为我们生成的 prisma 客户端选择一种编程语言。 选择Prisma Javascript Client

您将拥有 Prisma 基于所选选项生成的以下文件:

第 3 步 — 部署 Prisma

现在我们已经设置了 Prisma 服务器,请确保 docker 正在运行。 然后,运行以下命令来启动服务器:

docker-compose up -d

Docker compose 用于将多个容器作为单个服务运行。 前面的命令将启动我们的 Prisma 服务器和 Postgres 数据库。 在浏览器中前往 127.0.0.1:4466 以查看 Prisma 游乐场。

如果要停止服务器,请运行 docker-compose stop

接下来,打开您的 datamodel.prisma 文件并将演示内容替换为以下内容:

type Recipe {
  id: ID! @unique
  createdAt: DateTime!
  updatedAt: DateTime!
  title: String! @unique
  ingredients: String!
  directions: String!
  published: Boolean! @default(value: "false")
}

然后运行以下命令部署到演示服务器:

prisma deploy

您将收到显示创建的模型和您的 Prisma 端点的响应,如下所示:

要查看已部署的服务器,请在 https://app.prisma.io/ 打开 Prisma 仪表板并导航到服务。 您将在仪表板中显示以下内容:

要部署到本地服务器,请打开 prisma.yml 文件并将端点更改为 http://localhost:4466,然后运行 prisma deploy

第 4 步 — 设置 React 应用程序

现在我们的 Prisma 服务器已经准备就绪,我们可以设置我们的 React 应用程序来使用 Prisma GraphQL 端点。

在项目文件夹中,运行以下命令以使用 create-react-app 引导我们的客户端应用程序:

create-react-app client

要使用 GraphQL,我们需要一些依赖项。 导航到客户端文件夹并运行以下命令来安装它们:

cd client
npm install apollo-boost react-apollo graphql-tag graphql --save

对于 UI,我们将使用 Ant Design

npm install antd --save

文件夹结构:

我们的应用程序文件夹结构如下:

src
├── components
│   ├── App.js
│   ├── App.test.js
│   ├── RecipeCard
│   │   ├── RecipeCard.js
│   │   └── index.js
│   └── modals
│       ├── AddRecipeModal.js
│       └── ViewRecipeModal.js
├── containers
│   └── AllRecipesContainer
│       ├── AllRecipesContainer.js
│       └── index.js
├── graphql
│   ├── mutations
│   │   ├── AddNewRecipe.js
│   │   └── UpdateRecipe.js
│   └── queries
│       ├── GetAllPublishedRecipes.js
│       └── GetSingleRecipe.js
├── index.js
├── serviceWorker.js
└── styles
    └── index.css

第 5 步 — 编写代码

索引.js

在这里,我们将进行 apollo 配置。 这将是我们应用程序的主要入口文件:

import React from 'react';
import ReactDOM from 'react-dom';
import ApolloClient from 'apollo-boost';
import { ApolloProvider } from 'react-apollo';

import App from './components/App';

// Pass your prisma endpoint to uri
const client = new ApolloClient({
  uri: 'https://eu1.prisma.sh/XXXXXX'
});

ReactDOM.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>,
  document.getElementById('root')
);

GetAllPublishedRecipes.js

查询以获取所有食谱:

import { gql } from 'apollo-boost';

export default gql`query GetAllPublishedRecipes {
    recipes(where: { published: true }) {
      id
      createdAt
      title
      ingredients
      directions
      published
    }
  }`;

GetSingleRecipe.js

查询以通过配方 ID 获取配方:

import { gql } from 'apollo-boost';

export default gql`query GetSingleRecipe($recipeId: ID!) {
    recipe(where: { id: $recipeId }) {
      id
      createdAt
      title
      directions
      ingredients
      published
    }
  }`;

AddNewRecipe.js

创建新配方的突变:

import { gql } from 'apollo-boost';

export default gql`mutation AddRecipe(
    $directions: String!
    $title: String!
    $ingredients: String!
    $published: Boolean
  ) {
    createRecipe(
      data: {
        directions: $directions
        title: $title
        ingredients: $ingredients
        published: $published
      }
    ) {
      id
    }
  }`;

更新配方.js

更新配方的突变:

import { gql } from 'apollo-boost';

export default gql`mutation UpdateRecipe(
    $id: ID!
    $directions: String!
    $title: String!
    $ingredients: String!
    $published: Boolean
  ) {
    updateRecipe(
      where: { id: $id }
      data: {
        directions: $directions
        title: $title
        ingredients: $ingredients
        published: $published
      }
    ) {
      id
    }
  }`;

AllRecipesContainer.js

这是我们的 CRUD 操作逻辑的基础。 文件很大,所以我们只包含了关键部分。 您可以在 GitHub 上查看其余代码

为了使用我们的查询和突变,我们需要导入它们,然后使用 react-apollo's graphql,它允许我们创建一个可以执行查询和响应式更新的 higher-order component基于我们在应用程序中的数据。

这是我们如何获取和显示所有已发布食谱的示例:

import React, { Component } from 'react';
import { graphql } from 'react-apollo';

import { Card, Col, Row, Empty, Spin } from 'antd';

// queries
import GetAllPublishedRecipes from '../../graphql/queries/GetAllPublishedRecipes';

class AllRecipesContainer extends Component {
  render() {
    const { loading, recipes } = this.props.data;

    return (
      <div>
        {loading ? (
          <div className="spin-container">
            <Spin />
          </div>
        ) : recipes.length > 0 ? (
          <Row gutter={16}>
            {recipes.map(recipe => (
              <Col span={6} key={recipe.id}>
                <RecipeCard
                  title={recipe.title}
                  content={
                    <Fragment>
                      <Card
                        type="inner"
                        title="Ingredients"
                        style={{ marginBottom: '15px' }}
                      >
                        {`${recipe.ingredients.substring(0, 50)}.....`}
                      </Card>
                      <Card type="inner" title="Directions">
                        {`${recipe.directions.substring(0, 50)}.....`}
                      </Card>
                    </Fragment>
                  }
                  handleOnClick={this._handleOnClick}
                  handleOnEdit={this._handleOnEdit}
                  handleOnDelete={this._handleOnDelete}
                  {...recipe}
                />
              </Col>
            ))}
          </Row>
        ) : (
          <Empty />
        )}
      </div>
    );
  }
}

graphql(GetAllPublishedRecipes)(AllRecipesContainer);

生成的视图如下所示:

注意: 由于文件大小,不会包含组件的样式。 该代码可在 GitHub 存储库 中找到。


由于我们在组件中需要多个增强器,因此我们将使用 compose 来合并组件所需的所有增强器:

import React, { Component } from 'react';
import { graphql, compose, withApollo } from 'react-apollo';

// queries
import GetAllPublishedRecipes from '../../graphql/queries/GetAllPublishedRecipes';
import GetSingleRecipe from '../../graphql/queries/GetSingleRecipe';

// mutations
import UpdateRecipe from '../../graphql/mutations/UpdateRecipe';
import AddNewRecipe from '../../graphql/mutations/AddNewRecipe';

// other imports

class GetAllPublishedRecipes extends Component {
    // class logic
}

export default compose(
  graphql(UpdateRecipe, { name: 'updateRecipeMutation' }),
  graphql(AddNewRecipe, { name: 'addNewRecipeMutation' }),
  graphql(GetAllPublishedRecipes)
)(withApollo(AllRecipesContainer));

我们还需要 withApollo 增强器,它提供对 ApolloClient 实例的直接访问。 这将很有用,因为我们需要执行一次性查询来获取配方的数据。

创建配方

从以下表单捕获数据后:

然后我们执行以下 handleSubmit 回调,它运行 addNewRecipeMutation 突变:

class GetAllPublishedRecipes extends Component {
  //other logic
   _handleSubmit = event => {
    this.props
      .addNewRecipeMutation({
        variables: {
          directions,
          title,
          ingredients,
          published
        },
        refetchQueries: [
          {
            query: GetAllPublishedRecipes
          }
        ]
      })
      .then(res => {
        if (res.data.createRecipe.id) {
          this.setState(
            (prevState, nextProps) => ({
              addModalOpen: false
            }),
            () =>
              this.setState(
                (prevState, nextProps) => ({
                  notification: {
                    notificationOpen: true,
                    type: 'success',
                    message: `recipe ${title} added successfully`,
                    title: 'Success'
                  }
                }),
                () => this._handleResetState()
              )
          );
        }
      })
      .catch(e => {
        this.setState((prevState, nextProps) => ({
          notification: {
            ...prevState.notification,
            notificationOpen: true,
            type: 'error',
            message: e.message,
            title: 'Error Occured'
          }
        }));
      });
  };
};

编辑食谱

为了编辑配方,我们重新使用用于创建新配方的表单,然后传递配方数据。 当用户单击编辑图标时,会弹出表单,其中预先填写了如下数据:

然后我们运行一个不同的 handleSubmit 处理程序来运行更新突变,如下所示:

class GetAllPublishedRecipes extends Component {
  // other logic
  _updateRecipe = ({
    id,
    directions,
    ingredients,
    title,
    published,
    action
  }) => {
    this.props
      .updateRecipeMutation({
        variables: {
          id,
          directions,
          title,
          ingredients,
          published: false
        },
        refetchQueries: [
          {
            query: GetAllPublishedRecipes
          }
        ]
      })
      .then(res => {
        if (res.data.updateRecipe.id) {
          this.setState(
            (prevState, nextProps) => ({
              isEditing: false
            }),
            () =>
              this.setState(
                (prevState, nextProps) => ({
                  notification: {
                    notificationOpen: true,
                    type: 'success',
                    message: `recipe ${title} ${action} successfully`,
                    title: 'Success'
                  }
                }),
                () => this._handleResetState()
              )
          );
        }
      })
      .catch(e => {
        this.setState((prevState, nextProps) => ({
          notification: {
            ...prevState.notification,
            notificationOpen: true,
            type: 'error',
            message: e.message,
            title: 'Error Occured'
          }
        }));
      });
  };
}

删除食谱

至于删除功能,我们将对已删除的配方执行 soft-delete,这意味着我们会将 published 属性更改为 false,因为在获取文章时,我们会过滤以确保我们只获取 published 文章。

我们将使用与之前相同的函数,并将published作为false传入,如下例所示:

class GetAllPublishedRecipes extends Component {
   // other logic 
   _handleOnDelete = ({ id, directions, ingredients, title }) => {
    // user confirmed delete prompt 
    this._updateRecipe({
      id,
      directions,
      ingredients,
      title,
      published: false, // soft delete the recipe
      action: 'deleted'
    });
  };
};

结论:

在本教程中,您使用 React 和 GraphQL 构建了一个食谱应用程序,并使用 Prisma 来管理您的 GraphQL 服务器。 Prisma 是一项可靠的服务,可让您专注于实现业务逻辑。

您可以在 GitHub 访问代码。