erui _ eruie 002使用Express,Angular和GraphQL构建一个简单的Web应用程序
Ťhis article was originally published on the Okta developer blog. Thank you for supporting the partners who make SitePoint possible.
I will start by implementing the server. I will assume that you have Node installed on your system and that the
npmcommand is available. I will also be using SQLite to store the data. In order to create the database tables and import the data, I will be making use of the
sqlite3command line tool. If you haven’t got
sqlite3installed, head over to the SQLite download page and install the package that contains the command-line shell.
To start off, create a directory that will contain the server code. I have simply called mine
server/. Inside the directory run
npm init -y
npm install --save express@4.16.4 cors@2.8.4 express-graphql@0.6.12 graphql@14.0.2 sqlite3@4.0.2
git clone https://github.com/JeffSackmann/tennis_atp.gitIn this tutorial, I will only be using two of the files from this repository,
atp_players.csvand
atp_rankings_current.csv. In your
server/directory start SQLite.
sqlite3 tennis.db
This will create a file
tennis.dbthat will contain the data and will give you a command line prompt in which you can type SQL commands. Let’s create our database tables. Paste and run the following in the SQLite3 shell.
CREATE TABLE players( "id" INTEGER, "first_name" TEXT, "last_name" TEXT, "hand" TEXT, "birthday" INTEGER, "country" TEXT ); CREATE TABLE rankings( "date" INTEGER, "rank" INTEGER, "player" INTEGER, "points" INTEGER );
.mode csv .import {PATH_TO_TENNIS_DATA}/atp_players.csv players .import {PATH_TO_TENNIS_DATA}/atp_rankings_current.csv rankingsIn the above, replace
{PATH_TO_TENNIS_DATA}with the path in which you have downloaded the tennis data repository. You have now created a database that contains all ATP ranked tennis players ever, and the rankings of all active players during the current year. You are ready to leave SQLite3.
.quit
Let’s now implement the server. Open up a new file
index.js, the main entry point of your server application. Start with the Express and CORS basics.
const express = require('express'); const cors = require('cors'); const app = express().use(cors());
Now import SQLite and open up the tennis database in
tennis.db.
const sqlite3 = require('sqlite3'); const db = new sqlite3.Database('tennis.db');
This creates a variable
dbon which you can issue SQL queries and obtain results.
Now you are ready to dive into the magic of GraphQL. Add the following code to your
index.jsfile.
const graphqlHTTP = require('express-graphql'); const { buildSchema } = require('graphql'); const schema = buildSchema(` type Query { players(offset:Int = 0, limit:Int = 10): [Player] player(id:ID!): Player rankings(rank:Int!): [Ranking] } type Player { id: ID first_name: String last_name: String hand: String birthday: Int country: String } type Ranking { date: Int rank: Int player: Player points: Int } `);
The first two lines import
graphqlHTTPand
buildSchema. The function
graphqlHTTPplugs into Express and is able to understand and respond to GraphQL requests. The
buildSchemais used to create a GraphQL schema from a string. Let’s look at the schema definition in a little more detail.
The two types
Playerand
Rankingreflect the contents of the database tables. These will be used as the return types to the GraphQL queries. If you look closely, you can see that the definition of
Rankingcontains a
playerfield that has the
Playertype. At this point, the database only has an
INTEGERthat refers to a row in the
playerstable. The GraphQL data structure should replace this integer with the player it refers to.
The
type Querydefines the queries a client is allowed to make. In this example, there are three queries.
playersreturns an array of
Playerstructures. The list can be restricted by an
offsetand a
limit. This will allow paging through the table of players. The
playerquery returns a single player by its
ID. The
rankingsquery will return an array of
Rankingobjects for a given player rank.
To make your life a little easier, create a utility function that issues an SQL query and returns a
Promisethat resolves when the query returns. This is helpful because the
sqlite3interface is based on callbacks but GraphQL works better with Promises. In
index.jsadd the following function.
function query(sql, single) { return new Promise((resolve, reject) => { var callback = (err, result) => { if (err) { return reject(err); } resolve(result); }; if (single) db.get(sql, callback); else db.all(sql, callback); }); }
Now it’s time to implement the database queries that power the GraphQL queries. GraphQL uses something called
rootValueto define the functions corresponding to the GraphQL queries.
const root = { players: args => { return query( `SELECT * FROM players LIMIT ${args.offset}, ${args.limit}`, false ); }, player: args => { return query(`SELECT * FROM players WHERE id='${args.id}'`, true); }, rankings: args => { return query( `SELECT r.date, r.rank, r.points, p.id, p.first_name, p.last_name, p.hand, p.birthday, p.country FROM players AS p LEFT JOIN rankings AS r ON p.id=r.player WHERE r.rank=${args.rank}`, false ).then(rows => rows.map(result => { return { date: result.date, points: result.points, rank: result.rank, player: { id: result.id, first_name: result.first_name, last_name: result.last_name, hand: result.hand, birthday: result.birthday, country: result.country } }; }) ); } };
The first two queries are pretty straightforward. They consist of simple
SELECTstatements. The result is passed straight back. The
rankingsquery is a little more complicated because a
LEFT JOINstatement is needed to combine the two database tables. Afterward, the result is cast into the correct data structure for the GraphQL query. Note in all these queries how
argscontains the arguments passed in from the client. You do not need to worry in any way about checking missing values, assigning defaults, or checking the correct type. This is all done for you by the GraphQL server.
All that is left to do is create a route and link the
graphqlHTTPfunction into it.
app.use( '/graphql', graphqlHTTP({ schema, rootValue: root, graphiql: true }) ); app.listen(4201, err => { if (err) { return console.log(err); } return console.log('My Express App listening on port 4201'); });
The
graphiqlprovides you with a nice user interface on which you can test queries to the server.
node index.jsThen open your browser and navigate to
http://localhost:4201/graphql. You will see an interactive test-bed for GraphQL queries.
npm install -g @angular/cli@7.1.0
You might have to run this command using
sudo, depending on your operating system. Now you can create a new angular application. In a new directory run:
ng new AngularGraphQLClient
这将创建一个新目录,并将Angular应用程序的所有必需软件包安装到其中。 系统将提示您两个问题。 回答是在应用程序中包括路由。 我将在本教程中使用的样式表将是简单的CSS。
The application will contain three component associated with the main
appmodule. You can generate them by navigating into the directory that was just created and running the following three commands.
ng generate component Home ng generate component Players ng generate component Ranking
This will create three directories in
src/appand add the component
.tscode file, the
.htmltemplate and the
.cssstylesheet for each component. In order to use GraphQL in Angular, I will be making use of the Apollo library. Setting up Apollo in angular is a simple command.
ng add apollo-angular
This command will install a number of Node modules. It will also create an Angular module in a file
graphql.module.tsin the
/src/app/folder and import it into the main
appmodule. Inside this file, you will see the line
const uri = ''; // <-- add the URL of the GraphQL server here
const uri = 'http://localhost:4201/graphql';
注意:如果要在安装Apollo Angular之后生成任何组件,则需要指定该组件所属的模块。 所以产生家上面的组件将更改为
ng generate component Home --module appI will be using the Forms Module in order to bind values to input elements in the HTML. Open up
src/app/app.module.tsand add
import { FormsModule } from '@angular/forms';
to the top of the file. Then add
FormsModuleto the
importsarray in the
@NgModuledeclaration.
Now open
src/index.html. This file contains the HTML container in which your Angular app will live. You will need some external CSS and JavaScript resources to spruce up the design of your application. Add the following lines inside the
<head>tag. This will include some minimal Material Design styling.
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" /> <link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.indigo-pink.min.css" /> <script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
Next, open
src/app.component.htmland replace the content with the following.
<div class="mdl-layout mdl-js-layout mdl-layout--fixed-header"> <div class="mdl-layout__header mdl-layout__header--waterfall"> <div class="mdl-layout__header-row"> <span class="mdl-layout-title" routerLink="/"> <i class="material-icons">home</i> Angular with GraphQL </span> <!-- Add spacer, to align navigation to the right in desktop --> <div class="mdl-layout-spacer"></div> <!-- Navigation --> <ul class="mdl-navigation"> <li class="mdl-navigation__link" routerLink="/">Home</li> <li class="mdl-navigation__link" routerLink="/players">Players</li> <li class="mdl-navigation__link" routerLink="/ranking">Rankings</li> <li class="mdl-navigation__link" *ngIf="!isAuthenticated" (click)="login()" > Login </li> <li class="mdl-navigation__link" *ngIf="isAuthenticated" (click)="logout()" > Logout </li> </ul> </div> </div> <div class="mdl-layout__drawer"> <ul class="mdl-navigation"> <li class="mdl-navigation__link" routerLink="/">Home</li> <li class="mdl-navigation__link" routerLink="/players">Players</li> <li class="mdl-navigation__link" routerLink="/ranking">Rankings</li> <li class="mdl-navigation__link" *ngIf="!isAuthenticated" (click)="login()" > Login </li> <li class="mdl-navigation__link" *ngIf="isAuthenticated" (click)="logout()" > Logout </li> </ul> </div> <div class="mdl-layout__content content"><router-outlet></router-outlet></div> </div>
This creates a basic layout with a top-bar and a few links which will load different components into the
router-outlet. In order to load make the routes available to the application you should modify the
app-routing.module.ts. At the top you will see the declaration of the
routesarray.
const routes: Routes = [];
import { PlayersComponent } from './players/players.component'; import { HomeComponent } from './home/home.component'; import { RankingComponent } from './ranking/ranking.component'; const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'players', component: PlayersComponent }, { path: 'ranking', component: RankingComponent } ];
Finally, let’s give the page some styling. In
app.component.csspaste the following content.
.content { padding: 1rem; display: flex; justify-content: center; }
You are ready to implement the components. Let’s start with the component that lets the user page through all the tennis players in the database. Copy the following into the file
src/app/players/players.component.ts. I will walk you through the meaning of each part of this file next.
import { Component, OnInit } from '@angular/core'; import { Apollo, QueryRef } from 'apollo-angular'; import gql from 'graphql-tag'; const PLAYERS_QUERY = gql` query players($offset: Int) { players(offset: $offset, limit: 10) { id first_name last_name hand birthday country } } `; @Component({ selector: 'app-players', templateUrl: './players.component.html', styleUrls: ['./players.component.css'] }) export class PlayersComponent implements OnInit { page = 1; players: any[] = []; private query: QueryRef<any>; constructor(private apollo: Apollo) {} ngOnInit() { this.query = this.apollo.watchQuery({ query: PLAYERS_QUERY, variables: { offset: 10 * this.page } }); this.query.valueChanges.subscribe(result => { this.players = result.data && result.data.players; }); } update() { this.query.refetch({ offset: 10 * this.page }); } nextPage() { this.page++; this.update(); } prevPage() { if (this.page > 0) this.page--; this.update(); } }
import { Component, OnInit } from '@angular/core'; import { Apollo, QueryRef } from 'apollo-angular'; import gql from 'graphql-tag';Apart from the core Angular imports, this makes available
Apolloand
QueryReffrom
apollo-angular, and
gqlfrom
graphql-tag. The latter of these to is used straight away to create a GraphQL query.
const PLAYERS_QUERY = gql` query players($offset: Int) { players(offset: $offset, limit: 10) { id first_name last_name hand birthday country } } `;
The
gqltag takes the template string and turns it into a query object. The query defined here will ask the server to return an array of players, populated with all the player’s fields. The
limitparameter will cause the server to return at most 10 records. The offset parameter can be specified as a parameter to the query. This will allow paging through the players.
@Component({ selector: 'app-players', templateUrl: './players.component.html', styleUrls: ['./players.component.css'] }) export class PlayersComponent implements OnInit { page = 0; players: any[] = []; private query: QueryRef<any>; constructor(private apollo: Apollo) {} }
The properties of
PlayersComponentspecify the state of the component. The property
pagestores the current page in the list of players.
playerswill contain the array of players that will be displayed in a table. There is also a
queryvariable which stores the query. This is needed to be able to re-fetch data, whenever the user navigates to another page. The constructor will inject the
apolloproperty so that you can access the GraphQL interface.
ngOnInit() { this.query = this.apollo .watchQuery({ query: PLAYERS_QUERY, variables: {offset : 10*this.page} }); this.query.valueChanges.subscribe(result => { this.players = result.data && result.data.players; }); }
During the initialization phase of the component’s life-cycle the
ngOnInitmethod will be called. This is the place where the Players Component will initiate the loading of the data. This is achieved by
this.apollo.watchQuery. By passing the
PLAYERS_QUERYtogether with a value for the
offsetparameter. You can now subscribe to any data changes using
valueChanges.subscribe. This method takes a callback which will set the
playersarray with the data obtained from the server.
update() { this.query.refetch({offset : 10*this.page}); } nextPage() { this.page++; this.update(); } prevPage() { if (this.page>0) this.page--; this.update(); }
To round things off,
nextPageand
prevPagewill increment or decrement the
pageproperty. By calling
refetchon
querywith the new parameters a server request is issued. When the data is received the subscription callback will be called automatically.
The HTML template that goes with this component is stored in
players.component.html. Paste the following content into it.
<table class="mdl-data-table mdl-js-data-table mdl-data-table--selectable mdl-shadow--2dp" > <tr> <th class="mdl-data-table__cell--non-numeric">First Name</th> <th class="mdl-data-table__cell--non-numeric">Last Name</th> <th class="mdl-data-table__cell--non-numeric">Hand</th> <th>Birthday</th> <th class="mdl-data-table__cell--non-numeric">Country</th> </tr> <tr *ngFor="let player of players"> <td class="mdl-data-table__cell--non-numeric">{{player.first_name}}</td> <td class="mdl-data-table__cell--non-numeric">{{player.last_name}}</td> <td class="mdl-data-table__cell--non-numeric">{{player.hand}}</td> <td>{{player.birthday}}</td> <td class="mdl-data-table__cell--non-numeric">{{player.country}}</td> </tr> </table> <div class="paging"> <button class="mdl-button mdl-js-button mdl-button--colored" (click)="prevPage()" > <i class="material-icons">arrow_back</i> </button> Page {{page+1}} <button class="mdl-button mdl-js-button mdl-button--colored" (click)="nextPage()" > <i class="material-icons">arrow_forward</i> </button> </div>
The Ranking component pretty much follows the same pattern. The
src/app/ranking.component.tslooks like this.
import { Component, OnInit } from '@angular/core'; import { Apollo, QueryRef } from 'apollo-angular'; import gql from 'graphql-tag'; const RANKINGS_QUERY = gql` query rankings($rank: Int!) { rankings(rank: $rank) { date rank points player { first_name last_name } } } `; @Component({ selector: 'app-ranking', templateUrl: './ranking.component.html', styleUrls: ['./ranking.component.css'] }) export class RankingComponent implements OnInit { rank: number = 1; rankings: any[]; private query: QueryRef<any>; constructor(private apollo: Apollo) {} ngOnInit() { this.query = this.apollo.watchQuery({ query: RANKINGS_QUERY, variables: { rank: Math.round(this.rank) } }); this.query.valueChanges.subscribe(result => { this.rankings = result.data && result.data.rankings; }); } update() { return this.query.refetch({ rank: Math.round(this.rank) }); } }
As you can see, most of the code is very similar to that in
players.component.ts. The definition of
RANKINGS_QUERYqueries the players over time which held a particular rank. Note that the query is only requesting the
first_nameand
last_nameof the player. This means that the server will not be sending any additional player data back which the client hasn’t asked for.
The template for the rankings component contains a text field and button in which the user can enter a rank and reload the page. Below that is the table of players. This is the content of
ranking.component.html.
<h1>Rankings</h1> <input class="mdl-textfield__input" type="text" id="rank" [(ngModel)]="rank" /> <button class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect" (click)="update()" > Update </button> <table class="mdl-data-table mdl-js-data-table mdl-shadow--2dp" *ngIf="rankings" > <tr> <th>Rank</th> <th>Date</th> <th>Points</th> <th class="mdl-data-table__cell--non-numeric">First Name</th> <th class="mdl-data-table__cell--non-numeric">Last Name</th> </tr> <tr *ngFor="let ranking of rankings"> <td>{{ranking.rank}}</td> <td>{{ranking.date}}</td> <td>{{ranking.points}}</td> <td class="mdl-data-table__cell--non-numeric"> {{ranking.player.first_name}} </td> <td class="mdl-data-table__cell--non-numeric"> {{ranking.player.last_name}} </td> </tr> </table>
ng serve
In order to use Okta authentication with the Angular client, you will have to install the
okta-angularlibrary. In the base directory of your client application run the following command.
npm install @okta/okta-angular@1.0.7 apollo-link-context@1.0.10 --save
Now open
src/app/app.module.ts. At the top of the file add the import statement.
import { OktaAuthModule } from '@okta/okta-angular';
Now add the module to the list of
importsof the
appmodule.
OktaAuthModule.initAuth({ issuer: 'https://{yourOktaDomain}/oauth2/default', redirectUri: 'http://localhost:4200/implicit/callback', clientId: '{yourClientId}' });
You will need to replace
yourOktaDomaindevelopment domain you see in your browser when you navigate to your Okta dashboard. Also, replace
yourClientIdwith the client ID that you obtained when registering your application. Now you are ready to use Okta authentication throughout your application. Next, you will implement logging in and logging out from the application. Open
app.component.tsand import
OktaAuthServicefrom
okta-angular. Paste the following code into the file.
import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { OktaAuthService } from '@okta/okta-angular'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit { public title = 'My Angular App'; public isAuthenticated: boolean; constructor(public oktaAuth: OktaAuthService) { this.oktaAuth.$authenticationState.subscribe( (isAuthenticated: boolean) => (this.isAuthenticated = isAuthenticated) ); } async ngOnInit() { this.isAuthenticated = await this.oktaAuth.isAuthenticated(); } login() { this.oktaAuth.loginRedirect(); } logout() { this.oktaAuth.logout('/'); } }
The
OktaAuthServiceservice is injected through the constructor. It is then used to set the
isAuthenticatedflag. The
subscribemethod subscribes a callback function that is triggered whenever the log-in status changes. The
isAuthenticatedis initialized during the
ngOnInitphase to reflect the log-in status when the application is first loaded.
loginand
logouthandle the process of logging in and out. In order to make authentication work,
okta-angularuses a special route called
implicit/callback. In the file
app-routing.module.tsadd the following import.
import { OktaCallbackComponent, OktaAuthGuard } from '@okta/okta-angular';
The
implicit/callbackroute is now linked to the
OktaCallbackComponentby adding the following to the
routesarray.
{ path: 'implicit/callback', component: OktaCallbackComponent }
This is all that is needed to log in and out. But the application is not protected yet. For any route that you want to access-control, you will have to add an Authorization Guard. Luckily this is easy. In each of the routes that you want to protect add the
canActivateproperty. Add the following to the
playersand the
rankingroutes.
canActivate: [OktaAuthGuard];
You have secured the client pages, but before you can move on to securing the back end let’s take a moment and think about how the server will authenticate the user. Okta uses a bearer token that identifies the user. The bearer token must be sent to the server with every request. To achieve this, the client has to make sure that the bearer token is added to the HTTP headers. All you need to do is add a few lines of code to the
graphql.module.ts. At the top of the file import the following.
import { OktaAuthService } from '@okta/okta-angular'; import { setContext } from 'apollo-link-context';
Then modify the
createApollofunction to add the bearer token.
export function createApollo(httpLink: HttpLink, oktaAuth: OktaAuthService) { const http = httpLink.create({ uri }); const auth = setContext((_, { headers }) => { return oktaAuth.getAccessToken().then(token => { return token ? { headers: { Authorization: `Bearer ${token}` } } : {}; }); }); return { link: auth.concat(http), cache: new InMemoryCache() }; }
npm install @okta/jwt-verifier@0.0.13 body-parser@1.18.3 express-bearer-token@2.2.0Next, let’s create that function in a separate file called
auth.jsin the root folder of the server.
const OktaJwtVerifier = require('@okta/jwt-verifier'); const oktaJwtVerifier = new OktaJwtVerifier({ clientId: '{yourClientId}', issuer: 'https://{yourOktaDomain}/oauth2/default' }); module.exports = async function oktaAuth(req, res, next) { try { const token = req.token; if (!token) { return res.status(401).send('Not Authorized'); } const jwt = await oktaJwtVerifier.verifyAccessToken(token); req.user = { uid: jwt.claims.uid, email: jwt.claims.sub }; next(); } catch (err) { return res.status(401).send(err.message); } };
Again, you have to replace
yourOktaDomainand
yourClientIdwith the development domain and the client id. The purpose of this function is simple. It checks the presence of a token field in the request. If present,
oktaJwtVerifierchecks the validity of the token. If everything is in order, calling
next()signals success. Otherwise, a
401error is returned. All you have to do now is to make sure that the function is used in the application. Add the following require statements to the
index.jsfile.
const bodyParser = require('body-parser'); const bearerToken = require('express-bearer-token'); const oktaAuth = require('./auth');
Then modify the declaration of
appin the following way.
const app = express() .use(cors()) .use(bodyParser.json()) .use(bearerToken()) .use(oktaAuth);
The
bearerTokenmiddleware will look for a bearer token and add it to the request for
oktaAuthto find it. With this simple addition, your server will only allow requests that provide a valid authentication.
I have not talked about how to use GraphQL to add or modify the data in the database. In GraphQL language this is called mutations. To learn more about mutations using Apollo, check out the manual pages.
The complete code for this project can be found at https://github.com/oktadeveloper/okta-graphql-angular-example.
- Build a Simple API Service with Express and GraphQL
- Build a Secure API with Spring Boot and GraphQL
- Build and Understand Express Middleware
- Angular 6: What’s New and Why Upgrade?
- Build a Basic CRUD App with Angular and Node
Like what you learned today? We’d love to have you follow us on Twitter and subscribe to our YouTube channel!
from: https://www.sitepoint.com//build-a-simple-web-app-with-express-angular-and-graphql/
dangzhuang7815 原创文章 0获赞 0访问量 300 关注 私信- 使用bower+angular+express创建webapp(一)一个简单的demo
- Spring学习(二)——使用Gradle构建一个简单的Spring MVC Web应用程序
- 使用Express和GraphQL构建简单的API服务
- erui _ eruie 002使用Express在Node中构建您的第一个路由器
- 用 Express、React 和 GraphQL 构建一个简单的 Web 应用程序
- 简单构建一个xmlhttp对象池合理创建和使用xmlhttp对象
- 使用Beetle.Express简单构建高吞吐的TCP&UDP应用
- 使用ASP.NET MVC2+PDF.NET 构建一个简单的新闻管理程序
- nodejs使用express创建一个简单web应用
- 使用Beetle.Express简单构建高吞吐的TCP&UDP应用
- 构建一个简单的jquery定时器,方便随时拿来使用。
- 使用express搭建一个简单的查询服务器的方法
- 使用ant工具——构建一个简单的Hibernate应用程序
- 4 - Swift之2 - 使用xcode7构建一个简单的应用并在IOS9设备上真机运行
- GraphQL 用例:使用 Golang 和 PostgreSQL 构建一个博客引擎 API
- Angular5初探之--构建一个简单的单页应用,包含登录(login)和几个一级菜单
- 构建一个简单的使用Spring Security保护的web应用
- 使用ACE构建一个简单的客户端
- 使用rpcgen构建分布式程序的一个简单例子
- Hudson--一个使用简单的构建系统