用 Express、React 和 GraphQL 构建一个简单的 Web 应用程序

GraphQL和React在最近几年都变得非常流行,可以肯定地说它们像鳄梨和烤面包一样融合在一起。 GraphQL服务器可以用Node编写,并允许您使用JavaScript类和函数轻松创建灵活的API。 当前端开发人员查询服务器时,仅需处理要求提供的信息。 这意味着您可以通过仅请求正在查看的页面所需的信息来使后端具有强大的功能,同时保持前端的光线明亮。

GraphQL是用于定义类型和查询数据的相对较新的标准,并且它在服务器端和客户端都有许多不同的实现。 今天,我将向您展示如何使用Express创建GraphQL服务器,以及如何在React中创建使用Apollo客户端查询服务器的单页应用程序。

创建React应用

使用React应用程序最快的方法是使用Create React App 。 如果尚未安装Node、Yarn和Create React App,则可以运行以下命令:

curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash
npm install --global yarn create-react-app

接下来,创建并启动一个新应用:

create-react-app graphql-express-react
cd graphql-express-react
yarn start

运行create-react-app ,您将获得一个新文件夹,其中包含开始所需的所有内容,并且所需的所有依赖项都将使用yarn在本地安装。 当您从文件夹中键入yarn start ,您将启动前端开发服务器,该服务器将在您编辑任何文件时自动更新。

 

创建GraphQL服务器

在我们继续编写前端之前,您需要连接服务器。 运行以下命令以安装启动和运行所需的依赖项:

yarn add [email protected] [email protected] [email protected] [email protected] [email protected]

在项目的src文件夹中创建一个名为server的新目录:

mkdir src/server

在其中,使用以下代码创建一个名为index.js的新文件:

const express = require('express');
const cors = require('cors');
const graphqlHTTP = require('express-graphql');
const gql = require('graphql-tag');
const { buildASTSchema } = require('graphql');

const POSTS = [
  { author: "John Doe", body: "Hello world" },
  { author: "Jane Doe", body: "Hi, planet!" },
];

const schema = buildASTSchema(gql`
  type Query {
    posts: [Post]
    post(id: ID!): Post
  }

  type Post {
    id: ID
    author: String
    body: String
  }
`);

const mapPost = (post, id) => post && ({ id, ...post });

const root = {
  posts: () => POSTS.map(mapPost),
  post: ({ id }) => mapPost(POSTS[id], id),
};

const app = express();
app.use(cors());
app.use('/graphql', graphqlHTTP({
  schema,
  rootValue: root,
  graphiql: true,
}));

const port = process.env.PORT || 4000
app.listen(port);
console.log(`Running a GraphQL API server at localhost:${port}/graphql`);

让我解释一下此代码的不同部分。

在文件顶部,使用require标记导入依赖项。 本机节点尚不支持import标记,但是您可以使用require代替。 Node的未来版本可能会支持import 。 Create React App在运行之前使用babel来翻译代码,这可以让您在React代码中使用import语法,因此当您到达前端代码时,您将看到它。

现在,这只是使用一些模拟数据,这就是const POSTS包含的内容。 每个项目都包含一个author和一个body

使用gql标签,您最喜欢的代码编辑器可以意识到您正在编写GraphQL代码,以便可以对其进行适当的样式化。 它还解析该字符串并将其转换为GraphQL AST 抽象语法树 。 然后,您需要使用buildASTSchema构建模式。

 

GraphQL模式可能是此代码中最有趣的部分。 这就是定义不同类型的内容,并允许您说出客户端可以查询的内容。 这还将自动生成一些非常有用的文档,以便您可以专注于编码。

type Query {
  posts: [Post]
  post(id: ID!): Post
}

type Post {
  id: ID
  author: String
  body: String
}

在这里,您定义了一个Post类型,其中包含一个idauthor和一个body 。 您需要说出每个元素的类型。 在这里, authorbody都使用原始的String类型,而id是一个ID

