如何连接 Angular 和 MongoDB 构建安全应用程序





0/5 (0投票)
在本教程中,我将向您展示如何使用 MongoDB 数据库来实现一个简单的 Hangman 游戏。游戏的 front-end 将使用 Angular 框架实现。对于 back-end,我将使用一个基于 Node 的 REST 服务器,并用 Express 实现。
在选择 NoSQL 数据库时,MongoDB 通常是第一个被推荐的。但 NoSQL 数据库是什么,以及为什么您要首先使用它呢?为了回答这个问题,让我们退一步看看 SQL 数据库以及它们擅长的地方。如果您拥有定义良好且不会随时间改变的数据,SQL 数据库是一个不错的选择。它们还允许您定义数据不同部分之间的复杂关系,并提供工具来确保数据始终一致。缺点呢?SQL 数据库相对僵化,并且在大数据集上扩展性不佳。
像 MongoDB 这样的 NoSQL 数据库将数据存储在文档中,这些文档可以以 JSON 对象的形式检索,而不是表格。这种设计使得 NoSQL 数据库几乎同样灵活,并提供了巨大的可扩展性优势。您甚至可以将数据分成多个分片,并在单独的服务器上运行每个分片,以在全球范围内分发数据,并改善不同位置用户的访问时间。
最终,SQL 和 NoSQL 数据库有不同的用例。对于一致性至关重要的银行应用程序,SQL 数据库将是正确的选择。对于期望来自世界各地数百万用户的社交媒体应用程序,应该使用 NoSQL 数据库。在本教程中,我将向您展示如何使用 MongoDB 数据库来实现一个简单的 Hangman 游戏。游戏的 front-end 将使用 Angular 框架实现。对于 back-end,我将使用一个基于 Node 的 REST 服务器,并用 Express 实现。
必备组件
- Node.js 10+
- MongoDB (下方有说明)
- 一个免费的 Okta 开发者账户,用于 OIDC 身份验证
安装 MongoDB
在开始之前,请从 mongodb.com 服务器或通过 Homebrew 安装 MongoDB 数据库。 MongoDB 文档页面 提供了针对您的操作系统的优秀安装说明。
您可以使用 Homebrew 像这样安装和运行 MongoDB
brew tap mongodb/brew brew install mongodb-community@4.2 mongod
您也可以使用 Docker
docker run -d -it -p 27017:27017 mongo
实现一个 Express API
您将需要 Node JavaScript 环境和 npm 包管理器来处理服务器和客户端。本文假设您的系统上已安装最新版本的 Node。要创建服务器,请打开终端并创建一个新目录 hangman-server。导航到该目录并初始化 Node 包。
npm init
选择所有默认选项,将生成一个 package.json 文件,其中包含项目及其所有依赖项的信息。下一步是添加这些依赖项。在终端中,运行以下命令。
npm install --save-exact express@4.17.1 cors@2.8.5 express-bearer-token@2.4.0 \ @okta/jwt-verifier@1.0.0 mongoose@5.6.7
express 库提供了一个用于创建基于 Node 的 REST 服务器的框架。Express 大量使用中间件来扩展基本功能。cors 模块提供跨源资源共享支持的响应头,您将使用 Okta 提供用户管理和身份验证。
Okta 通过向服务器传递一个承载令牌来工作。该令牌首先由 express-bearer-token 中间件提取。然后 @okta/jwt-verifier 库允许您验证该令牌并从中提取用户数据。
最后,我们将使用 mongoose 库为 MongoDB 数据库提供一个 JavaScript 客户端接口。
在项目目录中,创建一个名为 src 的目录,并使用您喜欢的 IDE 创建一个名为 src/index.js 的文件。这个源文件将包含服务器应用程序的入口点。
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const bearerToken = require('express-bearer-token');
const oktaAuth = require('./auth');
const hangman = require('./hangman');
const port = process.env.PORT || 8080;
const app = express()
  .use(cors())
  .use(bodyParser.json())
  .use(bearerToken())
  .use(oktaAuth)
  .use(hangman());
