react中使用构建缓存_如何使用React,GraphQL和Okta构建健康跟踪应用

邓令
2023-12-01

react中使用构建缓存

介绍 (Introduction)

In this tutorial, you will build a health tracking app using GraphQL API with Vesper framework, TypeORM, and MySQL as a database. These are Node frameworks, and you’ll use TypeScript for the language. For the client, you’ll use React, reactstrap, and Apollo Client to talk to the API. Once you have this environment working, you will add secure user authentication with Okta. Okta is a cloud service that allows developers to create, edit, and securely store user accounts and user account data, and connect them with one or multiple applications.

在本教程中,您将使用带有Vesper框架,TypeORM和MySQL作为数据库的GraphQL API构建健康跟踪应用程序。 这些是Node框架,您将使用TypeScript作为语言。 对于客户端,您将使用React,reactstrap和Apollo Client与API通讯。 在运行此环境后,将使用Okta添加安全的用户身份验证。 Okta是一项云服务,允许开发人员创建,编辑和安全地存储用户帐户和用户帐户数据,并将它们与一个或多个应用程序连接。

Before using this tutorial, register for a free developer Okta account.

在使用本教程之前,请注册一个免费的开发人员Okta帐户

第1步–使用TypeORM,GraphQL和Vesper构建API (Step 1 — Building an API with TypeORM, GraphQL, and Vesper)

TypeORM is an ORM (object-relational mapper) framework that can run in most JavaScript platforms, including Node, a browser, Cordova, React Native, and Electron. It’s heavily influenced by Hibernate, Doctrine, and Entity Framework.

TypeORM是一个ORM(对象关系映射器)框架,可以在大多数JavaScript平台上运行,包括Node,浏览器,Cordova,React Native和Electron。 它在很大程度上受Hibernate,Doctrine和Entity Framework的影响。

Install TypeORM globally to begin creating your API:

全局安装TypeORM以开始创建您的API:

  • npm i -g typeorm@0.2.7

    npm i -g typeorm@0.2.7

Create a directory to hold the React client and GraphQL API:

创建一个目录来保存React客户端和GraphQL API:

  • mkdir health-tracker

    mkdir健康跟踪器
  • cd health-tracker

    cd健康追踪器

Create a new project with MySQL using the following command:

使用以下命令使用MySQL创建一个新项目:

  • typeorm init --name graphql-api --database mysql

    typeorm init --name graphql-api-数据库mysql

Edit graphql-api/ormconfig.json to customize the username, password, and database.

编辑graphql-api/ormconfig.json以自定义用户名,密码和数据库。

graphql-api/ormconfig.json
graphql-api / ormconfig.json
{
    ...
    "username": "health",
    "password": "pointstest",
    "database": "healthpoints",
    ...
}

Note: To see the queries being executed against MySQL, change the logging value in this file to be all. Many other logging options are available, too.

注意:要查看针对MySQL执行的查询,请将此文件中的logging值更改为all 。 许多其他日志记录选项也可用。

安装MySQL (Install MySQL)

Install MySQL if you don’t already have it installed. On Ubuntu, you can use sudo apt-get install mysql-server. On macOS, you can use Homebrew and brew install mysql. For Windows, you can use the MySQL Installer.

如果尚未安装MySQL,请安装它。 在Ubuntu上,您可以使用sudo apt-get install mysql-server 。 在macOS上,您可以使用Homebrew并brew install mysql 。 对于Windows,您可以使用MySQL Installer

Once you’ve got MySQL installed and configured with a root password, log in and create a healthpoints database.

安装并配置了root密码MySQL后,登录并创建一个healthpoints数据库。

  • mysql -u root -p

    mysql -u root -p
  • create database healthpoints;

    创建数据库健康点;
  • use healthpoints;

    使用健康点;
  • grant all privileges on *.* to 'health'@'localhost' identified by 'points';

    将*。*上的所有特权授予“ points”标识的“ health” @“ localhost”;

Navigate to your graphql-api project in a terminal window, install the project’s dependencies, then start it to ensure you can connect to MySQL.

在终端窗口中导航到graphql-api项目,安装项目的依赖项,然后启动它以确保可以连接到MySQL。

  • cd graphql-api

    cd graphql-api
  • npm i

  • npm start

    npm开始

You will see the following output:

您将看到以下输出:

Inserting a new user into the database...
Saved a new user with id: 1
Loading users from the database...
Loaded users:  [ User { id: 1, firstName: 'Timber', lastName: 'Saw', age: 25 } ]
Here you can setup and run express/koa/any other framework.

安装Vesper以集成TypeORM和GraphQL (Installing Vesper to Integrate TypeORM and GraphQL)

Vesper is a Node framework that integrates TypeORM and GraphQL. To install it, use npm:

Vesper是一个集成TypeORM和GraphQL的Node框架。 要安装它,请使用npm:

  • npm i vesper@0.1.9

    npm我vesper@0.1.9

Now it’s time to create some GraphQL models (that define what your data look like) and some controllers (that explain how to interact with your data).

现在是时候创建一些GraphQL模型(定义数据的样子)和一些控制器(解释如何与数据交互)了。

Create graphql-api/src/schema/model/Points.graphql:

创建graphql-api/src/schema/model/Points.graphql

graphql-api/src/schema/model/Points.graphql
graphql-api / src / schema / model / Points.graphql
type Points {
  id: Int
  date: Date
  exercise: Int
  diet: Int
  alcohol: Int
  notes: String
  user: User
}