Query类型是一种特殊类型,可让您查询数据。 在这里,您说的是posts将为您提供一系列Post ,但是如果您想要一个Post ,则可以通过调用post并传递ID来查询它。

const mapPost = (post, id) => post && ({ id, ...post });

const root = {
  posts: () => POSTS.map(mapPost),
  post: ({ id }) => mapPost(POSTS[id], id),
};

您需要提供一组解析器来告诉GraphQL如何处理查询。 当某人查询posts时 ,它将运行此功能,并使用其索引作为ID提供所有POSTS的数组。

当查询post ,它需要一个id并将以给定索引返回该帖子。

const app = express();
app.use(cors());
app.use('/graphql', graphqlHTTP({
  schema,
  rootValue: root,
  graphiql: true,
}));

const port = process.env.PORT || 4000
app.listen(port);
console.log(`Running a GraphQL API server at localhost:${port}/graphql`);

现在,您可以创建服务器了。 graphqlHTTP函数创建一个运行GraphQL的Express服务器,该服务器期望解析器为rootValue和架构。 graphiql标志是可选的,它将为您运行服务器,使您可以更轻松地可视化数据并查看自动生成的文档。 当您运行app.listen时,您正在启动GraphQL服务器。

为了确保我们可以轻松地同时运行服务器和客户端,请添加以下dev依赖项:

yarn add -D [email protected] [email protected]

接下来,编辑您的package.json文件,以使scripts部分如下所示:

{
  "start": "npm-run-all --parallel watch:server start:web",
  "start:web": "react-scripts start",
  "start:server": "node src/server",
  "watch:server": "nodemon --watch src/server src/server",
  "build": "react-scripts build",
  "test": "react-scripts test --env=jsdom",
  "eject": "react-scripts eject"
},

关闭现有的Web服务器,然后简单地再次键入yarn start以同时运行服务器和客户端。 每当您对服务器进行更改时,仅服务器将重新启动。 每当您对前端代码进行更改时,页面应自动刷新为最新更改。

将您的浏览器指向http://localhost:4000/graphql来获取GraphiQL服务器。 您可以随时在这里更改服务器中的一些代码后回到这里并刷新,以查看最新的Schema并测试您的查询。

 

将React连接到GraphQL

接下来,您需要将前端连接到GraphQL。 我将使用Bootstrap进行一些体面的样式设计,而只需花费很少的精力。 Apollo是一个出色的React客户端,可以链接到任何GraphQL服务器。 要安装前端所需的依赖项,请运行以下命令:

yarn add [email protected] [email protected] [email protected] [email protected]

您需要配置Apollo客户端,以了解在何处连接到后端。 使用以下代码创建一个新文件src/apollo.js

import ApolloClient from 'apollo-boost';

export default new ApolloClient({
  uri: "http://localhost:4000/graphql",
});

为了使Apollo的Query React组件能够使用客户端进行连接,整个应用程序都需要包装在ApolloProvider组件中。 您还将希望包括Bootstrap的样式,并且现在可以摆脱Create React App随附的index.css文件。 对src/index.js文件进行以下更改:

@@ -1,8 +1,17 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
-import './index.css';
+import { ApolloProvider } from 'react-apollo';
+
+import 'bootstrap/dist/css/bootstrap.min.css';
 import App from './App';
 import registerServiceWorker from './registerServiceWorker';
+import client from './apollo';

-ReactDOM.render(, document.getElementById('root'));
+ReactDOM.render(
+  
+    
+  ,
+  document.getElementById('root')
+);
 serviceWorker.unregister();
+if (module.hot) module.hot.accept();

module.hot.accept()并不是真正必需的,但它使应用程序中更改的组件在您更新它们时将刷新,而不是刷新整个页面。有时您可能需要刷新一次以重置应用程序的状态,但是通常,这会导致更快的周转时间。