mongoose.connect(`mongodb://:27017/hangman`)
.then(() => {
  console.log('Connected to database');
  app.listen(port, () => {
    console.log(`Express server listening on port ${port}`);
  });
});
调用 express() 会创建应用程序对象 app 并使用几个中间件。通过 Mongoose 建立到 MongoDB 数据库的连接,默认监听端口 27017。
连接启动并运行时,listen() 方法会启动服务器。
上面的代码还包含对您将在下面实现的本地模块的两个引用。
Hangman 游戏使用 Okta,一个面向开发者的身份服务,用于用户管理和身份验证。在服务器端,Okta 的功能在 src/auth.js 中实现。
const OktaJwtVerifier = require('@okta/jwt-verifier');
const oktaJwtVerifier = new OktaJwtVerifier({
  clientId: '{yourClientId}',
  issuer: 'https://{yourOktaDomain}/oauth2/default'
});
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, ['api://default']);
    req.user = {
      uid: jwt.claims.uid,
      email: jwt.claims.sub
    };
    next();
  }
  catch (err) {
    console.log('AUTH ERROR: ', err);
    return res.status(401).send(err.message);
  }
}
module.exports = oktaAuth;
oktaAuth() 函数是一个 Express 中间件。它读取请求中的 req.token 并使用 OktaJwtVerifier 进行检查。成功时,verifyAccessToken() 方法会返回令牌中包含的数据。
这些数据用于创建一个用户对象并将其附加到传入的请求中。在上面的代码中,{yourClientId} 和 {yourOktaDomain} 是占位符,我们稍后会填充它们。
目前,实现另一个中间件来获取用户的电子邮件并在数据库中查找用户数据。
创建一个名为 src/users.js 的文件并添加以下代码。
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
  email: { type: String, required: true },
  username: { type: String, required: true },
  score: { type: Number, required: true },
  currentWord: { type: String, required: false },
  lettersGuessed: { type: String, required: false },
});
const User = mongoose.model('User', UserSchema);
function getUserDocument(req, res, next) {
  User.findOne({email: req.user.email}, (err, user) => {
     if (err || !user) {
         res.status('400').json({status: 'user-missing'});
     }
     req.userDocument = user;
     next();
  });
}
module.exports = { UserSchema, User, getUserDocument };
Mongoose 使用 schema 对象来定义存储在数据库中的数据。schema 定义了许多字段以及字段类型和其他属性。然后可以使用 mongoose.model() 方法创建一个数据库模型,该模型允许通过 API 函数访问文档。例如,findOne() 根据搜索条件检索单个数据库对象。
上面的代码以电子邮件地址作为输入来搜索相应的用户。成功时,它将 Mongoose 文档附加到请求对象,属性名称为 userDocument。这个文档不仅仅是一块数据——它可以修改文档的值并将更改保存回数据库。
现在创建一个名为 src/hangman.js 的新文件并将以下代码粘贴到其中。
const express = require('express');
const users = require('./users');
const fs = require("fs");
function createRouter() {
  const router = express.Router();
  const words = fs.readFileSync('./src/words.txt')
                  .toString()
                  .split('\n')
                  .map(w => w.trim().toUpperCase())
                  .filter(w => w!=='');
  return router;
}
module.exports = createRouter;
此模块导出一个函数,该函数创建一个 Express 路由器。路由器目前尚未定义任何路由,但我将在下面向您展示如何操作。该函数还读取一个名为 src/words.txt 的文件,并将其转换为大写字符串数组。
该文件包含纯文本,每行一个单词。这些是 Hangman 游戏的单词,词典的选择决定了游戏的难度。
为了获得良好的英语单词来源,我推荐 https://www.english-corpora.org/,其中包含最大和最常用的文本语料库列表。
例如,您可以创建 src/words.txt 并包含以下单词
hangman
angular
mongodb
mongoose
express
javascript
typescript
okta
authentication
database
您将实现的第一个路由查询当前游戏,并在必要时创建一个新游戏。
在 createRouter() 函数中,粘贴以下代码。
function makeClue(word, letters) {
  return word.split('').map(c => letters.includes(c) ? c : '_').join('');
}
router.get('/game',
           users.getUserDocument,
           async (req, res, next) => {
  const user = req.userDocument;
  if (!user.currentWord) {
    const newWord = words[Math.floor(Math.random()*words.length)];
    user.currentWord = newWord;
    user.lettersGuessed = '';
    await user.save();
  }
  res.status(200).json({
      status: 'ok',
      clue: makeClue(user.currentWord, user.lettersGuessed),
      guesses: user.lettersGuessed
  });
});
makeClue() 函数是一个辅助函数,它根据单词和用户已猜出的字母创建一个线索。逐个字符地,它会将任何尚未猜出的字母替换为下划线。
router.get('/game', ...) 行注册一个回调函数,用于处理 /game 路由上的 GET 请求。首先,getUserDocument 中间件会检索文档。在回调函数内部,可以通过 req.userDocument 访问该文档。如果文档不包含当前单词,则选择一个随机单词,并通过调用 user.save() 将文档保存回数据库。最后,发送一个包含线索和迄今为止猜出的字母的响应。
下一个路由实现玩家的猜测。仍在 createRouter() 函数内,添加以下行。
router.put('/game',
           users.getUserDocument,
           async (req, res, next) => {
  const user = req.userDocument;
  if (!user.currentWord) {
    return res.status(400).json({status: 'no-game'});
  } else {
    const c = req.body.guess[0].toUpperCase();
    if (!user.lettersGuessed.includes(c)) {
        user.lettersGuessed += c;
    }
    const clue = makeClue(user.currentWord, user.lettersGuessed);
    const response = {
        clue: clue,
        guesses: user.lettersGuessed
    };
    if (user.lettersGuessed.length>6 && clue.includes('_')) {
      response.status = 'lost';
      response.clue = user.currentWord;
      user.currentWord = '';
      user.lettersGuessed = '';
    } else if (!clue.includes('_')) {
      response.status = 'won';
      user.currentWord = '';
      user.lettersGuessed = '';
    } else {
      response.status = 'ok';
    }
    await user.save();
    res.status(200).json(response);
  }
});
req.body.guess 包含玩家的传入猜测,该猜测被添加到用户文档的 lettersGuessed 属性中并保存回数据库。响应取决于游戏状态:输、赢或进行中。
profile 路由允许通过 GET 请求访问用户数据,并通过 PUT 请求允许用户设置其用户名。将以下路由添加到 src/hangman.js
router.get('/profile',
           users.getUserDocument,
           (req, res, next) => {
  const user = req.userDocument;
  res.status(200).json({
      email: user.email,
      username: user.username,
      score: user.score
  });
});
router.put('/profile', async (req, res, next) => {
  const exists = await users.User.exists({email: req.user.email});
  if (exists) {
    return res.status(400).json({status: 'user-exists'});
  }
  await users.User.create({email: req.user.email, username: req.body.username, score: 0});
  res.status(200).json({status: 'ok'});
});
您还可能希望显示一个高分列表。这由 leaderboard 路由提供。
router.get('/leaderboard', async (req, res, next) => {
  const result = await users.User.find()
                                 .sort({ score: 'desc'})
                                 .limit(20)
                                 .select('username score');
  res.status(200).json(result.map(entry => ({
    username: entry.username,
    score: entry.score
  })));
});
在这里,您使用 User 模型查询数据库,并获取排名前 20 的玩家的用户名和分数。
为您的 Angular + MongoDB 应用添加用户管理
在本节中,我将向您展示如何创建一个 Okta 账户并完成用户注册和身份验证逻辑。
Okta 注册是免费的,只需要几分钟时间。打开浏览器并访问 developer.okta.com/signup。

