Spring Cloud – Adding Angular 4 – Spring Cloud – 添加Angular 4

最后修改: 2017年 5月 26日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In our last Spring Cloud article, we added Zipkin support into our application. In this article, we’re going to be adding a front-end application to our stack.

在上一篇Spring Cloud文章中,我们在我们的应用程序中添加了Zipkin支持。在这篇文章中,我们将向我们的堆栈添加一个前端应用程序。

Up until now, we’ve been working entirely on the back end to build our cloud application. But what good is a web app if there’s no UI? In this article, we’re going to solve that issue by integrating a single page application into our project.

到目前为止,我们一直完全在后端工作,以建立我们的云应用程序。但是,如果没有用户界面,一个网络应用有什么用呢?在这篇文章中,我们将通过在我们的项目中整合一个单页应用程序来解决这个问题。

We will be writing this app using Angular and Bootstrap. The style of Angular 4 code feels a lot like coding a Spring app which is a natural crossover for a Spring developer! While the front end code will be using Angular, the content of this article can be easily extended to any front end framework with minimal effort.

我们将使用AngularBootstrap编写这个应用程序。Angular 4代码的风格感觉很像在编写Spring应用,这对Spring开发者来说是一个很自然的交叉点!虽然前端代码将使用Angular,但本文的内容可以很容易地扩展到任何前端框架,只需付出最小的努力。

In this article, we’re going to be building an Angular 4 app and connecting it to our cloud services. We will demonstrate how to integrate login between a SPA and Spring Security. We’ll also show how to access our application’s data using Angular’s support for HTTP communication.

在这篇文章中,我们将建立一个Angular 4应用程序,并将其连接到我们的云服务。我们将演示如何在SPA和Spring Security之间整合登录。我们还将展示如何使用Angular对HTTP通信的支持来访问我们应用程序的数据。

2. Gateway Changes

2.网关变化

With front end in place, we’re going to switch to form based login and secure parts of UI to privileged users. This requires making changes to our gateway security configuration.

随着前端的到位,我们将切换到基于表单的登录,并确保UI的部分内容对特权用户的安全。这需要对我们的网关安全配置进行修改。

2.1. Update HttpSecurity

2.1.更新HttpSecurity

First, let’s update configure(HttpSecurity http) method in our gateway SecurityConfig.java class:

首先,让我们更新我们的网关Configure(HttpSecurity http)类中的SecurityConfig.java方法。

@Override
protected void configure(HttpSecurity http) {
    http
      .formLogin()
      .defaultSuccessUrl("/home/index.html", true)
      .and()
    .authorizeRequests()
      .antMatchers("/book-service/**", "/rating-service/**", "/login*", "/")
      .permitAll()
      .antMatchers("/eureka/**").hasRole("ADMIN")
      .anyRequest().authenticated()
      .and()
    .logout()
      .and()
    .csrf().disable();
}

First, we add a default success URL to point to /home/index.html as this will be where our Angular app lives. Next, we configure the ant matchers to allow any request through the gateway except for the Eureka resources. This will delegate all security checks to back-end services.

首先,我们添加一个默认的成功URL,指向/home/index.html,因为这将是我们的Angular应用程序的所在地。接下来,我们配置蚂蚁匹配器,允许任何请求通过网关,除了Eureka资源。这将把所有安全检查委托给后端服务。

Next, we removed the logout success URL, as the default redirect back to the login page will work fine.

接下来,我们删除了注销成功的URL,因为默认的重定向回到登录页面就可以正常工作了。

2.2. Add a Principal Endpoint

2.2.添加一个校长端点

Next, let’s add an endpoint to return the authenticated user. This will be used in our Angular app to log in and identify the roles our user has. This will help us control what actions they can do on our site.

接下来,让我们添加一个端点来返回认证的用户。这将在我们的Angular应用程序中用于登录和识别我们的用户所拥有的角色。这将帮助我们控制他们在我们的网站上可以做什么动作。