创建一个新文件src/PostViewer.js ,它将获取数据并将其呈现在表中:

import React from 'react';
import gql from 'graphql-tag';
import { Query } from 'react-apollo';
import { Table } from 'reactstrap';

export const GET_POSTS = gql`
  query GetPosts {
    posts {
      id
      author
      body
    }
  }
`;

export default () => (
  
    {({ loading, data }) => !loading && (
      
          {data.posts.map(post => (
            
          ))}
        
Author Body
{post.author} {post.body}
)}
);

Query组件需要GraphQL查询。 在这种情况下,您将获得所有带有ID以及authorbody的帖子。 Query组件还需要一个render函数作为其唯一的子级。 它提供了loading状态,但是在我们的例子中, loading时我们什么也不显示,因为它可以非常快地在本地获取数据。 加载完成后, data变量将是一个包含您请求的数据的对象。

上面的代码呈现了一个包含所有帖子的表格( Table是一个组件,其中包括您需要使其看上去漂亮的所有Bootstrap类)。

现在,您应该更改src/App.js文件,使其包含刚刚创建的PostViewer组件。 它看起来应该像这样:

import React, { Component } from 'react';

import PostViewer from './PostViewer';

class App extends Component {
  render() {
    return (
      
); } } export default App;

添加在GraphQL中编辑帖子的功能

在GraphQL中,查询通常是只读的。 如果要修改数据,则应改用所谓的突变

src/server/index.js const schema中创建一个新的Mutation类型,以提交帖子。 您可以创建input类型以简化输入变量。 新的变异应在成功Post返回新的Post

type Mutation {
  submitPost(input: PostInput!): Post
}

input PostInput {
  id: ID
  author: String!
  body: String!
}

您还需要更新根变量来为submitPost创建一个新的解析器。添加以下解决方案:

如果提供id ,它将尝试在该索引处查找帖子,并将数据替换为提供的authorbody 。 否则,它将添加一个新帖子。 然后,它将返回您提供的帖子以及新的id 。 当您向GraphQL发送变异请求时,您可以定义要退回的部分。

对于前端,您需要创建一个新组件来编辑帖子。 React中的表单可以通过名为Final Form的库来简化 。 yarn安装:

yarn add [email protected] [email protected]

现在,创建一个新文件src/PostEditor.js并用以下内容填充(我将在下面对其进行详细说明):

import React from 'react';
import gql from 'graphql-tag';
import {
  Button,
  Form,
  FormGroup,
  Label,
  Modal,
  ModalHeader,
  ModalBody,
  ModalFooter,
} from 'reactstrap';
import { Form as FinalForm, Field } from 'react-final-form';

import client from './apollo';
import { GET_POSTS } from './PostViewer';

const SUBMIT_POST = gql`
  mutation SubmitPost($input: PostInput!) {
    submitPost(input: $input) {
      id
    }
  }
`;

const PostEditor = ({ post, onClose }) => (
   {
      const input = { id, author, body };

      await client.mutate({
        variables: { input },
        mutation: SUBMIT_POST,
        refetchQueries: () => [{ query: GET_POSTS }],
      });

      onClose();
    }}
    initialValues={post}
    render={({ handleSubmit, pristine, invalid }) => (
      
        
{post.id ? 'Edit Post' : 'New Post'}
)} /> ); export default PostEditor;

submitPost突变是连接到后端的新突变。 它可以使用服务器中定义的PostInput类型:

const SUBMIT_POST = gql`
  mutation SubmitPost($input: PostInput!) {
    submitPost(input: $input) {
      id
    }
  }
`;

最终表单采用onSubmit函数,该函数将传递用户输入的数据。 提交帖子后,您将需要关闭模式,因此PostEditor在完成提交PostEditor使用onClose道具进行调用。

最终表单还采用了initialValues对象来定义表单最初应具有的值。 在这种情况下, PostEditor组件将采用一个post道具,该道具中包含您所需的变量,以便作为初始值传递。

