首页>>新闻资讯

使用 Angular 5.0 和 Spring Boot 2.0 构建一个基本的 CRUD 应用

发布时间:2018-04-24 点击:89 次

在所有 Web 开发的框架中,Anglar 和 Spring Boot 可以说是两个最流行的了。那么我们不妨看看如何在你的应用中使用它们。

现在技术进展得很快,跟上最新的趋势以及你喜欢的项目的最新发布版本是很有挑战性的。Anglar 和 Spring Boot 是我最喜欢的两个项目。因此我想我应该给你们写个指南,让你清楚如何使用它们最新、最完整的版本构建一个基本的应用程序。

对于 Spring Boot,在 2.0 版本中最重要的变化是它的全新 Web 框架:Spring WebFlux。在 Angular 5.0 中我们也在表格中有了一个新的 HttpClient。这个类代替了 Http,并且使用起来更简单一些,使用更少的样板(boilerplate)代码即可。但今天,我并不打算去探索 Spring WebFlux,因为在我们能够支持 Okta Spring Boot Starte 之前我们还有一些工作要做

好消息是我们的 Angular SDK 能够很好地与 Angular 5 兼容,我将在这篇博文中展示如何使用它。说到 Angular,你知道在 Stack Overflow 上,Angular 是最引人注目的问题之一吗?你可能认为这意味着很多人都对 Angular 有相关的疑问。我更偏向于认为是使用人数庞大,开发者在使用新技术时经常有疑问(所导致)。这是一个健康的社区的明确标志。对于垂死的技术你很少会在 Stack Overflow 上看到很多的问题。

本文将讲解如何构建一个简单的 CRUD 应用来显示一个酷的汽车的列表。它允许你去编辑这个列表,并且它将显示一个与汽车名称相匹配的源于 GIPHY 的 gif 动画。你也会学习到如何使用 Okta’s Spring Boot starter 和 Angular SDK 来保护你的应用程序。

本教程中,你将会需要在电脑中安装 Java 8 和 Node.js 8

使用 Spring Boot 2.0 创建 API

在一开始使用 Spring Boot 2.0 时,你可以使用最新的里程碑版本。访问 start.spring.io,然后使用 Java、Spring Boot 2.0.0 M6 创建一个新项目,并选择创建一个简单的 API:JPA,H2,Rest Repositories,Lombok 和 Web。在这个例子中,我已经添加了Actuator(执行器),它是 Spring Boot 中一个非常酷的功能

创建一个目录来存放你的服务器和客户端应用程序。我的目录命名为 okta-spring-boot-2-angular-5-example,你可以命名为你喜欢的任意名称。如果你只想看该应用程序运行而不是编写代码,那么你可以在 GitHub 上查看示例,或使用以下命令进行本地克隆和运行。

git clone https://github.com/oktadeveloper/okta-spring-boot-2-angular-5-example.gitcd okta-spring-boot-2-angular-5-example/client && npm install && ng serve &cd ../server && ./mvnw spring-boot:run

从 start.spring.io 下载了 demo.zip 后,将其解压并将 demo 文件复制到应用程序存放目录。将 demo 重命名为 server。用你喜欢的 IDE 打开项目,在 src/main/java/com/okta/developer/demo 目录下创建一个 Car.java 文件。 你可以使用 Lombok 注解来减少样板代码。

package com.okta.developer.demo;import lombok.*;import javax.persistence.Id;import javax.persistence.GeneratedValue;import javax.persistence.Entity;@Entity@Getter @Setter@NoArgsConstructor@ToString @EqualsAndHashCodepublic class Car {
    @Id @GeneratedValue
    private Long id;    private @NonNull String name;
}

创建 CarRepository 类以在 Car 实体上执行 CRUD(创建,读取,更新和删除)。

package com.okta.developer.demo;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

@RepositoryRestResourceinterface CarRepository extends JpaRepository<Car, Long> {}



将 ApplicationRunner bean 添加到 DemoApplication 类(在 src/main/java/com/okta/developer/demo/DemoApplication.java 中),并使用它添加一些默认数据到数据库。