Create graphql-api/src/schema/model/User.graphql:

创建graphql-api/src/schema/model/User.graphql

graphql-api/src/schema/model/User.graphql
graphql-api / src / schema / model / User.graphql
type User {
  id: String
  firstName: String
  lastName: String
  points: [Points]
}

Next, create a graphql-api/src/schema/controller/PointsController.graphql with queries and mutations:

接下来,创建带有查询和突变的graphql-api/src/schema/controller/PointsController.graphql

graphql-api/src/schema/controller/PointsController.graphql
graphql-api / src / schema / controller / PointsController.graphql
type Query {
  points: [Points]
  pointsGet(id: Int): Points
  users: [User]
}

type Mutation {
  pointsSave(id: Int, date: Date, exercise: Int, diet: Int, alcohol: Int, notes: String): Points
  pointsDelete(id: Int): Boolean
}

Now that your data has GraphQL metadata, create entities that will be managed by TypeORM. Change src/entity/User.ts to have the following code that allows points to be associated with a user.

现在,您的数据具有GraphQL元数据,创建将由TypeORM管理的实体。 将src/entity/User.ts更改为以下代码,该代码允许将点与用户关联。

src/entity/User.ts
src / entity / User.ts
import { Column, Entity, OneToMany, PrimaryColumn } from 'typeorm';
import { Points } from './Points';

@Entity()
export class User {

  @PrimaryColumn()
  id: string;

  @Column()
  firstName: string;

  @Column()
  lastName: string;

  @OneToMany(() => Points, points => points.user)
  points: Points[];
}

In the same src/entity directory, create a Points.ts class with the following code.

在相同的src/entity目录中,使用以下代码创建Points.ts类。

Points.ts
点数
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';
import { User } from './User';

@Entity()
export class Points {

  @PrimaryGeneratedColumn()
  id: number;

  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP'})
  date: Date;

  @Column()
  exercise: number;

  @Column()
  diet: number;

  @Column()
  alcohol: number;

  @Column()
  notes: string;

  @ManyToOne(() => User, user => user.points, { cascade: ["insert"] })
  user: User|null;
}

Note the cascade: ["insert"] option on the @ManyToOne annotation. This option will automatically insert a user if it’s present on the entity. Create src/controller/PointsController.ts to handle converting the data from your GraphQL queries and mutations.

请注意@ManyToOne批注上的cascade: ["insert"]选项。 如果实体上存在用户,此选项将自动插入用户。 创建src/controller/PointsController.ts来处理来自GraphQL查询和变异的数据转换。

src/controller/PointsController.ts
src / controller / PointsController.ts
import { Controller, Mutation, Query } from 'vesper';
import { EntityManager } from 'typeorm';
import { Points } from '../entity/Points';

@Controller()
export class PointsController {

  constructor(private entityManager: EntityManager) {
  }

  // serves "points: [Points]" requests
  @Query()
  points() {
    return this.entityManager.find(Points);
  }

  // serves "pointsGet(id: Int): Points" requests
  @Query()
  pointsGet({id}) {
    return this.entityManager.findOne(Points, id);
  }

  // serves "pointsSave(id: Int, date: Date, exercise: Int, diet: Int, alcohol: Int, notes: String): Points" requests
  @Mutation()
  pointsSave(args) {
    const points = this.entityManager.create(Points, args);
    return this.entityManager.save(Points, points);
  }

  // serves "pointsDelete(id: Int): Boolean" requests
  @Mutation()
  async pointsDelete({id}) {
    await this.entityManager.remove(Points, {id: id});
    return true;
  }
}

Change src/index.ts to use Vesper’s bootstrap() to configure everything.

更改src/index.ts以使用Vesper的bootstrap()来配置所有内容。

src/index.ts
src / index.ts
import { bootstrap } from 'vesper';
import { PointsController } from './controller/PointsController';
import { Points } from './entity/Points';
import { User } from './entity/User';

bootstrap({
  port: 4000,
  controllers: [
    PointsController
  ],
  entities: [
    Points,
    User
  ],
  schemas: [
    __dirname + '/schema/**/*.graphql'
  ],
  cors: true
}).then(() => {
  console.log('Your app is up and running on http://localhost:4000. ' +
    'You can use playground in development mode on http://localhost:4000/playground');
}).catch(error => {
  console.error(error.stack ? error.stack : error);
});

This code tells Vesper to register controllers, entities, and GraphQL schemas to run on port 4000 and to enable CORS (cross-origin resource sharing).

此代码告诉Vesper注册控制器,实体和GraphQL模式以在端口4000上运行并启用CORS(跨域资源共享)。

Start your API using npm start and navigate to http://localhost:4000/playground. In the left pane, enter the following mutation and press the play button. Try typing the following code so you can experience the code completion that GraphQL provides you.

使用npm start启动您的API并导航到http://localhost:4000/playground 。 在左窗格中,输入以下变体,然后按播放按钮。 尝试键入以下代码,以便您可以体验GraphQL为您提供的代码完成。

mutation {
  pointsSave(exercise:1, diet:1, alcohol:1, notes:"Hello World") {
    id
    date
    exercise
    diet
    alcohol
    notes
  }
}

Your result will look similar to this.

您的结果将与此类似。

You can click the SCHEMA tab on the right to see the available queries and mutations.

您可以单击右侧的SCHEMA选项卡以查看可用的查询和变异。