另一个必需的道具是render函数,它将渲染表单。 最终表单为您提供了一些有用的表单道具,因此您可以知道表单是否有效,或者是否已从initialValues对其进行了修改。

const PostEditor = ({ post, onClose }) => (
  
);

export default PostEditor;

onSubmit函数中,您将调用提交帖子所需的变体。 Apollo使您可以重新获取查询。 由于您知道一旦提交修改,您的帖子列表就会过时,因此您可以在此处重新获取GET_POSTS查询。

onSubmit={async ({ id, author, body }) => {
  const input = { id, author, body };

  await client.mutate({
    variables: { input },
    mutation: SUBMIT_POST,
    refetchQueries: () => [{ query: GET_POSTS }],
  });

  onClose();
}}

render功能将显示Bootstrap模式。 仅当您希望打开此PostEditor组件时才将其呈现,因此isOpen仅设置为true 。 在这里,当用户在模态之外单击,单击Esc或单击“取消”按钮时,您还可以使用onClose道具关闭模态。

表单需要将handleSubmit函数作为onSubmit道具传递给它。 这告诉表单通过最终表单,而不是向页面发送POST请求。

最终形式还处理具有受控input所需的所有样板。 不必在用户键入任何内容时将数据存储在状态中,而只需使用Field组件即可。

render={({ handleSubmit, pristine, invalid }) => (
  
    
{post.id ? 'Edit Post' : 'New Post'}
)}

接下来,您必须对PostViewer进行一些小的更改。 这会为每行添加一个挂钩,以便您可以确定该行是否应可编辑,如果可以,则稍微更改样式,然后单击该行。 单击该行将调用另一个回调,您可以使用该回调设置正在编辑的帖子。

diff --git a/src/PostViewer.js b/src/PostViewer.js
index 5c53b5a..84177e0 100644
--- a/src/PostViewer.js
+++ b/src/PostViewer.js
@@ -13,7 +13,11 @@ export const GET_POSTS = gql`
   }
 `;

