您的位置:首页 > Web前端 > AngularJS

erui _ eruie 002使用Express,Angular和GraphQL构建一个简单的Web应用程序

2020-05-06 04:36 1151 查看

Ť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

npm
command 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
sqlite3
command line tool. If you haven’t got
sqlite3
installed, 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.git
In this tutorial, I will only be using two of the files from this repository,
atp_players.csv
and
atp_rankings_current.csv
. In your
server/
directory start SQLite.

sqlite3 tennis.db

This will create a file

tennis.db
that 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 rankings
In 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

db
on 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.js
file.

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

graphqlHTTP
and
buildSchema
. The function
graphqlHTTP
plugs into Express and is able to understand and respond to GraphQL requests. The
buildSchema
is used to create a GraphQL schema from a string. Let’s look at the schema definition in a little more detail.

The two types

Player
and
Ranking
reflect 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
Ranking
contains a
player
field that has the
Player
type. At this point, the database only has an
INTEGER
that refers to a row in the
players
table. The GraphQL data structure should replace this integer with the player it refers to.

The

type Query
defines the queries a client is allowed to make. In this example, there are three queries.
players
returns an array of
Player
structures. The list can be restricted by an
offset
and a
limit
. This will allow paging through the table of players. The
player
query returns a single player by its
ID
. The
rankings
query will return an array of
Ranking
objects for a given player rank.

To make your life a little easier, create a utility function that issues an SQL query and returns a

Promise
that resolves when the query returns. This is helpful because the
sqlite3
interface is based on callbacks but GraphQL works better with Promises. In
index.js
add 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

rootValue
to 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

SELECT
statements. The result is passed straight back. The
rankings
query is a little more complicated because a
LEFT JOIN
statement 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
args
contains 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

graphqlHTTP
function 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

graphiql
provides you with a nice user interface on which you can test queries to the server.

node index.js
Then 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

app
module. 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/app
and add the component
.ts
code file, the
.html
template and the
.css
stylesheet 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.ts
in the
/src/app/
folder and import it into the main
app
module. 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 app
I will be using the Forms Module in order to bind values to input elements in the HTML. Open up
src/app/app.module.ts
and add

import { FormsModule } from '@angular/forms';

to the top of the file. Then add

FormsModule
to the
imports
array in the
@NgModule
declaration.

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.html
and 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
routes
array.

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.css
paste 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
Apollo
and
QueryRef
from
apollo-angular
, and
gql
from
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

gql
tag 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
limit
parameter 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

PlayersComponent
specify the state of the component. The property
page
stores the current page in the list of players.
players
will contain the array of players that will be displayed in a table. There is also a
query
variable 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
apollo
property 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

ngOnInit
method 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_QUERY
together with a value for the
offset
parameter. You can now subscribe to any data changes using
valueChanges.subscribe
. This method takes a callback which will set the
players
array 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,

nextPage
and
prevPage
will increment or decrement the
page
property. By calling
refetch
on
query
with 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.ts
looks 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_QUERY
queries the players over time which held a particular rank. Note that the query is only requesting the
first_name
and
last_name
of 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-angular
library. 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

imports
of the
app
module.

OktaAuthModule.initAuth({
issuer: 'https://{yourOktaDomain}/oauth2/default',
redirectUri: 'http://localhost:4200/implicit/callback',
clientId: '{yourClientId}'
});

You will need to replace

yourOktaDomain
development domain you see in your browser when you navigate to your Okta dashboard. Also, replace
yourClientId
with 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.ts
and import
OktaAuthService
from
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

OktaAuthService
service is injected through the constructor. It is then used to set the
isAuthenticated
flag. The
subscribe
method subscribes a callback function that is triggered whenever the log-in status changes. The
isAuthenticated
is initialized during the
ngOnInit
phase to reflect the log-in status when the application is first loaded.
login
and
logout
handle the process of logging in and out. In order to make authentication work,
okta-angular
uses a special route called
implicit/callback
. In the file
app-routing.module.ts
add the following import.

import { OktaCallbackComponent, OktaAuthGuard } from '@okta/okta-angular';

The

implicit/callback
route is now linked to the
OktaCallbackComponent
by adding the following to the
routes
array.

{
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

canActivate
property. Add the following to the
players
and the
ranking
routes.

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

createApollo
function 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.0
Next, let’s create that function in a separate file called
auth.js
in 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

yourOktaDomain
and
yourClientId
with 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,
oktaJwtVerifier
checks the validity of the token. If everything is in order, calling
next()
signals success. Otherwise, a
401
error 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.js
file.

const bodyParser = require('body-parser');
const bearerToken = require('express-bearer-token');
const oktaAuth = require('./auth');

Then modify the declaration of

app
in the following way.

const app = express()
.use(cors())
.use(bodyParser.json())
.use(bearerToken())
.use(oktaAuth);

The

bearerToken
middleware will look for a bearer token and add it to the request for
oktaAuth
to 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.

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 关注 私信
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: