erui _ eruie 002使用Express,React和GraphQL构建一个简单的Web应用程序
Ťhis article was originally published on the Okta developer blog. Thank you for supporting the partners who make SitePoint possible.
The quickest way to get started with a React app is to use Create React App. If you don’t already have Node, Yarn, and Create React App installed, you can run the following commands:
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 startWhen you run
create-react-app, you’ll get a new folder with everything you need to get started, and all the dependencies you need will be installed locally using
yarn. When you type
yarn startfrom within the folder, you’re starting the frontend development server that will automatically update whenever you edit any files.
yarn add express@4.16.3 cors@2.8.4 graphql@14.0.2 express-graphql@0.6.12 graphql-tag@2.9.2Create a new directory in your project’s
srcfolder, named
server:
mkdir src/server
In there, create a new file named
index.js, with the following code:
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`);
At the top of the file, you use the
requiretag to import your dependencies. Native Node doesn’t support the
importtag yet, but you can use
requireinstead. A future version of Node will likely support
import. Create React App uses
babelto transpile the code before running it, which allows you to use the
importsyntax in the React code, so you’ll see that when we get to the frontend code.
For now, this is just using some mock data, which is what the
const POSTScontains. Each item contains an
authorand a
body.
The
gqltag allows your favorite code editor to realize that you’re writing GraphQL code so that it can stylize it appropriately. It also parses the string and converts it to GraphQL AST Abstract Syntax Tree. You then need to build a schema using
buildASTSchema.
type Query { posts: [Post] post(id: ID!): Post } type Post { id: ID author: String body: String }Here, you’ve defined a
Posttype, which contains an
id, and
author, and a
body. You need to say what the types are for each element. Here,
authorand
bodyboth use the primitive
Stringtype, and
idis an
ID.
The
Querytype is a special type that lets you query the data. Here, you’re saying that
postswill give you an array of
Posts, but if you want a single
Postyou can query it by calling
postand passing in the ID.
const mapPost = (post, id) => post && ({ id, ...post }); const root = { posts: () => POSTS.map(mapPost), post: ({ id }) => mapPost(POSTS[id], id), };
You need to provide a set of resolvers to tell GraphQL how to handle the queries. When someone queries
posts, it will run this function, providing an array of all the
POSTS, using their index as an ID.
When you query
post, it expects an
idand will return the post at the given 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`);
Now you are able to create the server. The
graphqlHTTPfunction creates an Express server running GraphQL, which expects the resolvers as
rootValue, and the schema. The
graphiqlflag is optional and will run a server for you allowing you to more easily visualize the data and see the auto-generated documentation. When you run
app.listen, you’re starting the GraphQL server.
yarn add -D nodemon@1.18.4 npm-run-all@4.1.3Next, edit your
package.jsonfile so that the
scriptssection looks like this:
{ "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" },
Close your existing web server, then simply type
yarn startagain to run both the server and client at the same time. Whenever you make changes to the server, just the server will restart. Whenever you make changes to the frontend code, the page should automatically refresh with the latest changes.
Point your browser to
http://localhost:4000/graphqlto get the GraphiQL server. You can always come back here and refresh after changing some code around in the server to see the latest Schema and test your queries.
Next, you need to connect the frontend to GraphQL. I’ll use Bootstrap for some decent styling with minimal effort. Apollo makes a great React client that can link up to any GraphQL server. To install the dependencies you need for the frontend, run the following:
yarn add bootstrap@4.1.3 reactstrap@6.4.0 apollo-boost@0.1.16 react-apollo@2.1.11
You’ll need to configure the Apollo client to know where to connect to the backend. Create a new file
src/apollo.jswith the following code:
import ApolloClient from 'apollo-boost'; export default new ApolloClient({ uri: "http://localhost:4000/graphql", });
In order for Apollo’s
QueryReact component to be able to connect using the client, the entire app needs to be wrapped in an
ApolloProvidercomponent. You’ll also want to include the styling for Bootstrap, and you can get rid of the
index.cssfile that came with Create React App now. Make the following changes to your
src/index.jsfile:
@@ -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(<App />, document.getElementById('root')); +ReactDOM.render( + <ApolloProvider client={client}> + <App /> + </ApolloProvider>, + document.getElementById('root') +); serviceWorker.unregister(); +if (module.hot) module.hot.accept();
The
module.hot.accept()isn’t really necessary, but makes it so that just the components changing within the app will refresh as you update them, rather than refreshing the entire page. Every once in a while you may need to refresh just to reset the state of the app, but generally, this leads to a quicker turnaround time.
Create a new file
src/PostViewer.jsthat will fetch the data and render it in a table:
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 () => ( <Query query={GET_POSTS}> {({ loading, data }) => !loading && ( <Table> <thead> <tr> <th>Author</th> <th>Body</th> </tr> </thead> <tbody> {data.posts.map(post => ( <tr key={post.id}> <td>{post.author}</td> <td>{post.body}</td> </tr> ))} </tbody> </Table> )} </Query> );
The
Querycomponent requires a GraphQL query. In this case, you’re just getting all of the posts with their ID and the
authorand
body. The
Querycomponent also requires a render function as its only child. It provides a
loadingstate, but in our case, we just won’t show anything while it’s loading, since it will be really quick to fetch the data locally. Once it’s done loading, the
datavariable will be an object including the data you requested.
The above code renders a table (
Tableis a component that includes all the Bootstrap classes you need to make it look pretty) with all of the posts.
You should now change your
src/App.jsfile to include the
PostViewercomponent you just made. It should look like this:
import React, { Component } from 'react'; import PostViewer from './PostViewer'; class App extends Component { render() { return ( <main> <PostViewer /> </main> ); } } export default App;
Now if you go to
http://localhost:3000you should see this:
在GraphQL中,查询通常是只读的。 如果您要修改数据,则应使用称为突变代替。
Create a new
Mutationtype in your
const schemain
src/server/index.jsto submit a post. You can create an
inputtype to simplify your input variables. The new mutation should return the new
Poston success:
type Mutation { submitPost(input: PostInput!): Post } input PostInput { id: ID author: String! body: String! }
You’ll need to update your
rootvariable to create a new resolver for
submitPostas well. Add the following resolver:
submitPost: ({ input: { id, author, body } }) => { const post = { author, 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); },
If you provide an
id, it will try to find the post at that index and replace the data with the
authorand
bodythat was provided. Otherwise, it will add a new post. Then it returns the post you provided along with the new
idfor it. When you send a mutation request to GraphQL, you can define which pieces you want back:
For the frontend, you’ll need to create a new component for editing posts. Forms in React can be made easier by a library called Final Form. Install it with
yarn:
yarn add final-form@4.10.0 react-final-form@3.6.5
Now, make a new file
src/PostEditor.jsand fill it with the following (I’ll explain it in more detail just below):
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 }) => ( <FinalForm onSubmit={async ({ id, author, body }) => { const input = { id, author, body }; await client.mutate({ variables: { input }, mutation: SUBMIT_POST, refetchQueries: () => [{ query: GET_POSTS }], }); onClose(); }}initialValues={post} render={({ handleSubmit, pristine, invalid }) => ( <Modal isOpen toggle={onClose}> <Form onSubmit={handleSubmit}> <ModalHeader toggle={onClose}> {post.id ? 'Edit Post' : 'New Post'} </ModalHeader> <ModalBody> <FormGroup> <Label>Author</Label> <Field required name="author" className="form-control" component="input" /> </FormGroup> <FormGroup> <Label>Body</Label> <Field required name="body" className="form-control" component="input" /> </FormGroup> </ModalBody> <ModalFooter> <Button type="submit" disabled={pristine} color="primary">Save</Button> <Button color="secondary" onClick={onClose}>Cancel</Button> </ModalFooter> </Form> </Modal> )}/> ); export default PostEditor;
The
submitPostmutation is the new mutation to connect to the backend. It can use the
PostInputtype defined in the server:
const SUBMIT_POST = gql` mutation SubmitPost($input: PostInput!) { submitPost(input: $input) { id } } `;
Final Form takes an
onSubmitfunction that will pass in the data entered by the user. After the post is submitted, you’ll want to close the modal, so
PostEditortakes an
onCloseprop to call when you’re done submitting.
Final Form also takes an
initialValuesobject to define what values the form should initially have. In this case, the
PostEditorcomponent will take a
postprop that has the variables you need in it, so that gets passed along as the initial values.
The other required prop is the
renderfunction, which will render the form. Final Form gives you a few useful form props so you can know if the form is valid or not, or if it’s been modified from the
initialValues.
const PostEditor = ({ post, onClose }) => ( <FinalForm onSubmit={/* ... */} initialValues={post} render={/* ... */} /> ); export default PostEditor;
In the
onSubmitfunction, you’ll call the mutation needed to submit the post. Apollo lets you re-fetch queries. Since you know your list of posts will be out of date once you submit edits, you can re-fetch the
GET_POSTSquery here.
onSubmit={async ({ id, author, body }) => { const input = { id, author, body }; await client.mutate({ variables: { input }, mutation: SUBMIT_POST, refetchQueries: () => [{ query: GET_POSTS }], }); onClose(); }}
The
renderfunction will display a Bootstrap modal. This
PostEditorcomponent will only be rendered when you want it to be open, so
isOpenis just set to
true. Here you also use the
onCloseprop to close the modal when the user clicks outside the modal, hits
Esc, or clicks the Cancel button.
The form needs to have the
handleSubmitfunction passed to it as an
onSubmitprop. This tells the form to go through Final Form instead of sending a
POSTrequest to the page.
Final Form also handles all the boilerplate needed to have a controlled
input. Instead of storing the data in state whenever the user types something, you can just use the
Fieldcomponent.
render={({ handleSubmit, pristine, invalid }) => ( <Modal isOpen toggle={onClose}> <Form onSubmit={handleSubmit}> <ModalHeader toggle={onClose}> {post.id ? 'Edit Post' : 'New Post'} </ModalHeader> <ModalBody> <FormGroup> <Label>Author</Label> <Field required name="author" className="form-control" component="input" /> </FormGroup> <FormGroup> <Label>Body</Label> <Field required name="body" className="form-control" component="input" /> </FormGroup> </ModalBody> <ModalFooter> <Button type="submit" disabled={pristine} color="primary">Save</Button> <Button color="secondary" onClick={onClose}>Cancel</Button> </ModalFooter> </Form> </Modal> )}
Next, you’ll have to make a couple small changes to your
PostViewer. This adds a hook to each row so that you can determine whether the row should be editable or not and if so, changes the styles a bit and lets you click on the row. Clicking on the row calls another callback, which you can use to set which post is being edited.
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 }) => ( <Query query={GET_POSTS}> {({ loading, data }) => !loading && ( <Table> @@ -25,7 +29,11 @@ export default () => ( </thead> <tbody> {data.posts.map(post => ( - <tr key={post.id}> + <tr + key={post.id} + style={rowStyles(post, canEdit)} + onClick={() => canEdit(post) && onEdit(post)} + > <td>{post.author}</td> <td>{post.body}</td> </tr> @@ -35,3 +43,10 @@ export default () => ( )} </Query> ); + +PostViewer.defaultProps = { + canEdit: () => false, + onEdit: () => null, +}; + +export default PostViewer;
Now, tie this all together in
src/App.js. You can create a “New Post” button to create a new post, and make it so that you can edit any other existing post as well:
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 ( <Container fluid> <Button className="my-2" color="primary" onClick={() => this.setState({ editing: {} })} > New Post </Button> <PostViewer canEdit={() => true} onEdit={(post) => this.setState({ editing: post })} /> {editing && ( <PostEditor post={editing} onClose={() => this.setState({ editing: null })} /> )} </Container> ); } } export default App;
One simple way to add authentication to your project is 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. If you don’t already have one, sign up for a forever-free developer account. Log in to your developer console, navigate to Applications, then click Add Application. Select Single-Page App, then click Next.
Click Done to save your app, then copy your Client ID and paste it as a variable into a file called
.env.localin the root of your project. This will allow you to access the file in your code without needing to store credentials in source control. You’ll also need to add your organization URL (without the
-adminsuffix). Environment variables (other than
NODE_ENV) need to start with
REACT_APP_in order for Create React App to read them, so the file should end up looking like this:
REACT_APP_OKTA_CLIENT_ID={yourClientId} REACT_APP_OKTA_ORG_URL=https://{yourOktaDomain}You’re also going to need an API token later for the server, so while you’re in there, navigate to API -> Tokens, then click on Create Token. You can have many tokens, so just give this one a name that reminds you what it’s for, like “GraphQL Express”. You’ll be given a token that you can only see right now. If you lose the token, you’ll have to create another one. Add this to
.envalso.
REACT_APP_OKTA_TOKEN={yourOktaAPIToken}
The easiest way to add Authentication with Okta to a React app is to use Okta’s React SDK. You’ll also need to add routes, which can be done using React Router.
yarn add @okta/okta-react@1.1.1 react-router-dom@4.3.1
In order to know if the user is authenticated, Okta requires the app to be wrapped in a
Securitycomponent with some configuration. It also depends on React Router, so you’ll end up with a
BrowserRoutercomponent, wrapping a
Securitycomponent, wrapping an
ApolloProvidercomponent, which finally wraps your
Appin a
Route. Your
src/index.jsfile should end up looking like this:
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( <BrowserRouter> <Security issuer={`${process.env.REACT_APP_OKTA_ORG_URL}/oauth2/default`} redirect_uri={`${window.location.origin}/implicit/callback`} client_id={process.env.REACT_APP_OKTA_CLIENT_ID} > <ApolloProvider client={client}> <Route path="/implicit/callback" component={ImplicitCallback} /> <Route path="/" component={App} /> </ApolloProvider> </Security> </BrowserRouter>, document.getElementById('root') ); registerServiceWorker(); if (module.hot) module.hot.accept();
The Okta SDK comes with a
withAuthhigher order component (HoC) that can be used for a wide variety of auth-related things, but for this example, you’ll only need to know whether or not you’re authenticated, and some information about the user. To make this a little easier, I wrote a simple HoC to override the one that comes with the Okta SDK. Create a new file
src/withAuth.jscontaining the following:
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 <Component {...props} auth={this.state} />; } });
Now you can wrap the
Appcomponent with this
withAuthHoC. For a short time when the app first loads, Okta won’t quite be sure whether a user is logged in or not. To keep things simple, just don’t render anything in your
Appcomponent during this loading period. You could, however, choose to render the posts and just disable editing until you know more information about the user.
At the very top of your render function in
src/App.js, add the following:
const { auth } = this.props; if (auth.loading) return null; const { user, login, logout } = auth;
{user ? ( <div> <Button className="my-2" color="primary" onClick={() => this.setState({ editing: {} })} > New Post </Button> <Button className="m-2" color="secondary" onClick={() => logout()} > Sign Out (signed in as {user.name}) </Button> </div> ) : ( <Button className="my-2" color="primary" onClick={() => login()} > Sign In </Button> )}To make sure you also can’t edit a post unless you’re logged, change the
canEditprop to check that you have a user.
canEdit={() => Boolean(user)}
You also need to export
withAuth(App)instead of
App. Your
src/App.jsfile should now look like this:
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 ( <Container fluid> {user ? ( <div> <Button className="my-2" color="primary" onClick={() => this.setState({ editing: {} })} > New Post </Button> <Button className="m-2" color="secondary" onClick={() => logout()} > Sign Out (signed in as {user.name}) </Button> </div> ) : ( <Button className="my-2" color="primary" onClick={() => login()} > Sign In </Button> )}<PostViewer canEdit={() => Boolean(user)}onEdit={(post) => this.setState({ editing: post })} /> {editing && ( <PostEditor post={editing} onClose={() => this.setState({ editing: null })} /> )} </Container> ); } } export default withAuth(App);
The web app now requires you to be logged in to create a post, but a savvy user could still modify the data by sending a request directly to your server. To prevent this, add some authentication to the server. You’ll need to add Okta’s Node SDK and the JWT Verifier as dependencies. You’ll also need to use
dotenvin order to read the variables from
.env.local.
yarn add @okta/jwt-verifier@0.0.12 @okta/okta-sdk-nodejs@1.2.0 dotenv@6.0.0
At the top of your
src/server/index.jsfile, you’ll need to tell
dotenvto read in the environment variables:
require('dotenv').config({ path: '.env.local' });
You’re going to need the frontend to send a JSON Web Token (JWT) so that users can identify themselves. When you get a JWT on the server, you’ll need to verify it using Okta’s JWT Verifier. To get more information about a user, you’ll also need to use Okta’s Node SDK. You can set these up near the top of your server, just after all the other
requirestatements.
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, });
Now that you’re going to be using real users, it doesn’t make as much sense to just send a string with the username, especially since that could change over time. It would be better if a post is associated with a user. To set this up, create a new
AUTHORSvariable for your users, and change the
POSTSvariable to just have an
authorIdinstead of an
authorstring:
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!" }, ];
In your schema, you’ll no longer need the
author: Stringinput in
PostInput, and
authoron
Postshould now be of type
Authorinstead of
String. You’ll also need to make this new
Authortype:
type Author { id: ID name: String }
When looking up your user, you’ll now want to pull the author from the
AUTHORSvariable:
const mapPost = (post, id) => post && ({ ...post, id, author: AUTHORS[post.authorId], });
Now, you’ll need to create a
getUserIdfunction that can verify the access token and fetch some information about the user. The token will be sent as an
Authorizationheader, and will look something like
Bearer eyJraWQ...7h-zfqg. The following function will add the author’s name to the
AUTHORSobject if it doesn’t already exist.
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; } };
Now you can change the
submitPostfunction to get the user’s ID when they post. If the user isn’t logged in, you can just return
null. This will prevent the post from getting created. You can also return
nullif the user is trying to edit a post they didn’t create.
- 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 {
Your final
src/server/index.jsfile should now look like this:
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`);
You’ll now need to make a few more frontend changes to make sure you’re requesting an
authorobject instead of assuming it’s a string, and you’ll need to pass in your auth token as a header.
The
PostViewercomponent will need a minor update
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)} > - <td>{post.author}</td> + <td>{post.author.name}</td> <td>{post.body}</td> </tr> ))}
In
PostEditoryou’ll just need to get rid of the
authoraltogether since that won’t be editable by the user, and will be determined by the auth token.
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 }) => ( <FinalForm - onSubmit={async ({ id, author, body }) => { - 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'} </ModalHeader> <ModalBody> - <FormGroup> - <Label>Author</Label> - <Field - required - name="author" - className="form-control" - component="input" - /> - </FormGroup> <FormGroup> <Label>Body</Label> <Field
Your Apollo Client is where you’ll be sending the auth token. In order to access the auth token, you’ll need some sort of closure. On each request, Apollo lets you modify headers. Change
src/apollo.jsto the following:
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}`, }, }); }, });
Now you’ll need to call the
updateAuthcomponent whenever
authchanges in
src/withAuth.js, to make sure that’s always up to date.
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();
Now if you change
canEditin your
src/App.jsfile once again, you can make it so users can only edit their own posts:
onChange={(post) => user && user.sub === post.author.id}
You’ve now successfully built a GraphQL server, hooked it up to React, and locked it down with secure user authentication! As an exercise, see if you can switch the server from using simple, in-memory JavaScript objects to using a persistent data storage. For an example of using Sequelize in Node, check out Randall’s blog.
If you’d like to see the final sample code, you can find it on github.
- Build and Understand Express Middleware Through Examples
- Build a Basic CRUD App with Node and React
- Build and Understand a Simple Node.js Website with User Authentication
- Build a Health Tracking App with React, GraphQL, and User Authentication
If you have any questions about this post, please add a comment below. For more awesome content, follow @oktadev on Twitter, like us on Facebook, or subscribe to our YouTube channel.
from: https://www.sitepoint.com//build-a-simple-web-app-with-express-react-and-graphql/
dangzhuang7815 原创文章 0获赞 0访问量 309 关注 私信- erui _ eruie 002使用Express,Angular和GraphQL构建一个简单的Web应用程序
- 用 Express、React 和 GraphQL 构建一个简单的 Web 应用程序
- 使用Express和GraphQL构建简单的API服务
- Spring学习(二)——使用Gradle构建一个简单的Spring MVC Web应用程序
- erui _ eruie 002使用Express在Node中构建您的第一个路由器
- erui _ eruie 002如何使用TypeScript通过Express构建节点API
- 简单构建一个xmlhttp对象池合理创建和使用xmlhttp对象
- 使用Beetle.Express简单构建高吞吐的TCP&UDP应用
- 使用ASP.NET MVC2+PDF.NET 构建一个简单的新闻管理程序
- 使用react-native做一个简单的应用-06商品界面的实现
- 边学边用--使用React下的Material UI框架开发一个简单的仿MetaMask的网页版以太坊钱包(一)
- 使用Beetle.Express简单构建高吞吐的TCP&UDP应用
- 构建一个简单的jquery定时器,方便随时拿来使用。
- GraphQL 用例:使用 Golang 和 PostgreSQL 构建一个博客引擎 API
- 【React】知识点归纳:使用redux来编写一个简单的应用
- 构建一个简单的使用Spring Security保护的web应用
- 使用ant工具——构建一个简单的Hibernate应用程序
- 4 - Swift之2 - 使用xcode7构建一个简单的应用并在IOS9设备上真机运行
- 使用react-native做一个简单的应用-03欢迎界面
- 使用React并做一个简单的to-do-list