-export default () => (
+const rowStyles = (post, canEdit) => canEdit(post)
+  ? { cursor: 'pointer', fontWeight: 'bold' }
+  : {};
+
+const PostViewer = ({ canEdit, onEdit }) => (
   
     {({ loading, data }) => !loading && (
       
@@ -25,7 +29,11 @@ export default () => (
         
           {data.posts.map(post => (
-            
+             canEdit(post) && onEdit(post)}
+            >
               
@@ -35,3 +43,10 @@ export default () => (
     )}
   
 );
+
+PostViewer.defaultProps = {
+  canEdit: () => false,
+  onEdit: () => null,
+};
+
+export default PostViewer;

现在,在src/App.js中将它们捆绑在一起。 您可以创建一个“新帖子”按钮来创建一个新帖子,并进行创建,以便您也可以编辑任何其他现有帖子:

import React, { Component } from 'react';
import { Button, Container } from 'reactstrap';

import PostViewer from './PostViewer';
import PostEditor from './PostEditor';

class App extends Component {
  state = {
    editing: null,
  };

  render() {
    const { editing } = this.state;

    return (
      
        
         true}
          onEdit={(post) => this.setState({ editing: post })}
        />
        {editing && (
           this.setState({ editing: null })}
          />
        )}
      
    );
  }
}

export default App;

将用户身份验证添加到React + GraphQL Web App

Okta是向项目添加身份验证的一种简单方法。 Okta是一项云服务,允许开发人员创建、编辑和安全地存储用户帐户和用户帐户数据,并将它们与一个或多个应用程序连接。 如果您还没有一个,请注册一个永久免费的开发者帐户 。 登录到开发人员控制台,导航至“ 应用程序” ,然后单击“ 添加应用程序” 。 选择单页应用程序 ,然后单击下一步

 

由于默认情况下,Create React App运行在端口3000上,因此您应该将其添加为基本URI和登录重定向URI。

单击“完成”以保存您的应用程序,然后复制您的“ 客户端ID”并将其作为变量粘贴到项目根目录中名为.env.local的文件中。 这将允许您访问代码中的文件,而无需在源代码管理中存储凭据。 您还需要添加组织URL(不带-admin后缀)。 环境变量( NODE_ENV )需要以REACT_APP_ ,以便Create React App读取它们,因此文件应最终看起来像这样:

.env.local

REACT_APP_OKTA_CLIENT_ID={yourClientId}
REACT_APP_OKTA_ORG_URL=https://{yourOktaDomain}

稍后您还将需要服务器的API令牌,因此在服务器中时,导航至API- > 令牌 ,然后单击创建令牌 。 您可以有很多令牌,因此只需给它一个名称即可提醒您它的用途,例如“ GraphQL Express”。 系统会为您提供一个令牌,您现在只能看到它。 如果丢失令牌,则必须创建另一个令牌。 也将此添加到.env

REACT_APP_OKTA_TOKEN={yourOktaAPIToken}

将Okta身份验证添加到React应用程序的最简单方法是使用Okta的React SDK 。 您还需要添加路由,这可以使用React Router完成。

yarn add @okta/[email protected] [email protected]

为了知道用户是否已通过身份验证,Okta要求将应用程序包装在具有某些配置的Security组件中。 它还取决于React Router,因此您将获得一个BrowserRouter组件,包装一个Security组件,包装一个ApolloProvider组件,最后将您的App包装在Route 。 您的src/index.js文件应该最终看起来像这样:

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route } from 'react-router-dom';
import { Security, ImplicitCallback } from '@okta/okta-react';
import { ApolloProvider } from 'react-apollo';

import 'bootstrap/dist/css/bootstrap.min.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import client from './apollo';

ReactDOM.render(
  
    
      
        
        
      
    
  ,
  document.getElementById('root')
);
registerServiceWorker();
if (module.hot) module.hot.accept();

Okta SDK带有withAuth高阶组件(HoC),可用于各种与auth相关的事情,但是对于本示例,您只需要知道您是否已通过身份验证,以及一些信息关于用户。 为了使操作更简单,我编写了一个简单的HoC来覆盖Okta SDK附带的HoC。 创建一个包含以下内容的新文件src/withAuth.js

import React from 'react';
import { withAuth } from '@okta/okta-react';

export default Component => withAuth(class WithAuth extends React.Component {
  state = {
    ...this.props.auth,
    authenticated: null,
    user: null,
    loading: true,
  };

  componentDidMount() {
    this.updateAuth();
  }

  componentDidUpdate() {
    this.updateAuth();
  }

  async updateAuth() {
    const authenticated = await this.props.auth.isAuthenticated();
    if (authenticated !== this.state.authenticated) {
      const user = await this.props.auth.getUser();
      this.setState({ authenticated, user, loading: false });
    }
  }

  render() {
    const { auth, ...props } = this.props;
    return ;
  }
});

通过使用此新功能包装组件,无论用户何时登录或注销,您的应用程序都将自动重新呈现,您将能够访问有关该用户的信息。

现在,您可以使用withAuth HoC来包装App组件。 在应用首次加载的短时间内,Okta不太确定用户是否已登录。 为简单起见,在此加载期间不要在您的App组件中呈现任何内容。 但是,您可以选择呈现帖子,而只是禁用编辑,直到您了解有关用户的更多信息为止。

src/App.js呈现功能的最顶部,添加以下内容:

const { auth } = this.props;
if (auth.loading) return null;

const { user, login, logout } = auth;

现在,您可以将以下代码替换为“ New Post”按钮,如果您未登录,则将显示“ Sign In”按钮。如果您已登录,则将同时看到“ New Post”按钮之前的操作以及“退出”按钮。 这样可以使您必须登录才能创建新帖子。

{user ? (
  
) : ( )}

为确保除非登录,否则也无法编辑帖子,请更改canEdit以检查您是否有用户。

canEdit={() => Boolean(user)}

您还需要导出withAuth(App)而不是App 。 您的src/App.js文件现在应如下所示:

import React, { Component } from 'react';
import { Button, Container } from 'reactstrap';

import PostViewer from './PostViewer';
import PostEditor from './PostEditor';
import withAuth from './withAuth';

class App extends Component {
  state = {
    editing: null,
  };

  render() {
    const { auth } = this.props;
    if (auth.loading) return null;

    const { user, login, logout } = auth;
    const { editing } = this.state;

    return (
      
        {user ? (
          
) : ( )} Boolean(user)} onEdit={(post) => this.setState({ editing: post })} /> {editing && ( this.setState({ editing: null })} /> )}
); } } export default withAuth(App);