Use the following points query to verify that data is in your database:

使用以下points查询来验证数据是否在数据库中:

query {
  points {id date exercise diet notes}
}

定稿日期 (Fixing Dates)

You might notice that the date returned from pointsSave and the points query is in a format the might be difficult for a JavaScript client to understand. You can fix that by installing graphql-iso-date.

您可能会注意到, pointsSavepoints查询返回的日期采用JavaScript客户端可能难以理解的格式。 您可以通过安装graphql-iso-date来解决此问题。

  • npm i graphql-iso-date@3.5.0

    npm我graphql-iso-date@3.5.0

Then, add an import in src/index.ts and configure custom resolvers for the various date types. This example only uses Date, but it’s helpful to know the other options.

然后,在src/index.ts添加导入,并为各种日期类型配置自定义解析器。 此示例仅使用Date ,但是了解其他选项会有所帮助。

src/index.ts
src / index.ts
import { GraphQLDate, GraphQLDateTime, GraphQLTime } from 'graphql-iso-date';

bootstrap({
  ...
  // https://github.com/vesper-framework/vesper/issues/4
  customResolvers: {
    Date: GraphQLDate,
    Time: GraphQLTime,
    DateTime: GraphQLDateTime
  },
  ...
});

Now running the points query will return a more client-friendly result.

现在,运行points查询将返回更适合客户的结果。

{
  "data": {
    "points": [
      {
        "id": 1,
        "date": "2018-06-04",
        "exercise": 1,
        "diet": 1,
        "notes": "Hello World"
      }
    ]
  }
}

You’ve now written an API with GraphQL and TypeScript. In the next sections, you’ll create a React client for this API and add authentication with OIDC. Adding authentication will give you the ability to get the user’s information and associate a user with their points.

现在,您已经使用GraphQL和TypeScript编写了API。 在下一部分中,您将为此API创建一个React客户端,并使用OIDC添加身份验证。 添加身份验证将使您能够获取用户的信息并将用户与他们的积分相关联。

第2步— React入门 (Step 2 — Getting Started with React)

One of the quickest ways to get started with React is to use Create React App.

使用React最快的方法之一是使用Create React App

Install the latest release using this command:

使用以下命令安装最新版本:

  • npm i -g create-react-app@1.1.4

    npm我-g create-react-app@1.1.4

Navigate to the directory where you created your GraphQL API and create a React client:

导航到创建GraphQL API的目录并创建一个React客户端:

  • cd health-tracker

    cd健康追踪器
  • create-react-app react-client

    create-react-app react-client

Next, install the dependencies you’ll need to talk to integrate Apollo Client with React, as well as Bootstrap and reactstrap.

接下来,安装将Apollo Client与React以及Bootstrap和reactstrap集成所需的依赖

  • npm i apollo-boost@0.1.7 react-apollo@2.1.4 graphql-tag@2.9.2 graphql@0.13.2

    npm我apollo-boost@0.1.7 react-apollo@2.1.4 graphql-tag@2.9.2 graphql@0.13.2

为您的API配置Apollo客户端 (Configuring Apollo Client for your API)

Open react-client/src/App.js, import ApolloClient from apollo-boost, and add the endpoint to your GraphQL API:

打开react-client/src/App.js ,从apollo-boost导入ApolloClient ,并将端点添加到您的GraphQL API:

react-client/src/App.js
react-client / src / App.js
import ApolloClient from 'apollo-boost';

const client = new ApolloClient({
  uri: "http://localhost:4000/graphql"
});

With three lines of code, your app is ready to start fetching data. You can test it by importing the gql function from graphql-tag. This will parse your query string and turn it into a query document:

只需三行代码,您的应用就可以开始获取数据了。 您可以通过从graphql-tag导入gql函数来graphql-tag 。 这将解析您的查询字符串并将其转换为查询文档:

import gql from 'graphql-tag';

class App extends Component {

  componentDidMount() {
    client.query({
      query: gql`
        {
          points {
            id date exercise diet alcohol notes
          }
        }
      `
    })
    .then(result => console.log(result));
  }
...
}

Make sure to open your browser’s developer tools so you can see the data after making this change. You could modify the console.log() to use this.setState({points: results.data.points}), but then you’d have to initialize the default state in the constructor. But there’s a more efficient way: you can use ApolloProvider and Query components from react-apollo.

确保打开浏览器的开发人员工具,以便在进行此更改后可以查看数据。 您可以修改console.log()以使用this.setState({points: results.data.points}) ,但是随后您必须在构造函数中初始化默认状态。 但是有一种更有效的方法:您可以使用react-apollo ApolloProviderQuery组件。

The following is a modified version of react-client/src/App.js that uses these components.

以下是使用这些组件的react-client/src/App.js的修改版本。

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import ApolloClient from 'apollo-boost';
import gql from 'graphql-tag';
import { ApolloProvider, Query } from 'react-apollo';
const client = new ApolloClient({
  uri: "http://localhost:4000/graphql"
});

class App extends Component {

  render() {
    return (
      <ApolloProvider client={client}>
        <div className="App">
          <header className="App-header">
            <img src={logo} className="App-logo" alt="logo" />
            <h1 className="App-title">Welcome to React</h1>
          </header>
          <p className="App-intro">
            To get started, edit <code>src/App.js</code> and save to reload.
          </p>
          <Query query={gql`
            {
              points {id date exercise diet alcohol notes}
            }
          `}>
            {({loading, error, data}) => {
              if (loading) return <p>Loading...</p>;
              if (error) return <p>Error: {error}</p>;
              return data.points.map(p => {
                return <div key={p.id}>
                  <p>Date: {p.date}</p>
                  <p>Points: {p.exercise + p.diet + p.alcohol}</p>
                  <p>Notes: {p.notes}</p>
                </div>
              })
            }}
          </Query>
        </div>
      </ApolloProvider>
    );
  }
}