package com.okta.developer.demo;import org.springframework.boot.ApplicationRunner;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.annotation.Bean;import java.util.stream.Stream;@SpringBootApplicationpublic class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }    @Bean
    ApplicationRunner init(CarRepository repository) {        return args -> {
            Stream.of("Ferrari", "Jaguar", "Porsche", "Lamborghini", "Bugatti",                      "AMC Gremlin", "Triumph Stag", "Ford Pinto", "Yugo GV").forEach(name -> {
                Car car = new Car();
                car.setName(name);
                repository.save(car);
            });
            repository.findAll().forEach(System.out::println);
        };
    }
}

如果你在添加此代码后启动你的应用程序(使用 ./mvnw spring-boot:run),则会在启动时看到汽车列表显示在控制台中。

Car(id=1, name=Ferrari)
Car(id=2, name=Jaguar)
Car(id=3, name=Porsche)
Car(id=4, name=Lamborghini)
Car(id=5, name=Bugatti)
Car(id=6, name=AMC Gremlin)
Car(id=7, name=Triumph Stag)
Car(id=8, name=Ford Pinto)
Car(id=9, name=Yugo GV)

添加一个 CoolCarController 类(在 src/main/java/com/okta/developer/demo/CoolCarController.java 中),该类用于返回一个汽车列表,并在 Angular 客户端中显示。

package com.okta.developer.demo;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import java.util.Collection;import java.util.stream.Collectors;@RestControllerclass CoolCarController {    private CarRepository repository;    public CoolCarController(CarRepository repository) {        this.repository = repository;
    }    @GetMapping("/cool-cars")    public Collection<Car> coolCars() {        return repository.findAll().stream()
                .filter(this::isCool)
                .collect(Collectors.toList());
    }    private boolean isCool(Car car) {        return !car.getName().equals("AMC Gremlin") &&
                !car.getName().equals("Triumph Stag") &&
                !car.getName().equals("Ford Pinto") &&
                !car.getName().equals("Yugo GV");
    }
}

如果你重启服务器应用程序,并使用浏览器或命令行客户端键入 localhost:8080/cool-cars,则应该会看到过滤后的汽车列表。

http localhost:8080/cool-cars
HTTP/1.1 200Content-Type: application/json;charset=UTF-8Date: Sun, 19 Nov 2017 21:29:22 GMT
Transfer-Encoding: chunked

[
    {
        "id": 1,
        "name": "Ferrari"    },
    {
        "id": 2,
        "name": "Jaguar"    },
    {
        "id": 3,
        "name": "Porsche"    },
    {
        "id": 4,
        "name": "Lamborghini"    },
    {
        "id": 5,
        "name": "Bugatti"    }
]

使用 Angular CLI 创建一个客户端

Angular CLI 是一个命令行工具,可为你生成一个 Angular 项目。它不仅可以创建新项目,还可以生成代码。这是一个方便的工具,因为它还提供了命令用来构建和优化生产环境中使用的项目。它使用 covers 下的 webpack 用于构建。如果你想了解更多关于 webpack 的信息,推荐这个网站 —— webpack.academy

你可以通过 https://cli.angular.io 了解 Angular CLI 的基础知识。

安装最新版本的 Angular CLI,版本号是 1.5.2。

npm install -g @angular/cli@1.5.2

在你创建的伞形目录中新建一个项目。我的名字命名为 okta-spring-boot-2-angular-5-example。

ng new client

客户端创建后,导航到其目录并安装 Angular Material。

cd clientnpm install --save @angular/material @angular/cdk

你将使用 Angular Material 的组件来使 UI 看起来更好,特别是在手机上。安装 Angular 的动画库,因为其中的 Angular Material 组件有时会用到。

npm install --save @angular/animations

如果你想了解有关 Angular Material 的更多信息,请参阅 https://material.angular.io。它有各种组件的大量文档以及如何使用它们。

构建一个汽车列表页面

使用 Angular CLI 生成可与 Cool Cars API 交互的汽车服务。

ng g s car

将生成的文件移动到 client/src/app/shared/car 目录。