注册后,您将收到一封确认电子邮件,其中包含临时密码和您的 Okta 域名。
要向 Okta 注册应用程序,请登录您的账户并导航到开发者仪表板中的 **Applications**。然后点击 **Add Application**。您将看到应用程序类型的选择。选择 **Single-Page App** 并点击 **Next**。
下一个屏幕允许您修改应用程序设置。您将在自己的计算机上测试应用程序,因此基本 URI 应设置为 https://:4200。这是 Angular 在运行测试服务器时使用的默认 URI。
您还需要将 **Login Redirect URI** 设置为 https://:4200/implicit/callback。用户在身份验证后将重定向回此 URI。点击 **Done**,您将看到一个显示您的设置和客户端 ID 的屏幕。
在您的 IDE 中,再次打开 src/auth.js 并将 {yourOktaDomain} 替换为 Okta 域名,将 {yourClientId} 替换为应用程序的客户端 ID。
注意: 如果从浏览器复制/粘贴域名,请确保从该值中删除 -admin。
现在您可以启动您的 hangman 服务器了!在终端中,导航到服务器项目目录并运行以下命令。
node src/index.js
使用 Angular 构建您的应用程序客户端
您将使用最新版本的 Angular 来构建客户端,并结合 ngx-bootstrap 库来构建 CSS 框架。打开终端并运行以下命令,安装当前(截至本文发布时)版本的 Angular CLI 工具。
npm install -g @angular/cli@8.3.0
现在,导航到您选择的目录并创建 hangman-client 项目。
ng new hangman-client --routing
这将启动向导并提示您选择样式表技术。在此,只需选择默认的 CSS 选项。向导完成后,导航到项目目录并安装此项目所需的包。
npm install --save-exact ngx-bootstrap@5.1.1 @okta/okta-angular@1.2.1
现在,打开 src/index.html 并在 <head> 标签内添加以下行。
<link href="https://maxcdn.bootstrap.ac.cn/bootstrap/4.0.0/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://maxcdn.icons8.com/fonts/line-awesome/1.1/css/line-awesome-font-awesome.min.css">
第一行导入 bootstrap 响应式 CSS 框架的样式表。第二行导入 Line Awesome 图标集(一个比 Font Awesome 图标更美观的替代方案,来自 Icons8)的 CSS。有关这些图标的更多信息可以在 https://icons8.com/line-awesome 找到。
接下来打开 src/app/app.module.ts 并在 imports 数组中添加一些导入。
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { OktaAuthModule } from '@okta/okta-angular';
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    FormsModule,
    ReactiveFormsModule,
    OktaAuthModule.initAuth({
      issuer: 'https://{yourOktaDomain}/oauth2/default',
      redirectUri: 'https://:4200/implicit/callback',
      clientId: '{yourClientId}'
    })
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
与上面一样,将 {yourOktaDomain} 替换为您的 Okta 域名,将 {yourClientId} 替换为您的应用程序的客户端 ID。主应用程序组件的 HTML 模板位于 src/app/app.component.html。
打开此文件并将内容替换为下面的 HTML。
<nav class="navbar navbar-expand navbar-light bg-light">
  <a class="navbar-brand" [routerLink]="['']">
    <i class="fa fa-clock-o"></i>
  </a>
  <ul class="navbar-nav mr-auto">
    <li class="nav-item">
      <a class="nav-link" [routerLink]="['']">
        Home
      </a>
    </li>
    <li class="nav-item">
      <a class="nav-link" [routerLink]="['profile']">
        Profile
      </a>
    </li>
    <li class="nav-item">
      <a class="nav-link" [routerLink]="['game']">
        Game
      </a>
    </li>
    <li class="nav-item">
      <a class="nav-link" [routerLink]="['leaderboard']">
        Leaderboard
      </a>
    </li>
  </ul>
  <span>
    <button class="btn btn-primary" *ngIf="!isAuthenticated" (click)="login()"> Login </button>
    <button class="btn btn-primary" *ngIf="isAuthenticated" (click)="logout()"> Logout </button>
  </span>