export default App;

You’ve now built a GraphQL API and a React UI that talks to it. However, there’s still more to do. In the next sections, you will add authentication to React, verify JWTs with Vesper, and add CRUD functionality to the UI. CRUD functionality already exists in the API thanks to the mutations you wrote earlier.

现在,您已经构建了一个GraphQL API和与其对话的React UI。 但是,还有更多工作要做。 在下一部分中,您将向React添加身份验证,使用Vesper验证JWT,并将CRUD功能添加到UI。 由于您先前编写了一些变异,因此API中已经存在CRUD功能。

第3步—为React with OpenID Connect添加身份验证 (Step 3 — Adding Authentication for React with OpenID Connect)

You’ll need to configure React to use Okta for authentication. You’ll need to create an OIDC app in Okta for that.

您需要将React配置为使用Okta进行身份验证。 为此,您需要在Okta中创建OIDC应用。

Log in to your Okta Developer account (or sign up if you don’t have an account) and navigate to Applications > Add Application. Click Single-Page App, click Next, and give the app a name you’ll remember. Change all instances of localhost:8080 to localhost:3000 and click Done.

登录到您的1563开发者帐户(或者注册 ,如果你没有一个帐户)并导航到应用程序 > 添加应用程序 。 单击“ 单页应用程序” ,再单击“ 下一步” ,然后为应用程序命名。 将localhost:8080所有实例更改为localhost:3000 ,然后单击完成

Your settings will be similar to the following screenshot:

您的设置将类似于以下屏幕截图:

Okta’s React SDK allows you to integrate OIDC into a React application. To install, run the following commands:

Okta的React SDK允许您将OIDC集成到React应用程序中。 要安装,请运行以下命令:

  • npm i @okta/okta-react@1.0.2 react-router-dom@4.2.2

    npm我@ okta / okta-react @ 1.0.2 react-router-dom@4.2.2

Okta’s React SDK depends on react-router, thus you needed to install react-router-dom. Configuring routing in client/src/App.tsx is a common practice, so replace its code with the following JavaScript that sets up authentication with Okta.

Okta的React SDK依赖于react-router ,因此您需要安装react-router-dom 。 在client/src/App.tsx配置路由是一种常见的做法,因此请使用以下使用Okta设置身份验证JavaScript替换其代码。

client/src/App.tsx
客户端/src/App.tsx
import React, { Component } from 'react';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import { ImplicitCallback, SecureRoute, Security } from '@okta/okta-react';
import Home from './Home';
import Login from './Login';
import Points from './Points';

function onAuthRequired({history}) {
  history.push('/login');
}

class App extends Component {
  render() {
    return (
      <Router>
        <Security issuer='https://{yourOktaDomain}.com/oauth2/default'
                  client_id='{yourClientId}'
                  redirect_uri={window.location.origin + '/implicit/callback'}
                  onAuthRequired={onAuthRequired}>
          <Route path='/' exact={true} component={Home}/>
          <SecureRoute path='/points' component={Points}/>
          <Route path='/login' render={() => <Login baseUrl='https://{yourOktaDomain}.com'/>}/>
          <Route path='/implicit/callback' component={ImplicitCallback}/>
        </Security>
      </Router>
    );
  }
}

export default App;

Make sure to replace {yourOktaDomain} and {yourClientId} in the previous code. Your Okta domain should be something like dev-12345.oktapreview. Make sure you don’t end up with two .com values in the URL.

确保替换之前代码中的{yourOktaDomain}{yourClientId} 。 您的Okta域应该类似于dev-12345.oktapreview 。 确保在URL中不要以两个.com值结尾。

The code in App.js references two components that don’t exist yet: Home, Login, and Points. Create src/Home.js with the following code. This component renders the default route, provides a Login button, and links to your points and logs out after you’ve logged in:

App.js的代码引用了两个尚不存在的组件: HomeLoginPoints 。 使用以下代码创建src/Home.js 该组件呈现默认路线,提供“登录”按钮,并链接到您的点并在您登录后注销:

src/Home.js
src / Home.js
import React, { Component } from 'react';
import { withAuth } from '@okta/okta-react';
import { Button, Container } from 'reactstrap';
import AppNavbar from './AppNavbar';
import { Link } from 'react-router-dom';