mkdir -p src/app/shared/car
mv src/app/car.service.* src/app/shared/car/.

更新 car.service.ts 中的代码以从服务器获取汽车列表。

import { Injectable } from '@angular/core';import { HttpClient } from '@angular/common/http';import { Observable } from 'rxjs/Observable';@Injectable()export class CarService {

  constructor(private http: HttpClient) {
  }

  getAll(): Observable<any> {    return this.http.get('//localhost:8080/cool-cars');
  }
}

在 src/app/app.module.ts 中将此服务作为提供者添加,并导入 HttpClientModule。

import { CarService } from './shared/car/car.service';import { HttpClientModule } from '@angular/common/http';@NgModule({
  declarations: [
    AppComponent,
    CarListComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule
  ],
  providers: [CarService],
  bootstrap: [AppComponent]
})

生成 car-list 组件以显示汽车列表。

ng g c car-list

更新 client/src/app/car-list/car-list.component.ts 以使用 CarService 获取列表并在本地 cars 变量中设置值。

import { CarService } from '../shared/car/car.service';

export class CarListComponent implements OnInit {
  cars: Array<any>;

  constructor(private carService: CarService) { }

  ngOnInit() {
    this.carService.getAll().subscribe(data => {
      this.cars = data;
    });
  }
}

更新 client/src/app/car-list/car-list.component.html 以显示汽车列表。

<h2>Car List</h2><div *ngFor="let car of cars">
  {{car.name}}</div>

更新 client/src/app/app.component.html 以拥有 app-car-list 元素。

<div style="text-align:center">
  <h1>Welcome to {{title}}!</h1></div><app-car-list></app-car-list>

使用 ng serve 启动客户端应用程序。打开你喜欢的浏览器访问 http://localhost:4200。不过你目前还不会看到汽车列表,如果你打开开发者控制台,就会看到原因。

发生此错误是因为你尚未在服务器上启用 CORS 服务(跨源资源共享)。

在服务器上启用 CORS

要在服务器上启用 CORS,请将 @CrossOrigin 注释添加到 CoolCarController(在 server/src/main/java/com/okta/developer/demo/CoolCarController.java 中)。

import org.springframework.web.bind.annotation.CrossOrigin;
...@GetMapping("/cool-cars")@CrossOrigin(origins = "http://localhost:4200")public Collection<Car> coolCars() {    return repository.findAll().stream()
            .filter(this::isCool)
            .collect(Collectors.toList());
}

另外,将它添加到 CarRepository 中,以便在添加/删除/编辑时可以与其端点进行通信。

@RepositoryRestResource
@CrossOrigin(origins = "http://localhost:4200")interface CarRepository extends JpaRepository<Car, Long> {}

重新启动服务器,刷新客户端,然后就可以在浏览器中看到汽车列表。

添加 Angular Material

你已经安装了 Angular Material,要使用它的组件,只需导入它们即可。打开 client/src/app/app.module.ts,并为动画,Material 的工具栏,按钮,输入,列表和卡片布局添加导入。

import { MatButtonModule, MatCardModule, MatInputModule, MatListModule, MatToolbarModule } from '@angular/material';import { BrowserAnimationsModule } from '@angular/platform-browser/animations';@NgModule({
  ...
  imports: [
    BrowserModule,
    HttpClientModule,
    BrowserAnimationsModule,
    MatButtonModule,
    MatCardModule,
    MatInputModule,
    MatListModule,
    MatToolbarModule,
  ],
  ...
})

更新 client/src/app/app.component.html 以使用工具栏组件。

<mat-toolbar color="primary">
  <span>Welcome to {{title}}!</span></mat-toolbar><app-car-list></app-car-list>

更新 client/src/app/car-list/car-list.component.html 以使用卡片布局和列表组件。

<mat-card>
  <mat-card-header>Car List</mat-card-header>
  <mat-card-content>
    <mat-list>
      <mat-list-item *ngFor="let car of cars">
        <img mat-list-avatar src="{{car.giphyUrl}}" alt="{{car.name}}">
        <h3 mat-line>{{car.name}}</h3>
      </mat-list-item>
    </mat-list>
  </mat-card-content></mat-card>

