react中使用构建缓存
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帐户 。
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:
Create a directory to hold the React client and GraphQL API:
创建一个目录来保存React客户端和GraphQL API:
Create a new project with MySQL using the following command:
使用以下命令使用MySQL创建一个新项目:
Edit graphql-api/ormconfig.json
to customize the username, password, and database.
编辑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
。 许多其他日志记录选项也可用。
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
数据库。
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。
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 is a Node framework that integrates TypeORM and GraphQL. To install it, use npm:
Vesper是一个集成TypeORM和GraphQL的Node框架。 要安装它,请使用npm:
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
:
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
:
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
:
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
更改为以下代码,该代码允许将点与用户关联。
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
类。
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查询和变异的数据转换。
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()
来配置所有内容。
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}
}
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.
您可能会注意到, pointsSave
和points
查询返回的日期采用JavaScript客户端可能难以理解的格式。 您可以通过安装graphql-iso-date来解决此问题。
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
,但是了解其他选项会有所帮助。
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添加身份验证。 添加身份验证将使您能够获取用户的信息并将用户与他们的积分相关联。
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:
使用以下命令安装最新版本:
Navigate to the directory where you created your GraphQL API and create a React client:
导航到创建GraphQL API的目录并创建一个React客户端:
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集成所需的依赖项 。
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:
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
ApolloProvider
和Query
组件。
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功能。
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应用程序中。 要安装,请运行以下命令:
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替换其代码。
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
的代码引用了两个尚不存在的组件: Home
, Login
和Points
。 使用以下代码创建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,因此也要包括在内。
Add Bootstrap’s CSS file as an import in src/index.js
.
将BootstrapCSS文件作为导入添加到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安装登录小部件:
Create src/Login.js
and add the following code to it.
创建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
:
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进行身份验证需要一些新的依赖项。 现在安装这些:
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
并添加以下代码。
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
添加规则来添加一些填充。
.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仍然是开放的。 让我们修复它。
Navigate to your graphql-api
project in a terminal window and install Okta’s JWT Verifier:
在终端窗口中导航到您的graphql-api
项目,然后安装Okta的JWT验证程序:
Create graphql-api/src/CurrentUser.ts
to hold the current user’s information.
创建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
导入OktaJwtVerifier
和CurrentUser
并配置JWT验证程序以使用OIDC应用程序的设置。
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()
以在保存时设置用户。
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,现在它已完成。
You can find the source code for this article on GitHub.
This article showed you how to build a secure React app with GraphQL, TypeORM, and Node/Vesper.
本文向您展示了如何使用GraphQL,TypeORM和Node / Vesper构建安全的React应用。
react中使用构建缓存