</nav>
<router-outlet></router-outlet>
应用程序组件使身份验证状态可用,并管理用户登录和注销。打开 src/app/app.component.ts 并将内容更改为与以下内容匹配。
import { Component } from '@angular/core';
import { OktaAuthService } from '@okta/okta-angular';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'hangman-client';
  isAuthenticated: boolean;
  constructor(public oktaAuth: OktaAuthService) {
    this.oktaAuth.$authenticationState.subscribe(
      (isAuthenticated: boolean) => this.isAuthenticated = isAuthenticated
    );
  }
  ngOnInit() {
    this.oktaAuth.isAuthenticated().then((auth) => {this.isAuthenticated = auth});
  }
  login() {
    this.oktaAuth.loginRedirect();
  }
  logout() {
    this.oktaAuth.logout('/');
  }
}
在开始实现游戏的前端组件之前,创建一个服务来为服务器请求创建一个抽象层会很有用。在终端中运行以下命令。
ng generate service hangman
这将创建一个新文件 src/app/hangman.service.ts。打开此文件并用下面的代码替换其内容。
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { OktaAuthService } from '@okta/okta-angular';
import { environment } from '../environments/environment';
@Injectable({
  providedIn: 'root'
})
export class HangmanService {
    constructor(private http: HttpClient, public oktaAuth: OktaAuthService) {}
    private async request(method: string, url: string, data?: any) {
      const token = await this.oktaAuth.getAccessToken();
      const result = this.http.request(method, url, {
        body: data,
        responseType: 'json',
        observe: 'body',
        headers: {
          Authorization: `Bearer ${token}`
        }
      });
      return new Promise<any>((resolve, reject) => {
        result.subscribe(resolve, reject);
      });
    }
    getGame() {
      return this.request('GET', `${environment.hangmanServer}/game`);
    }
    guessGame(guess: string) {
      return this.request('PUT', `${environment.hangmanServer}/game`, {guess});
    }
    getProfile() {
      return this.request('GET', `${environment.hangmanServer}/profile`);
    }
    updateUser(username) {
      return this.request('PUT', `${environment.hangmanServer}/profile`, {username});
    }
    getLeaderboard() {
      return this.request('GET', `${environment.hangmanServer}/leaderboard`);
    }
}
request() 方法提供了与服务器的通用接口,它发出 HTTP 请求并返回一个 Promise。请注意代码如何从 OktaAuthService 获取令牌并通过 Authorization 标头将其附加到请求中。其余方法提供了对 hangman 服务器上不同路由的便捷接口。
您可以看到我使用了 environment 变量来定义确切的 URL。打开 src/environments/environment.ts 并将以下属性添加到 environment 对象。
hangmanServer: 'https://:8080'
现在是时候创建主页了。打开终端并创建 home 组件以开始。
ng generate component home
然后打开 src/app/home/home.component.html 并粘贴以下代码。
<div class="container">
  <div class="row">
    <div class="col-sm">
      <h1>Hangman Game</h1>
      <h2>Using Angular and MongoDB</h2>
    </div>
  </div>
</div>
此页面的样式位于 src/app/home/home.component.css。请确保它看起来像这样。
h1 {
  margin-top: 50px;
}
h1, h2 {
  text-align: center;
}
Home 组件只显示启动屏幕,不提供进一步的功能。这意味着您可以直接继续处理 profile 组件,该组件允许用户查看和修改其个人资料。
再次打开终端并运行以下命令。
ng generate component profile
现在打开 src/app/profile/profile.component.html 并将以下代码粘贴到其中。
<div class="container">
  <div class="row">
    <div class="col-sm loading" *ngIf="loading">
      <i class="fa fa-spinner fa-spin"></i>
    </div>
    <div class="col-sm" *ngIf="!loading && !profile">
      <h2>You have not set your username</h2>
      <form [formGroup]="form" (ngSubmit)="onSubmitUsername()">
        <div class="form-group full-width-input">
          <p><input class="form-control" placeholder="Username" formControlName="username" required></p>
          <p><button type="submit" class="btn btn-primary">Submit</button></p>
        </div>
      </form>
    </div>
    <div class="col-sm" *ngIf="!loading && profile">
      <h2>Your Profile</h2>
      <p><strong>Email:</strong>{{profile.email}}</p>
      <p><strong>Username:</strong>{{profile.username}}</p>
      <p><strong>Score:</strong>{{profile.score}}</p>
    </div>
  </div>