修改 client/src/styles.csss 来指定主题和图标。

@import "~@angular/material/prebuilt-themes/pink-bluegrey.css";@import '~https://fonts.googleapis.com/icon?family=Material+Icons';body { margin: 0; font-family: Roboto, sans-serif;}

如果你用 ng serve 运行你的客户端并访问 http://localhost:4200,你会看到汽车列表,但没有与它们关联的图像。

使用 Giphy 添加动画 GIFs

要将 giphyUrl 属性添加到汽车,请创建 client/src/app/shared/giphy/giphy.service.ts 并用下面的代码填充它。

import { Injectable } from '@angular/core';import { HttpClient } from '@angular/common/http';import 'rxjs/add/operator/map';@Injectable()export class GiphyService {

  // Public beta key: https://github.com/Giphy/GiphyAPI#public-beta-key
  giphyApi = '//api.giphy.com/v1/gifs/search?api_key=dc6zaTOxFJmzC&limit=1&q=';

  constructor(public http: HttpClient) {
  }

  get(searchTerm) {    const apiLink = this.giphyApi + searchTerm;    return this.http.get(apiLink).map((response: any) => {      if (response.data.length > 0) {        return response.data[0].images.original.url;
      } else {        return 'https://media.giphy.com/media/YaOxRsmrv9IeA/giphy.gif'; // dancing cat for 404
      }
    });
  }
}

在 client/src/app/app.module.ts 中添加 GiphyService 作为提供者。

import { GiphyService } from './shared/giphy/giphy.service';@NgModule({
  ...
  providers: [CarService, GiphyService],
  bootstrap: [AppComponent]
})

更新 client/src/app/car-list/car-list.component.ts 中的代码以设置每辆车的 giphyUrl 属性。

import { GiphyService } from '../shared/giphy/giphy.service';

export class CarListComponent implements OnInit {
  cars: Array<any>;

  constructor(private carService: CarService, private giphyService: GiphyService) { }

  ngOnInit() {
    this.carService.getAll().subscribe(data => {
      this.cars = data;      for (const car of this.cars) {
        this.giphyService.get(car.name).subscribe(url => car.giphyUrl = url);
      }
    });
  }
}

现在你的浏览器应该会显示汽车名称列表,以及旁边的头像图片。

添加编辑功能

有一个汽车名称和图像的列表显得十分美观,但如果能和它进行交互就更好了!要添加编辑功能,首先生成一个 car-edit 组件。

ng g c car-edit

更新 client/src/app/shared/car/car.service.ts 以拥有添加、删除和更新汽车的方法。这些方法与 CarRepository 和 @RepositoryRestResource 注释提供的端点进行交互。

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class CarService {  public API = '//localhost:8080';  public CAR_API = this.API + '/cars';

  constructor(private http: HttpClient) {
  }

  getAll(): Observable<any> {    return this.http.get(this.API + '/cool-cars');
  }  get(id: string) {    return this.http.get(this.CAR_API + '/' + id);
  }

  save(car: any): Observable<any> {    let result: Observable<Object>;    if (car['href']) {
      result = this.http.put(car.href, car);
    } else {
      result = this.http.post(this.CAR_API, car);
    }    return result;
  }

  remove(href: string) {    return this.http.delete(href);
  }
}

在 client/src/app/car-list/car-list.component.html 中,添加一个到编辑组件的链接。另外,在底部添加一个按钮来添加一辆新车。

<mat-card>
  <mat-card-header>Car List</mat-card-header>
  <mat-card-content>
    <mat-list>
      <mat-list-item *ngFor="let car of cars">
        <img mat-list-avatar src="{{car.giphyUrl}}" alt="{{car.name}}">
        <h3 mat-line>
          <a mat-button [routerLink]="['/car-edit', car.id]">{{car.name}}</a>
        </h3>
      </mat-list-item>
    </mat-list>
  </mat-card-content>

  <button mat-fab color="primary" [routerLink]="['/car-add']">Add</button></mat-card>

在 client/src/app/app.module.ts 中,添加路由并导入 FormsModule。

import { FormsModule } from '@angular/forms';import { RouterModule, Routes } from '@angular/router';

const appRoutes: Routes = [
  { path: '', redirectTo: '/car-list', pathMatch: 'full' },
  {
    path: 'car-list',
    component: CarListComponent
  },
  {
    path: 'car-add',
    component: CarEditComponent
  },
  {
    path: 'car-edit/:id',
    component: CarEditComponent
  }
];@NgModule({
  ...
  imports: [
    ...
    FormsModule,
    RouterModule.forRoot(appRoutes)
  ],
  ...
})

修改 client/src/app/car-edit/car-edit.component.ts 以从 URL 上传递的 id 获取汽车的信息,并添加保存和删除的方法。

import { Component, OnDestroy, OnInit } from '@angular/core';import { Subscription } from 'rxjs/Subscription';import { ActivatedRoute, Router } from '@angular/router';import { CarService } from '../shared/car/car.service';import { GiphyService } from '../shared/giphy/giphy.service';import { NgForm } from '@angular/forms';@Component({  selector: 'app-car-edit',  templateUrl: './car-edit.component.html',  styleUrls: ['./car-edit.component.css']
})export class CarEditComponent implements OnInit, OnDestroy {
  car: any = {};  sub: Subscription;

  constructor(private route: ActivatedRoute,
              private router: Router,
              private carService: CarService,
              private giphyService: GiphyService) {
  }

  ngOnInit() {    this.sub = this.route.params.subscribe(params => {      const id = params['id'];      if (id) {        this.carService.get(id).subscribe((car: any) => {          if (car) {            this.car = car;            this.car.href = car._links.self.href;            this.giphyService.get(car.name).subscribe(url => car.giphyUrl = url);
          } else {            console.log(`Car with id '${id}' not found, returning to list`);            this.gotoList();
          }
        });
      }
    });
  }  ngOnDestroy() {    this.sub.unsubscribe();
  }  gotoList() {    this.router.navigate(['/car-list']);
  }  save(form: NgForm) {    this.carService.save(form).subscribe(result => {      this.gotoList();
    }, error => console.error(error))
  }  remove(href) {    this.carService.remove(href).subscribe(result => {      this.gotoList();
    }, error => console.error(error))
  }
}

更新 client/src/app/car-edit/car-edit.component.html 中的 HTML 以使用汽车名称的表格,以及显示来自 Giphy 的图像。

<mat-card>
  <form #carForm="ngForm" (ngSubmit)="save(carForm.value)">
    <mat-card-header>
      <mat-card-title><h2>{{car.name ? 'Edit' : 'Add'}} Car</h2></mat-card-title>
    </mat-card-header>
    <mat-card-content>
      <input type="hidden" name="href" [(ngModel)]="car.href">
      <mat-form-field>
        <input matInput placeholder="Car Name" [(ngModel)]="car.name"
               required name="name" #name>
      </mat-form-field>
    </mat-card-content>
    <mat-card-actions>
      <button mat-raised-button color="primary" type="submit"
              [disabled]="!carForm.form.valid">Save</button>
      <button mat-raised-button color="secondary" (click)="remove(car.href)"
              *ngIf="car.href" type="button">Delete</button>
      <a mat-button routerLink="/car-list">Cancel</a>
    </mat-card-actions>
    <mat-card-footer>
      <div class="giphy">
        <img src="{{car.giphyUrl}}" alt="{{car.name}}">
      </div>
    </mat-card-footer>
  </form></mat-card>

将下面的 CSS 添加到 client/src/app/car-edit/car-edit.component.css 中,以在图片周围添加一些填充。

.giphy {  margin: 10px;}

修改 client/src/app/app.component.html 并用 <router-outlet></router-outlet> 替换 <app-car-list></app-car-list>。这种更改是必要的,或者组件之间的路由不起作用。

<mat-toolbar color="primary">
  <span>Welcome to !</span></mat-toolbar><router-outlet></router-outlet>

完成所有这些更改后,你应该可以添加、编辑或删除任何汽车。 以下是包含添加按钮的显示列表的屏幕截图。