export default withAuth(class Home extends Component {
  constructor(props) {
    super(props);
    this.state = {authenticated: null, userinfo: null, isOpen: false};
    this.checkAuthentication = this.checkAuthentication.bind(this);
    this.checkAuthentication();
    this.login = this.login.bind(this);
    this.logout = this.logout.bind(this);
  }

  async checkAuthentication() {
    const authenticated = await this.props.auth.isAuthenticated();
    if (authenticated !== this.state.authenticated) {
      if (authenticated && !this.state.userinfo) {
        const userinfo = await this.props.auth.getUser();
        this.setState({authenticated, userinfo});
      } else {
        this.setState({authenticated});
      }
    }
  }

  async componentDidMount() {
    this.checkAuthentication();
  }

  async componentDidUpdate() {
    this.checkAuthentication();
  }

  async login() {
    this.props.auth.login('/');
  }

  async logout() {
    this.props.auth.logout('/');
    this.setState({authenticated: null, userinfo: null});
  }

  render() {
    if (this.state.authenticated === null) return null;
    const button = this.state.authenticated ?
        <div>
          <Button color="link"><Link to="/points">Manage Points</Link></Button><br/>
          <Button color="link" onClick={this.logout}>Logout</Button>
        </div>:
      <Button color="primary" onClick={this.login}>Login</Button>;

    const message = this.state.userinfo ?
      <p>Hello, {this.state.userinfo.given_name}!</p> :
      <p>Please log in to manage your points.</p>;

    return (
      <div>
        <AppNavbar/>
        <Container fluid>
          {message}
          {button}
        </Container>
      </div>
    );
  }
});

This component uses <Container/> and <Button/> from reactstrap. Install reactstrap so that everything compiles. It depends on Bootstrap, so include that, too.

该组件使用reactstrap中的<Container/><Button/> 。 安装reactstrap,以便所有内容都能编译。 它取决于Bootstrap,因此也要包括在内。

  • npm i reactstrap@6.1.0 bootstrap@4.1.1

    npm我reactstrap@6.1.0 bootstrap@4.1.1

Add Bootstrap’s CSS file as an import in src/index.js.

将BootstrapCSS文件作为导入添加到src/index.js

src/index.js
src / index.js
import 'bootstrap/dist/css/bootstrap.min.css';

You might notice there’s a <AppNavbar/> in the Home component’s render() method. Create src/AppNavbar.js so you can use a common header between components.

您可能会注意到Home组件的render()方法中有一个<AppNavbar/> 。 创建src/AppNavbar.js以便可以在组件之间使用通用标头。

import React, { Component } from 'react';
import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
import { Link } from 'react-router-dom';

export default class AppNavbar extends Component {
  constructor(props) {
    super(props);
    this.state = {isOpen: false};
    this.toggle = this.toggle.bind(this);
  }

  toggle() {
    this.setState({
      isOpen: !this.state.isOpen
    });
  }

  render() {
    return <Navbar color="success" dark expand="md">
      <NavbarBrand tag={Link} to="/">Home</NavbarBrand>
      <NavbarToggler onClick={this.toggle}/>
      <Collapse isOpen={this.state.isOpen} navbar>
        <Nav className="ml-auto" navbar>
          <NavItem>
            <NavLink
              href="https://twitter.com/oktadev">@oktadev</NavLink>
          </NavItem>
          <NavItem>
            <NavLink href="https://github.com/oktadeveloper/okta-react-graphql-example/">GitHub</NavLink>
          </NavItem>
        </Nav>
      </Collapse>
    </Navbar>;
  }
}

In this example, you will embed Okta’s Sign-In Widget. Another option is to redirect to Okta and use a hosted login page.

在此示例中,您将嵌入Okta的Sign-In Widget 。 另一个选项是重定向到Okta并使用托管的登录页面。

Install the Sign-In Widget using npm:

使用npm安装登录小部件:

  • npm i @okta/okta-signin-widget@2.9.0

    npm我@ okta / okta-signin-widget @ 2.9.0

Create src/Login.js and add the following code to it.

创建src/Login.js并添加以下代码。

src/Login.js
src / Login.js
import React, { Component } from 'react';
import { Redirect } from 'react-router-dom';
import OktaSignInWidget from './OktaSignInWidget';
import { withAuth } from '@okta/okta-react';

export default withAuth(class Login extends Component {
  constructor(props) {
    super(props);
    this.onSuccess = this.onSuccess.bind(this);
    this.onError = this.onError.bind(this);
    this.state = {
      authenticated: null
    };
    this.checkAuthentication();
  }

  async checkAuthentication() {
    const authenticated = await this.props.auth.isAuthenticated();
    if (authenticated !== this.state.authenticated) {
      this.setState({authenticated});
    }
  }

  componentDidUpdate() {
    this.checkAuthentication();
  }

  onSuccess(res) {
    return this.props.auth.redirect({
      sessionToken: res.session.token
    });
  }

  onError(err) {
    console.log('error logging in', err);
  }

  render() {
    if (this.state.authenticated === null) return null;
    return this.state.authenticated ?
      <Redirect to={{pathname: '/'}}/> :
      <OktaSignInWidget
        baseUrl={this.props.baseUrl}
        onSuccess={this.onSuccess}
        onError={this.onError}/>;
  }
});

The Login component has a reference to OktaSignInWidget. Create src/OktaSignInWidget.js:

Login组件具有对OktaSignInWidget的引用。 创建src/OktaSignInWidget.js

src/OktaSignInWidget.js
src / OktaSignInWidget.js
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import OktaSignIn from '@okta/okta-signin-widget';
import '@okta/okta-signin-widget/dist/css/okta-sign-in.min.css';
import '@okta/okta-signin-widget/dist/css/okta-theme.css';
import './App.css';

export default class OktaSignInWidget extends Component {
  componentDidMount() {
    const el = ReactDOM.findDOMNode(this);
    this.widget = new OktaSignIn({
      baseUrl: this.props.baseUrl
    });
    this.widget.renderEl({el}, this.props.onSuccess, this.props.onError);
  }

  componentWillUnmount() {
    this.widget.remove();
  }

  render() {
    return <div/>;
  }
};