向服务器添加用户身份验证

现在,Web应用程序要求您登录才能创建帖子,但是精明的用户仍然可以通过直接向服务器发送请求来修改数据。 为防止这种情况,请向服务器添加一些身份验证。 您需要添加Okta的Node SDK和JWT Verifier作为依赖项。 您还需要使用dotenv以便从.env.local读取变量。

yarn add @okta/[email protected] @okta/[email protected] [email protected]

src/server/index.js文件的顶部,您需要告诉dotenv读取环境变量:

require('dotenv').config({ path: '.env.local' });

您将需要前端发送JSON Web令牌(JWT),以便用户可以标识自己。 当服务器上有JWT时,您需要使用Okta的JWT Verifier进行验证。 要获取有关用户的更多信息,您还需要使用Okta的Node SDK。 您可以在所有其他require语句之后,将它们设置在服务器顶部附近。

const { Client } = require('@okta/okta-sdk-nodejs');
const OktaJwtVerifier = require('@okta/jwt-verifier');

const oktaJwtVerifier = new OktaJwtVerifier({
  clientId: process.env.REACT_APP_OKTA_CLIENT_ID,
  issuer: `${process.env.REACT_APP_OKTA_ORG_URL}/oauth2/default`,
});

const client = new Client({
  orgUrl: process.env.REACT_APP_OKTA_ORG_URL,
  token: process.env.REACT_APP_OKTA_TOKEN,
});

现在您将要使用真实用户,仅发送带有用户名的字符串就没有多大意义了,特别是因为这可能会随着时间而改变。 如果帖子与用户相关联会更好。 要进行此设置,请为您的用户创建一个新的AUTHORS变量,并将POSTS变量更改为仅具有authorId而不是author字符串:

const AUTHORS = {
  1: { id: 1, name: "John Doe" },
  2: { id: 2, name: "Jane Doe" },
};

const POSTS = [
  { authorId: 1, body: "Hello world" },
  { authorId: 2, body: "Hi, planet!" },
];

在您的模式中,您将不再需要author: String PostInput author: String输入,并且Post author现在应该是Author类型,而不是String类型。 您还需要使这种新的Author类型:

type Author {
  id: ID
  name: String
}

查找用户时,现在您要从AUTHORS变量中提取AUTHORS

const mapPost = (post, id) => post && ({
  ...post,
  id,
  author: AUTHORS[post.authorId],
});

现在,您需要创建一个getUserId函数,该函数可以验证访问令牌并获取有关用户的一些信息。 该令牌将作为Authorization标头发送,外观类似于Bearer eyJraWQ...7h-zfqg 。 以下函数会将作者的名称添加到AUTHORS对象(如果尚不存在)。

const getUserId = async ({ authorization }) => {
  try {
    const accessToken = authorization.trim().split(' ')[1];
    const { claims: { uid } } = await oktaJwtVerifier.verifyAccessToken(accessToken);

    if (!AUTHORS[uid]) {
      const { profile: { firstName, lastName } } = await client.getUser(uid);

      AUTHORS[uid] = {
        id: uid,
        name: [firstName, lastName].filter(Boolean).join(' '),
      };
    }

    return uid;
  } catch (error) {
    return null;
  }
};

现在,您可以更改submitPost函数以在用户发布时获取其ID。 如果用户未登录,则可以返回null 。 这将阻止帖子的创建。 如果用户正在尝试编辑他们未创建的帖子,则还可以返回null

-  submitPost: ({ input: { id, author, body } }) => {
-    const post = { author, body };
+  submitPost: async ({ input: { id, body } }, { headers }) => {
+    const authorId = await getUserId(headers);
+    if (!authorId) return null;
+
+    const post = { authorId, body };
     let index = POSTS.length;

     if (id != null && id >= 0 && id < POSTS.length) {
+      if (POSTS[id].authorId !== authorId) return null;
+
       POSTS.splice(id, 1, post);
       index = id;
     } else {

现在,您的最终src/server/index.js文件应如下所示:

require('dotenv').config({ path: '.env.local' });

const express = require('express');
const cors = require('cors');
const graphqlHTTP = require('express-graphql');
const gql = require('graphql-tag');
const { buildASTSchema } = require('graphql');
const { Client } = require('@okta/okta-sdk-nodejs');
const OktaJwtVerifier = require('@okta/jwt-verifier');

const oktaJwtVerifier = new OktaJwtVerifier({
  clientId: process.env.REACT_APP_OKTA_CLIENT_ID,
  issuer: `${process.env.REACT_APP_OKTA_ORG_URL}/oauth2/default`,
});

const client = new Client({
  orgUrl: process.env.REACT_APP_OKTA_ORG_URL,
  token: process.env.REACT_APP_OKTA_TOKEN,
});

const AUTHORS = {
  1: { id: 1, name: "John Doe" },
  2: { id: 2, name: "Jane Doe" },
};

const POSTS = [
  { authorId: 1, body: "Hello world" },
  { authorId: 2, body: "Hi, planet!" },
];

const schema = buildASTSchema(gql`
  type Query {
    posts: [Post]
    post(id: ID): Post
  }

  type Mutation {
    submitPost(input: PostInput!): Post
  }

  input PostInput {
    id: ID
    body: String
  }

  type Post {
    id: ID
    author: Author
    body: String
  }

  type Author {
    id: ID
    name: String
  }
`);

const mapPost = (post, id) => post && ({
  ...post,
  id,
  author: AUTHORS[post.authorId],
});

const getUserId = async ({ authorization }) => {
  try {
    const accessToken = authorization.trim().split(' ')[1];
    const { claims: { uid } } = await oktaJwtVerifier.verifyAccessToken(accessToken);

    if (!AUTHORS[uid]) {
      const { profile: { firstName, lastName } } = await client.getUser(uid);

      AUTHORS[uid] = {
        id: uid,
        name: [firstName, lastName].filter(Boolean).join(' '),
      };
    }

    return uid;
  } catch (error) {
    return null;
  }
};

const root = {
  posts: () => POSTS.map(mapPost),
  post: ({ id }) => mapPost(POSTS[id], id),
  submitPost: async ({ input: { id, body } }, { headers }) => {
    const authorId = await getUserId(headers);
    if (!authorId) return null;

    const post = { authorId, body };
    let index = POSTS.length;

    if (id != null && id >= 0 && id < POSTS.length) {
      if (POSTS[id].authorId !== authorId) return null;

      POSTS.splice(id, 1, post);
      index = id;
    } else {
      POSTS.push(post);
    }

    return mapPost(post, index);
  },
};

const app = express();
app.use(cors());
app.use('/graphql', graphqlHTTP({
  schema,
  rootValue: root,
  graphiql: true,
}));

const port = process.env.PORT || 4000
app.listen(port);
console.log(`Running a GraphQL API server at localhost:${port}/graphql`);

现在,您需要进行一些前端更改,以确保您请求的是author对象,而不是假定它是字符串,并且需要将auth令牌作为标头传递。

PostViewer组件将需要进行较小的更新

diff --git a/src/PostViewer.js b/src/PostViewer.js
index 84177e0..6bfddb9 100644
--- a/src/PostViewer.js
+++ b/src/PostViewer.js
@@ -7,7 +7,10 @@ export const GET_POSTS = gql`
   query GetPosts {
     posts {
       id
-      author
+      author {
+        id
+        name
+      }
       body
     }
   }
@@ -34,7 +37,7 @@ const PostViewer = ({ canEdit, onEdit }) => (
               style={rowStyles(post, canEdit)}
               onClick={() => canEdit(post) && onEdit(post)}
             >
-              
+ ))}

PostEditor中您只需要完全删除author因为该用户将无法编辑该author ,而该author将由auth令牌确定。

diff --git a/src/PostEditor.js b/src/PostEditor.js
index 182d1cc..6cb075c 100644
--- a/src/PostEditor.js
+++ b/src/PostEditor.js
@@ -25,8 +25,8 @@ const SUBMIT_POST = gql`

 const PostEditor = ({ post, onClose }) => (
    {
-      const input = { id, author, body };
+    onSubmit={async ({ id, body }) => {
+      const input = { id, body };

       await client.mutate({
         variables: { input },
@@ -44,15 +44,6 @@ const PostEditor = ({ post, onClose }) => (
             {post.id ? 'Edit Post' : 'New Post'}
           
           
-            
-              
-              
-            
             
               
               

您的Apollo客户端是您发送身份验证令牌的地方。 为了访问auth令牌,您需要某种形式的关闭。 根据每个请求,Apollo允许您修改标题。 将src/apollo.js更改为以下内容:

import ApolloClient from 'apollo-boost';

let auth;

export const updateAuth = (newAuth) => {
  auth = newAuth;
};

export default new ApolloClient({
  uri: "http://localhost:4000/graphql",
  request: async (operation) => {
    const token = await auth.getAccessToken();
    operation.setContext({
      headers: {
        authorization: `Bearer ${token}`,
      },
    });
  },
});

现在,只要src/withAuth.js auth发生更改,您就需要调用updateAuth组件,以确保始终保持最新状态。

diff --git a/src/withAuth.js b/src/withAuth.js
index cce1b24..6d29dcc 100644
--- a/src/withAuth.js
+++ b/src/withAuth.js
@@ -1,6 +1,8 @@
 import React from 'react';
 import { withAuth } from '@okta/okta-react';

+import { updateAuth } from './apollo';
+
 export default Component => withAuth(class WithAuth extends React.Component {
   state = {
     ...this.props.auth,
@@ -18,6 +20,8 @@ export default Component => withAuth(class WithAuth extends React.Component {
   }

   async updateAuth() {
+    updateAuth(this.props.auth);
+
     const authenticated = await this.props.auth.isAuthenticated();
     if (authenticated !== this.state.authenticated) {
       const user = await this.props.auth.getUser();

现在,如果您canEditsrc/App.js文件中更改canEdit ,则可以进行更改,以便用户只能编辑自己的帖子:

onChange={(post) => user && user.sub === post.author.id}

了解有关GraphQL、React、Express和Web Security的更多信息

您现在已经成功构建了GraphQL服务器,将其连接到React,并通过安全的用户身份验证将其锁定! 作为练习,请查看是否可以将服务器从使用简单的内存中JavaScript对象切换为使用持久性数据存储。 有关在Node中使用Sequelize的示例,请查看Randall的博客 。

如果您想查看最终的示例代码,可以在github上找到它。

原文链接: https://www.sitepoint.com/build-a-simple-web-app-with-express-react-and-graphql/

你可能感兴趣的:(用 Express、React 和 GraphQL 构建一个简单的 Web 应用程序)

{post.author} {post.body}
{post.author}{post.author.name} {post.body}