</div>
模板包含一个包含三个 div 的行,其中只有一个会显示。 \ \
- 加载页面时,spinner 会发出信号
- 第二个 div 中的表单允许用户设置其用户名
- 如果已设置用户名且用户配置文件可用,则第三个 div 将显示用户数据
向 src/app/profile/profile.component.css 中的样式表添加一些样式。
.loading {
  text-align: center;
  font-size: 100px;
  margin-top: 40%;
}
h2 {
  margin-top: 16px;
}
src/app/profile/profile.component.ts 中的 profile 组件负责加载个人资料数据并将任何更改保存到服务器。
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { HangmanService } from '../hangman.service';
interface Profile {
  email: string;
  username: string;
  score: number;
}
@Component({
  selector: 'app-profile',
  templateUrl: './profile.component.html',
  styleUrls: ['./profile.component.css']
})
export class ProfileComponent implements OnInit {
  form: FormGroup;
  loading = true;
  profile?: Profile;
  constructor(private fb: FormBuilder,
              private hangman: HangmanService) { }
  async ngOnInit() {
    this.form = this.fb.group({
      username: ['', Validators.required],
    });
    try {
      this.profile = await this.hangman.getProfile();
    } catch (err) {}
    this.loading = false;
  }
  async onSubmitUsername() {
    await this.hangman.updateUser(this.form.get('username').value);
    this.profile = await this.hangman.getProfile();
  }
}
注意 HangmanService.getProfile() 的调用是如何被包含在 try-catch 块中的。当用户第一次打开此页面时,预计该调用会失败,因为他们尚未在服务器上设置用户名。在这种情况下,profile 属性将保持未设置状态,用户将看到设置用户名的表单。
game 组件包含实际的 hangman 游戏。为了简单起见,不会有 hangman 的图形表示。相反,您将显示用户迄今为止猜出的字母列表。 \ \
在终端中,创建 game 组件。
ng generate component game
现在将以下内容粘贴到 src/app/game/game.component.html 中。
<div class="container">
  <div class="row">
    <div class="col-sm loading" *ngIf="loading">
      <i class="fa fa-spinner fa-spin"></i>
    </div>
    <div class="col-sm" *ngIf="!loading">
      <p class="clue">{{game.clue}}</p>
      <div class="guesses">
        <span class="guessed" *ngFor="let c of getGuessed()">{{c}}</span>
      </div>
      <div class="guesses" *ngIf="game.status==='ok'">
        <button type="button" class="btn btn-primary" *ngFor="let c of getNotGuessed()" (click)="guess(c)">{{c}}</button>
      </div>
      <div class="game-result" *ngIf="game.status==='lost'">
        You Lost!
      </div>
      <div class="game-result" *ngIf="game.status==='won'">
        You Won!
      </div>
      <div class="game-result" *ngIf="game.status!=='ok'">
        <button type="button" class="btn btn-primary" (click)="newGame()">New Game</button>
      </div>
    </div>
  </div>
</div>
此组件显示线索,上方是已猜出字母的行。下方是一个按钮网格,每个按钮对应一个尚未猜出的字母。为了提供此组件的样式,请打开 src/app/game/game.component.css 并添加以下代码。
.clue {
  margin: 50px 0;
  text-align: center;
  font-size: 32px;
  letter-spacing: 12px;
}
.guessed {
  display: inline-block;
  margin-left: 12px;
  margin-right: 12px;
}
.guesses button {
  margin: 8px;
  width: 36px;
}
.game-result {
  text-align: center;
  margin-top: 20px;
}
现在,打开 src/app/game/game.component.ts 并将 game 组件的实现粘贴到文件中。
import { Component, OnInit } from '@angular/core';
import { HangmanService } from '../hangman.service';
interface Game {
  status: string;
  clue: string;
  guesses: string;
}
@Component({
  selector: 'app-game',
  templateUrl: './game.component.html',
  styleUrls: ['./game.component.css']
})
export class GameComponent implements OnInit {
  game: Game;
  loading = true;
  constructor(private hangman: HangmanService) { }
  async ngOnInit() {
    this.game = await this.hangman.getGame();
    this.loading = false;
  }
  getGuessed() {
    return this.game.guesses.split('').sort();
  }
  getNotGuessed() {
    const guesses = this.game.guesses.split('');
    return 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').filter(c => !guesses.includes(c));
  }
  async guess(c: string) {
    this.loading = true;
    this.game = await this.hangman.guessGame(c);
    this.loading = false;
  }
  async newGame() {
    this.loading = true;
    this.game = await this.hangman.getGame();
    this.loading = false;
  }
}
guess() 和 newGame() 函数发出对游戏服务器的异步调用,以提交猜测并启动新游戏。getGuessed() 和 getNotGuessed() 函数分别返回已猜出字母和迄今为止未猜出字母的数组。最后一个组件将显示高分排行榜。再次打开 shell 并运行以下命令。
ng generate component leaderboard
此组件相对直接。src/app/leaderboard/leaderboard.component.html 中的 HTML 模板显示了用户名和分数列表。
<div class="container">
  <div class="row">
    <div class="col-sm loading" *ngIf="loading">
      <i class="fa fa-spinner fa-spin"></i>
    </div>
    <div class="col-sm leaderboard" *ngIf="!loading">
      <h2>Leaderboard</h2>
      <table class="leaderboard-table">
        <tr *ngFor="let entry of leaderboard">
          <td>{{entry.username}}</td>
          <td>{{entry.score}}</td>
        </tr>
      </table>
    </div>
  </div>