In the gateway project, add an AuthenticationController class:

在网关项目中,添加一个AuthenticationController类。

@RestController
public class AuthenticationController {
 
    @GetMapping("/me")
    public Principal getMyUser(Principal principal) {
        return principal;
    }
}

The controller returns the currently logged in user object to the caller. This gives us all the information we need to control our Angular app.

该控制器将当前登录的用户对象返回给调用者。这为我们提供了控制Angular应用程序所需的所有信息。

2.3. Add a Landing Page

2.3.添加一个登陆页

Let’s add a very simple landing page so that users see something when they go to the root of our application.

让我们添加一个非常简单的登陆页面,以便用户在进入我们的应用程序的根目录时看到一些东西。

In src/main/resources/static, let’s add an index.html file with a link to the login page:

src/main/resources/static,中,让我们添加一个index.html文件,其中有一个指向登录页面的链接。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Book Rater Landing</title>
</head>
<body>
    <h1>Book Rater</h1>
    <p>So many great things about the books</p>
    <a href="/login">Login</a>
</body>
</html>

3. Angular CLI and the Starter Project

3.Angular CLI和启动项目

Before starting a new Angular project make sure to install the latest versions of Node.js and npm.

在开始一个新的Angular项目之前,请确保安装最新版本的Node.js和npm

3.1. Install the Angular CLI

3.1.安装Angular CLI

To begin, we will need to use npm to download and install the Angular command line interface. Open a terminal and run:

首先,我们需要使用npm来下载和安装Angular命令行界面。打开一个终端并运行。

npm install -g @angular/cli

This will download and install the CLI globally.

这将在全球范围内下载并安装CLI。

3.2. Install a New Project

3.2.安装一个新项目

While still in the terminal, navigate to the gateway project and go into the gateway/src/main folder. Create a directory called “angular” and navigate to it. From here run:

当仍然在终端时,导航到gateway项目,进入gateway/src/main文件夹。创建一个名为 “angular “的目录并导航到它。从这里运行。

ng new ui

Be patient; the CLI’s setting up a brand new project and downloading all the JavaScript dependencies with npm. It’s not uncommon for this process to take many minutes.

请耐心等待;CLI正在建立一个全新的项目,并通过npm下载所有的JavaScript依赖项。这个过程花上几分钟是常有的事。

The ng command’s the shortcut for the Angular CLI, the new parameter instructs that CLI to create a new project, and the ui command gives our project a name.

ng命令是Angular CLI的快捷方式,new参数指示CLI创建一个新的项目,而ui命令给我们的项目命名。

3.3. Run the Project

3.3.运行该项目

Once the new command’s complete. Navigate to the ui folder that was created and run:

一旦new命令完成。导航到所创建的ui文件夹,并运行。

ng serve

Once the project builds navigate to http://localhost:4200. We should see this in the browser:

一旦项目建立起来,就可以导航到http://localhost:4200。我们应该在浏览器中看到这个。

angular2 start 1

Congratulations! We just built an Angular app!

恭喜你!我们刚刚建立了一个Angular应用程序。我们刚刚建立了一个Angular应用程序!

3.4. Install Bootstrap

3.4.安装Bootstrap

Let’s use npm to install bootstrap. From the ui directory run this command:

让我们用npm来安装bootstrap。在ui目录下运行这个命令。

npm install bootstrap@4.0.0-alpha.6 --save

This will download bootstrap into the node_modules folder.

这将把bootstrap下载到node_modules文件夹中。

In the ui directory, open the .angular-cli.json file. This is the file that configures some properties about our project. Find the apps > styles property and add a file location of our Bootstrap CSS class:

ui目录,打开.angular-cli.json文件。这是配置关于我们项目的一些属性的文件。找到apps > styles属性,添加我们Bootstrap CSS类的文件位置。

"styles": [
    "styles.css",
    "../node_modules/bootstrap/dist/css/bootstrap.min.css"
],

This will instruct Angular to include Bootstrap in the compiled CSS file that’s built with the project.

这将指示Angular将Bootstrap包含在与项目一起构建的编译的CSS文件中。

3.5. Set the Build Output Directory

3.5.设置构建输出目录

Next, we need to tell Angular where to put the build files so that our spring boot app can serve them. Spring Boot can serve files from two locations in the resources folder:

接下来,我们需要告诉Angular将构建文件放在哪里,以便我们的Spring Boot应用能够为它们提供服务。Spring Boot可以从资源文件夹中的两个位置提供文件。

  • src/main/resources/static
  • src/main/resource/public

Since we’re already using the static folder to serve some resources for Eureka, and Angular deletes this folder each time a build is run, let’s build our Angular app into the public folder.

由于我们已经使用静态文件夹为Eureka提供一些资源,而Angular在每次运行构建时都会删除这个文件夹,所以让我们把Angular应用程序构建到公共文件夹中。

Open the .angular-cli.json file again and find the apps > outDir property. Update that string:

再次打开.angular-cli.json文件,找到apps > outDir属性。更新该字符串:

"outDir": "../../resources/static/home",

If the Angular project’s located in src/main/angular/ui, then it will build to the src/main/resources/public folder. If the app in another folder this string will need to be modified to set the location correctly.

如果 Angular 项目位于 src/main/angular/ui,那么它将构建到 src/main/resources/public 文件夹。如果应用程序在其他文件夹中,则需要修改这个字符串以正确设置位置。

3.6. Automate the Build With Maven

3.6.用Maven自动构建

Lastly, we will set up an automated build to run when we compile our code. This ant task will run the Angular CLI build task whenever “mvn compile” is run. Add this step to the gateway’s POM.xml to ensure that each time we compile we get the latest ui changes:

最后,我们将设置一个自动构建,在我们编译代码时运行。这个蚂蚁任务将在运行 “mvn compile “时运行Angular CLI构建任务。把这个步骤添加到网关的POM.xml中,以确保每次编译时都能得到最新的ui变化。

<plugin>
    <artifactId>maven-antrun-plugin</artifactId>
    <executions>
        <execution>
            <phase>generate-resources</phase>
            <configuration>
                <tasks>
                    <exec executable="cmd" osfamily="windows"
                      dir="${project.basedir}/src/main/angular/ui">
                        <arg value="/c"/>
                        <arg value="ng"/>
                        <arg value="build"/>
                    </exec>
                    <exec executable="/bin/sh" osfamily="mac"
                      dir="${project.basedir}/src/main/angular/ui">
                        <arg value="-c"/>
                        <arg value="ng build"/>
                    </exec>
                </tasks>
            </configuration>
            <goals>
                <goal>run</goal>
            </goals>
        </execution>
    </executions>
</plugin>

We should note that this set up does require that the Angular CLI be available on the classpath. Pushing this script to an environment that does not have that dependency will result in build failures.

我们应该注意到,这种设置确实需要Angular CLI在classpath上可用。如果把这个脚本推送到没有这个依赖关系的环境中,会导致构建失败。

Now let’s start building our Angular application!

现在,让我们开始构建我们的Angular应用程序吧!

4. Angular

4.角形

In this section of the tutorial, we build an authentication mechanism in our page. We use basic authentication and follow a simple flow to make it work.

在本节教程中,我们在我们的页面中建立一个认证机制。我们使用基本认证,并遵循一个简单的流程来使其运作。

Users have a login form where they can enter their username and password.

用户有一个登录表格,可以输入他们的用户名和密码。

Next, we use their credentials to create a base64 authentication token and request the “/me” endpoint. The endpoint returns a Principal object containing the roles of this user.

接下来,我们使用他们的凭证来创建一个base64认证令牌,并请求“/me”端点。该端点返回一个Principal对象,包含该用户的角色。

Lastly, we will store the credentials and the principal on the client to use in subsequent requests.

最后,我们将在客户端存储凭证和委托人,以便在后续请求中使用。

Let’s see how this’s done!

让我们看看这是怎么做的!

4.1. Template

4.1.模板

In the gateway project, navigate to src/main/angular/ui/src/app and open the app.component.html file. This’s the first template that Angular loads and will be where our users will land after logging in.

在网关项目中,导航到src/main/angular/ui/src/app并打开app.component.html文件。这是Angular加载的第一个模板,将是我们的用户在登录后登陆的地方。

In here, we’re going to add some code to display a navigation bar with a login form:

在这里,我们要添加一些代码来显示一个带有登录表单的导航条。

<nav class="navbar navbar-toggleable-md navbar-inverse fixed-top bg-inverse">
    <button class="navbar-toggler navbar-toggler-right" type="button" 
      data-toggle="collapse" data-target="#navbarCollapse" 
      aria-controls="navbarCollapse" aria-expanded="false" 
      aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
    </button>
    <a class="navbar-brand" href="#">Book Rater 
        <span *ngIf="principal.isAdmin()">Admin</span></a>
    <div class="collapse navbar-collapse" id="navbarCollapse">
    <ul class="navbar-nav mr-auto">
    </ul>
    <button *ngIf="principal.authenticated" type="button" 
      class="btn btn-link" (click)="onLogout()">Logout</button>
    </div>
</nav>

<div class="jumbotron">
    <div class="container">
        <h1>Book Rater App</h1>
        <p *ngIf="!principal.authenticated" class="lead">
        Anyone can view the books.
        </p>
        <p *ngIf="principal.authenticated && !principal.isAdmin()" class="lead">
        Users can view and create ratings</p>
        <p *ngIf="principal.isAdmin()"  class="lead">Admins can do anything!</p>
    </div>
</div>

This code sets up a navigation bar with Bootstrap classes. Embedded in the bar is an inline login form. Angular uses this markup to interact with JavaScript dynamically to render various parts of the page and control things like form submission.

这段代码用Bootstrap类设置了一个导航栏。嵌入栏中的是一个内联的登录表单。Angular使用这个标记与JavaScript进行动态交互,以呈现页面的各个部分,并控制表单提交等事项。

Statements like (ngSubmit)=”onLogin(f)” simply indicate that when the form is submitted call the method “onLogin(f)” and pass the form to that function. Within the jumbotron div, we have paragraph tags that will display dynamically depending on the state of our principal object.

(ngSubmit)=”onLogin(f)”这样的语句只是表明当表单被提交时,调用方法“onLogin(f)”并将表单传递给该函数。在jumbotron div中,我们有一些段落标签,它们将根据我们的主对象的状态动态地显示。

Next, let’s code up the Typescript file that will support this template.

接下来,让我们把支持这个模板的Typescript文件编码出来。

4.2. Typescript

4.2.类型脚本

From the same directory open the app.component.ts file. In this file we will add all the typescript properties and methods required to make our template function:

在同一目录下打开app.component.ts文件。在这个文件中,我们将添加所有需要的typescript属性和方法,以使我们的模板发挥作用。

import {Component} from "@angular/core";
import {Principal} from "./principal";
import {Response} from "@angular/http";
import {Book} from "./book";
import {HttpService} from "./http.service";

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})
export class AppComponent {
    selectedBook: Book = null;
    principal: Principal = new Principal(false, []);
    loginFailed: boolean = false;

    constructor(private httpService: HttpService){}

    ngOnInit(): void {
        this.httpService.me()
          .subscribe((response: Response) => {
              let principalJson = response.json();
              this.principal = new Principal(principalJson.authenticated,
              principalJson.authorities);
          }, (error) => {
              console.log(error);
        });
    }

    onLogout() {
        this.httpService.logout()
          .subscribe((response: Response) => {
              if (response.status === 200) {
                  this.loginFailed = false;
                  this.principal = new Principal(false, []);
                  window.location.replace(response.url);
              }
           }, (error) => {
               console.log(error);
       });
    }
}

This class hooks into the Angular life cycle method, ngOnInit(). In this method, we call the /me endpoint to get user’s current role and state. This determine’s what the user’s sees on the main page. This method will be fired whenever this component’s created which is a great time to be checking the user’s properties for permissions in our app.

这个类钩住了Angular生命周期的方法,ngOnInit()。在这个方法中,我们调用/me端点来获取用户的当前角色和状态。这决定了用户在主页面上看到的内容。每当这个组件被创建时,这个方法就会被触发,这是一个很好的时机来检查用户的属性,以获得我们应用程序中的权限。

We also have an onLogout() method that logs our user out and restores the state of this page to its original settings.

我们还有一个onLogout()方法,将我们的用户注销,并将这个页面的状态恢复到原来的设置。

There’s some magic going on here though. The httpService property that’s declared in the constructor. Angular is injecting this property into our class at runtime. Angular manages singleton instances of service classes and injects them using constructor injection, just like Spring!

但这里有一些神奇的事情发生。在构造函数中声明的httpService属性。Angular在运行时将这个属性注入我们的类中。Angular管理着服务类的单子实例,并使用构造函数注入,就像Spring一样!Angular也是如此。

Next, we need to define the HttpService class.

接下来,我们需要定义HttpService类。

4.3. HttpService

4.3.HttpService

In the same directory create a file named “http.service.ts”. In this file add this code to support the login and logout methods:

在同一目录下创建一个名为“http.service.ts”的文件。在这个文件中添加这段代码以支持登录和注销方法。

import {Injectable} from "@angular/core";
import {Observable} from "rxjs";
import {Response, Http, Headers, RequestOptions} from "@angular/http";
import {Book} from "./book";
import {Rating} from "./rating";

@Injectable()
export class HttpService {

    constructor(private http: Http) { }

    me(): Observable<Response> {
        return this.http.get("/me", this.makeOptions())
    }

    logout(): Observable<Response> {
        return this.http.post("/logout", '', this.makeOptions())
    }

    private makeOptions(): RequestOptions {
        let headers = new Headers({'Content-Type': 'application/json'});
        return new RequestOptions({headers: headers});
    }
}

In this class, we’re injecting another dependency using Angular’s DI construct. This time it’s the Http class. This class handles all HTTP communication and is provided to us by the framework.

在这个类中,我们要使用Angular的DI结构注入另一个依赖关系。这一次是Http类。这个类处理所有的HTTP通信,是由框架提供给我们的。

These methods each perform an HTTP request using angular’s HTTP library. Each request also specifies a content type in the headers.

这些方法各自使用angular的HTTP库执行一个HTTP请求。每个请求都在头文件中指定了一个内容类型。

Now we need to do one more thing to get the HttpService registered in the dependency injection system. Open the app.module.ts file and find the providers property. Add the HttpService to that array. The result should look like this:

现在我们还需要做一件事,让HttpService在依赖注入系统中注册。打开app.module.ts文件,找到providers属性。将HttpService添加到该数组中。结果应该是这样的。

providers: [HttpService],

4.4. Add Principal

4.4.添加校长

Next, let’s add our Principal DTO object in our Typescript code. In the same directory add a file called “principal.ts” and add this code:

接下来,让我们在Typescript代码中加入我们的Principal DTO对象。在同一目录下添加一个名为 “principal.ts “的文件,并添加这段代码。

export class Principal {
    public authenticated: boolean;
    public authorities: Authority[] = [];
    public credentials: any;

    constructor(authenticated: boolean, authorities: any[], credentials: any) {
        this.authenticated = authenticated;
        authorities.map(
          auth => this.authorities.push(new Authority(auth.authority)))
        this.credentials = credentials;
  }

    isAdmin() {
        return this.authorities.some(
          (auth: Authority) => auth.authority.indexOf('ADMIN') > -1)
    }
}

export class Authority {
    public authority: String;

    constructor(authority: String) {
        this.authority = authority;
    }
}

We added the Principal class and an Authority class. These are two DTO classes, much like POJOs in a Spring app. Because of that, we do not need to register these classes with the DI system in angular.

我们添加了Principal类和Authority类。这是两个DTO类,很像Spring应用中的POJOs。正因为如此,我们不需要在angular的DI系统中注册这些类。

Next, let’s configure a redirect rule to redirect unknown requests to the root of our application.

接下来,让我们配置一个重定向规则,将未知的请求重定向到我们应用程序的根。

4.5. 404 Handling

4.5.404的处理

Let’s navigate back into the Java code for the gateway service. In the where GatewayApplication class resides add a new class called ErrorPageConfig:

让我们回到网关服务的Java代码中去。在GatewayApplication类所在的地方添加一个新的类,称为ErrorPageConfig

@Component
public class ErrorPageConfig implements ErrorPageRegistrar {
 
    @Override
    public void registerErrorPages(ErrorPageRegistry registry) {
        registry.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND,
          "/home/index.html"));
    }

}

This class will identify any 404 response and redirect the user to “/home/index.html”. In a single page app, this’s how we handle all traffic not going to a dedicated resource since the client should be handling all navigable routes.

这个类将识别任何404响应并将用户重定向到“/home/index.html”。在一个单页应用程序中,这就是我们处理所有未进入专用资源的流量的方式,因为客户端应该处理所有可导航的路线。

Now we’re ready to fire this app up and see what we built!

现在我们已经准备好启动这个应用程序,看看我们建立了什么!

4.6. Build and View

4.6.建立和查看

Now run “mvn compile” from the gateway folder. This will compile our java source and build the Angular app to the public folder. Let’s start the other cloud applications: config, discovery, and zipkin. Then run the gateway project. When the service starts, navigate to http://localhost:8080 to see our app. We should see something like this:

现在从网关文件夹运行”mvn compile“。这将编译我们的java源代码,并将Angular应用程序构建到公共文件夹中。让我们启动其他的云应用程序。config, discovery, 和zipkin。然后运行网关项目。当服务启动时,导航到http://localhost:8080,看到我们的应用程序。我们应该看到类似这样的东西。

Book Rater

Next, let’s follow the link to the login page:

接下来,让我们按照链接进入登录页面。

ng2 login 1

Log in using the user/password credentials. Click “Login”, and we should be redirected to /home/index.html where our single page app loads.

使用用户/密码凭证登录。点击 “登录”,我们应该被重定向到/home/index.html,在那里我们的单页应用程序被加载。

Book Rater App

It looks like our jumbotron is indicating we’re logged in as a user! Now log out by clicking the link the upper right corner and log in using the admin/admin credentials this time.

看起来我们的jumbotron显示我们已经以用户身份登录了!现在点击右上角的链接退出,这次用admin/admin证书登录。

Book Rate App

Looks good! Now we’re logged in as an admin.

看起来不错!现在我们以管理员身份登录了。

5. Conclusion

5.结论

In this article, we have seen how easy it’s to integrate a single page app into our cloud system. We took a modern framework and integrated a working security configuration into our application.

在这篇文章中,我们已经看到将一个单页应用程序整合到我们的云系统中是多么容易。我们采用了一个现代框架,并在我们的应用程序中集成了一个工作的安全配置。

Using these examples, try to write some code to make a call to the book-service or rating-service. Since we now have examples of making HTTP calls and wiring data to the templates, this should be relatively easy.

使用这些例子,试着写一些代码,对book-servicerating-service进行调用。由于我们现在有了进行HTTP调用和将数据连接到模板的例子,这应该是比较容易的。

If you would like to see how the rest of the site’s built as always, you can find the source code over on Github.

如果你想看看网站的其他部分是如何一如既往地构建的,你可以在Github上找到源代码over