Create src/Points.js to render the list of points from your API:

创建src/Points.js来呈现API中的点列表:

import React, { Component } from 'react';
import { ApolloClient } from 'apollo-client';
import { createHttpLink } from 'apollo-link-http';
import { setContext } from 'apollo-link-context';
import { InMemoryCache } from 'apollo-cache-inmemory';
import gql from 'graphql-tag';
import { withAuth } from '@okta/okta-react';
import AppNavbar from './AppNavbar';
import { Alert, Button, Container, Table } from 'reactstrap';
import PointsModal from './PointsModal';

export const httpLink = createHttpLink({
  uri: 'http://localhost:4000/graphql'
});

export default withAuth(class Points extends Component {
  client;

  constructor(props) {
    super(props);
    this.state = {points: [], error: null};

    this.refresh = this.refresh.bind(this);
    this.remove = this.remove.bind(this);
  }

  refresh(item) {
    let existing = this.state.points.filter(p => p.id === item.id);
    let points = [...this.state.points];
    if (existing.length === 0) {
      points.push(item);
      this.setState({points});
    } else {
      this.state.points.forEach((p, idx) => {
        if (p.id === item.id) {
          points[idx] = item;
          this.setState({points});
        }
      })
    }
  }

  remove(item, index) {
    const deletePoints = gql`mutation pointsDelete($id: Int) { pointsDelete(id: $id) }`;

    this.client.mutate({
      mutation: deletePoints,
      variables: {id: item.id}
    }).then(result => {
      if (result.data.pointsDelete) {
        let updatedPoints = [...this.state.points].filter(i => i.id !== item.id);
        this.setState({points: updatedPoints});
      }
    });
  }

  componentDidMount() {
    const authLink = setContext(async (_, {headers}) => {
      const token = await this.props.auth.getAccessToken();
      const user = await this.props.auth.getUser();

      // return the headers to the context so httpLink can read them
      return {
        headers: {
          ...headers,
          authorization: token ? `Bearer ${token}` : '',
          'x-forwarded-user': user ? JSON.stringify(user) : ''
        }
      }
    });

    this.client = new ApolloClient({
      link: authLink.concat(httpLink),
      cache: new InMemoryCache(),
      connectToDevTools: true
    });

    this.client.query({
      query: gql`
        {
          points {
              id,
              user {
                  id,
                  lastName
              }
              date,
              alcohol,
              exercise,
              diet,
              notes
          }
        }`
    }).then(result => {
      this.setState({points: result.data.points});
    }).catch(error => {
      this.setState({error: <Alert color="danger">Failure to communicate with API.</Alert>});
    });
  }

  render() {
    const {points, error} = this.state;
    const pointsList = points.map(p => {
      const total = p.exercise + p.diet + p.alcohol;
      return <tr key={p.id}>
        <td style={{whiteSpace: 'nowrap'}}><PointsModal item={p} callback={this.refresh}/></td>
        <td className={total <= 1 ? 'text-danger' : 'text-success'}>{total}</td>
        <td>{p.notes}</td>
        <td><Button size="sm" color="danger" onClick={() => this.remove(p)}>Delete</Button></td>
      </tr>
    });

    return (
      <div>
        <AppNavbar/>
        <Container fluid>
          {error}
          <h3>Your Points</h3>
          <Table>
            <thead>
            <tr>
              <th width="10%">Date</th>
              <th width="10%">Points</th>
              <th>Notes</th>
              <th width="10%">Actions</th>
            </tr>
            </thead>
            <tbody>
            {pointsList}
            </tbody>
          </Table>
          <PointsModal callback={this.refresh}/>
        </Container>
      </div>
    );
  }
})

This code starts with the refresh() and remove() methods. The important part happens in componentDidMount(), where the access token is added in an Authorization header, and the user’s information is stuffed in an x-forwarded-user header. An ApolloClient is created with this information, a cache is added, and the connectToDevTools flag is turned on. This can be useful for debugging with Apollo Client Developer Tools.

这段代码从refresh()remove()方法开始。 重要的部分发生在componentDidMount() ,其中访问令牌添加在Authorization标头中,而用户信息则填充在x-forwarded-user标头中。 使用此信息创建一个ApolloClient ,添加一个缓存,并打开connectToDevTools标志。 这对于使用Apollo Client Developer Tools进行调试很有用。

componentDidMount() {
  const authLink = setContext(async (_, {headers}) => {
    const token = await this.props.auth.getAccessToken();

    // return the headers to the context so httpLink can read them
    return {
      headers: {
        ...headers,
        authorization: token ? `Bearer ${token}` : '',
        'x-forwarded-user': user ? JSON.stringify(user) : ''
      }
    }
  });

  this.client = new ApolloClient({
    link: authLink.concat(httpLink),
    cache: new InMemoryCache(),
    connectToDevTools: true
  });

  // this.client.query(...);
}

Authentication with Apollo Client requires a few new dependencies. Install these now:

使用Apollo Client进行身份验证需要一些新的依赖项。 现在安装这些:

  • npm apollo-link-context@1.0.8 apollo-link-http@1.5.4

    npm apollo-link-context@1.0.8 apollo-link-http@1.5.4

In the JSX of the page, there is a delete button that calls the remove() method in Points. There’s also a <PointsModal/> component. This is referenced for each item, as well as at the bottom. You’ll notice both of these reference the refresh() method, which updates the list.