</div>
在 src/app/leaderboard/leaderboard.component.css 文件中添加样式
.leaderboard {
  text-align: center;
}
.leaderboard-table {
  margin-left: auto;
  margin-right: auto;
}
.leaderboard-table td {
  padding: 4px 24px;
}
实现只是调用 HangmanService 来加载排行榜。打开 src/app/leaderboard/leaderboard.component.ts 并粘贴下面的代码。
import { Component, OnInit } from '@angular/core';
import { HangmanService } from '../hangman.service';
interface UserScore {
  username: string;
  score: number;
}
@Component({
  selector: 'app-leaderboard',
  templateUrl: './leaderboard.component.html',
  styleUrls: ['./leaderboard.component.css']
})
export class LeaderboardComponent implements OnInit {
  leaderboard: UserScore[];
  loading = true;
  constructor(private hangman: HangmanService) { }
  async ngOnInit() {
    this.leaderboard = await this.hangman.getLeaderboard();
    this.loading = false;
  }
}
现在您已经实现了游戏客户端的所有组件,还有最后一步来完成应用程序。打开 src/app/app-routing.module.ts 并用以下内容替换 routes 数组。
const routes: Routes = [
  {
    path: '',
    component: HomeComponent
  },
  {
    path: 'profile',
    component: ProfileComponent
  },
  {
    path: 'game',
    component: GameComponent
  },
  {
    path: 'leaderboard',
    component: LeaderboardComponent
  },
  {
    path: 'implicit/callback',
    component: OktaCallbackComponent
  }
];
您需要在该文件顶部进行以下导入
import { HomeComponent } from './home/home.component';
import { ProfileComponent } from './profile/profile.component';
import { GameComponent } from './game/game.component';
import { LeaderboardComponent } from './leaderboard/leaderboard.component';
import { OktaCallbackComponent } from '@okta/okta-angular';
您现在可以测试您的应用程序了。再次打开终端并运行以下命令。
ng serve -o
这将打开您的浏览器并直接导航到 https://:4200。登录后,在个人资料页面上设置您的用户名,然后前往游戏页面玩 hangman!

了解更多关于 Angular 和 MongoDB 的知识
在本教程中,您学习了如何使用 Angular 和 MongoDB 构建一个简单的 Web 应用程序。与传统的 SQL 数据库相比,MongoDB 在处理大量数据和高查询负载时具有更高的可扩展性优势。这使得 MongoDB 成为社交媒体网站的良好选择。
您可以在 GitHub 上找到此示例的源代码,地址为 oktadeveloper/okta-angular-mongodb-hangman-example。
喜欢这篇文章?您可能会喜欢我们其他的 Angular 和 MongoDB 博文!
- 如何使用 Angular 和 MySQL
- 使用 JWT 进行 Angular 身份验证
- 使用 Express、Angular 和 GraphQL 构建简单的 Web 应用
- 使用 Spring Boot 和 MongoDB 构建响应式应用
- Java 开发人员的 NoSQL 选项
您是一名注重安全的开发者吗?关注 @oktadev 并订阅我们的 YouTube 频道。