在页面的JSX中,有一个删除按钮,该按钮在Points中调用remove()方法。 还有一个<PointsModal/>组件。 每个项目以及其底部均引用此标记。 您会注意到这两个都引用了refresh()方法,该方法将更新列表。

<PointsModal item={p} callback={this.refresh}/>
<PointsModal callback={this.refresh}/>

This component renders a link to edit a component, or an Add button when no item is set.

如果未设置任何item则此组件将呈现一个链接以编辑该组件,或提供一个“添加”按钮。

Create src/PointsModal.js and add the following code to it.

创建src/PointsModal.js并添加以下代码。

src/PointsModal.js
src / PointsModal.js
import React, { Component } from 'react';
import { Button, Form, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import { withAuth } from '@okta/okta-react';
import { httpLink } from './Points';
import { ApolloClient } from 'apollo-client';
import { setContext } from 'apollo-link-context';
import { InMemoryCache } from 'apollo-cache-inmemory';
import gql from 'graphql-tag';
import { Link } from 'react-router-dom';

export default withAuth(class PointsModal extends Component {
  client;
  emptyItem = {
    date: (new Date()).toISOString().split('T')[0],
    exercise: 1,
    diet: 1,
    alcohol: 1,
    notes: ''
  };

  constructor(props) {
    super(props);
    this.state = {
      modal: false,
      item: this.emptyItem
    };

    this.toggle = this.toggle.bind(this);
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  componentDidMount() {
    if (this.props.item) {
      this.setState({item: this.props.item})
    }

    const authLink = setContext(async (_, {headers}) => {
      const token = await this.props.auth.getAccessToken();
      const user = await this.props.auth.getUser();

      // return the headers to the context so httpLink can read them
      return {
        headers: {
          ...headers,
          authorization: token ? `Bearer ${token}` : '',
          'x-forwarded-user': JSON.stringify(user)
        }
      }
    });

    this.client = new ApolloClient({
      link: authLink.concat(httpLink),
      cache: new InMemoryCache()
    });
  }

  toggle() {
    if (this.state.modal && !this.state.item.id) {
      this.setState({item: this.emptyItem});
    }
    this.setState({modal: !this.state.modal});
  }

  render() {
    const {item} = this.state;
    const opener = item.id ? <Link onClick={this.toggle} to="#">{this.props.item.date}</Link> :
      <Button color="primary" onClick={this.toggle}>Add Points</Button>;

    return (
      <div>
        {opener}
        <Modal isOpen={this.state.modal} toggle={this.toggle}>
          <ModalHeader toggle={this.toggle}>{(item.id ? 'Edit' : 'Add')} Points</ModalHeader>
          <ModalBody>
            <Form onSubmit={this.handleSubmit}>
              <FormGroup>
                <Label for="date">Date</Label>
                <Input type="date" name="date" id="date" value={item.date}
                       onChange={this.handleChange}/>
              </FormGroup>
              <FormGroup check>
                <Label check>
                  <Input type="checkbox" name="exercise" id="exercise" checked={item.exercise}
                         onChange={this.handleChange}/>{' '}
                  Did you exercise?
                </Label>
              </FormGroup>
              <FormGroup check>
                <Label check>
                  <Input type="checkbox" name="diet" id="diet" checked={item.diet}
                         onChange={this.handleChange}/>{' '}
                  Did you eat well?
                </Label>
              </FormGroup>
              <FormGroup check>
                <Label check>
                  <Input type="checkbox" name="alcohol" id="alcohol" checked={item.alcohol}
                         onChange={this.handleChange}/>{' '}
                  Did you drink responsibly?
                </Label>
              </FormGroup>
              <FormGroup>
                <Label for="notes">Notes</Label>
                <Input type="textarea" name="notes" id="notes" value={item.notes}
                       onChange={this.handleChange}/>
              </FormGroup>
            </Form>
          </ModalBody>
          <ModalFooter>
            <Button color="primary" onClick={this.handleSubmit}>Save</Button>{' '}
            <Button color="secondary" onClick={this.toggle}>Cancel</Button>
          </ModalFooter>
        </Modal>
      </div>
    )
  };

  handleChange(event) {
    const target = event.target;
    const value = target.type === 'checkbox' ? (target.checked ? 1 : 0) : target.value;
    const name = target.name;
    let item = {...this.state.item};
    item[name] = value;
    this.setState({item});
  }

  handleSubmit(event) {
    event.preventDefault();
    const {item} = this.state;

    const updatePoints = gql`
      mutation pointsSave($id: Int, $date: Date, $exercise: Int, $diet: Int, $alcohol: Int, $notes: String) {
        pointsSave(id: $id, date: $date, exercise: $exercise, diet: $diet, alcohol: $alcohol, notes: $notes) {
          id date
        }
      }`;

    this.client.mutate({
      mutation: updatePoints,
      variables: {
        id: item.id,
        date: item.date,
        exercise: item.exercise,
        diet: item.diet,
        alcohol: item.alcohol,
        notes: item.notes
      }
    }).then(result => {
      let newItem = {...item};
      newItem.id = result.data.pointsSave.id;
      this.props.callback(newItem);
      this.toggle();
    });
  }
});

Make sure your GraphQL backend is started, then start the React frontend with npm start. The text squishes up against the top navbar, so add some padding by adding a rule in src/index.css.

确保您的GraphQL后端已启动,然后使用npm start React前端。 文本紧靠顶部导航栏,因此通过在src/index.css添加规则来添加一些填充。

src/index.css
src / index.css
.container-fluid {
  padding-top: 10px;
}

You will see the Home component and a button to log in.

您将看到Home组件和一个登录按钮。

Click Login and you’ll be prompted to enter your Okta credentials.

单击登录 ,系统将提示您输入Okta凭据。

Once you enter the credentials, you’ll be logged in.

输入凭据后,您将登录。

Click Manage Points to see the points list.

单击管理点以查看点列表。

Your React frontend is secured, but your API is still wide open. Let’s fix that.

您的React前端是安全的,但是您的API仍然是开放的。 让我们修复它。

从JWT获取用户信息 (Get User Information from JWTs)

Navigate to your graphql-api project in a terminal window and install Okta’s JWT Verifier:

在终端窗口中导航到您的graphql-api项目,然后安装Okta的JWT验证程序:

  • npm i @okta/jwt-verifier@0.0.12

    npm我@ okta / jwt-verifier @ 0.0.12

Create graphql-api/src/CurrentUser.ts to hold the current user’s information.

创建graphql-api/src/CurrentUser.ts来保存当前用户的信息。

graphql-api/src/CurrentUser.ts
graphql-api / src / CurrentUser.ts
export class CurrentUser {
  constructor(public id: string, public firstName: string, public lastName: string) {}
}

Import OktaJwtVerifier and CurrentUser in graphql-api/src/index.ts and configure the JWT verifier to use your OIDC app’s settings.

graphql-api/src/index.ts导入OktaJwtVerifierCurrentUser并配置JWT验证程序以使用OIDC应用程序的设置。

graphql-api/src/index.ts
graphql-api / src / index.ts
import * as OktaJwtVerifier from '@okta/jwt-verifier';
import { CurrentUser } from './CurrentUser';

const oktaJwtVerifier = new OktaJwtVerifier({
  clientId: '{yourClientId},
  issuer: 'https://{yourOktaDomain}.com/oauth2/default'
});

In the bootstrap configuration, define setupContainer to require an authorization header and set the current user from the x-forwarded-user header.

在引导程序配置中,将setupContainer定义为需要authorization标头,然后从x-forwarded-user标头设置当前用户。

bootstrap({
  …
  cors: true,
  setupContainer: async (container, action) => {
    const request = action.request;
    // require every request to have an authorization header
    if (!request.headers.authorization) {
      throw Error('Authorization header is required!');
    }
    let parts = request.headers.authorization.trim().split(' ');
    let accessToken = parts.pop();
    await oktaJwtVerifier.verifyAccessToken(accessToken)
      .then(async jwt => {
        const user = JSON.parse(request.headers['x-forwarded-user'].toString());
        const currentUser = new CurrentUser(jwt.claims.uid, user.given_name, user.family_name);
        container.set(CurrentUser, currentUser);
      })
      .catch(error => {
        throw Error('JWT Validation failed!');
      })
  }
  ...
});

Modify graphql-api/src/controller/PointsController.ts to inject the CurrentUser as a dependency. While you’re in there, adjust the points() method to filter by user ID and modify pointsSave() to set the user when saving.

修改graphql-api/src/controller/PointsController.ts以注入CurrentUser作为依赖项。 当您在那里时,请调整points()方法以按用户ID进行过滤,并修改pointsSave()以在保存时设置用户。

graphql-api/src/controller/PointsController.ts
graphql-api / src / controller / PointsController.ts
import { Controller, Mutation, Query } from 'vesper';
import { EntityManager } from 'typeorm';
import { Points } from '../entity/Points';
import { User } from '../entity/User';
import { CurrentUser } from '../CurrentUser';

@Controller()
export class PointsController {

  constructor(private entityManager: EntityManager, private currentUser: CurrentUser) {
  }

  // serves "points: [Points]" requests
  @Query()
  points() {
    return this.entityManager.getRepository(Points).createQueryBuilder("points")
      .innerJoin("points.user", "user", "user.id = :id", { id: this.currentUser.id })
      .getMany();
  }

  // serves "pointsGet(id: Int): Points" requests
  @Query()
  pointsGet({id}) {
    return this.entityManager.findOne(Points, id);
  }

  // serves "pointsSave(id: Int, date: Date, exercise: Int, diet: Int, alcohol: Int, notes: String): Points" requests
  @Mutation()
  pointsSave(args) {
    // add current user to points saved
    if (this.currentUser) {
      const user = new User();
      user.id = this.currentUser.id;
      user.firstName = this.currentUser.firstName;
      user.lastName = this.currentUser.lastName;
      args.user = user;
    }

    const points = this.entityManager.create(Points, args);
    return this.entityManager.save(Points, points);
  }

  // serves "pointsDelete(id: Int): Boolean" requests
  @Mutation()
  async pointsDelete({id}) {
    await this.entityManager.remove(Points, {id: id});
    return true;
  }
}

Restart the API, and it will now be complete.

重新启动该API,现在它已完成。

源代码 (Source Code)

You can find the source code for this article on GitHub.

您可以在GitHub找到本文的源代码。

结论 (Conclusion)

This article showed you how to build a secure React app with GraphQL, TypeORM, and Node/Vesper.

本文向您展示了如何使用GraphQL,TypeORM和Node / Vesper构建安全的React应用。

翻译自: https://www.digitalocean.com/community/tutorials/how-to-build-a-health-tracking-app-with-react-graphql-and-okta-2

react中使用构建缓存

 类似资料: