Перейти к основному содержимому

Создание веб-приложения с Spring Boot и Angular

· 15 мин. чтения

Задача: Наибольшая подстрока палиндром

Для заданной строки s, верните наибольшую подстроку палиндром входящую в s. Подстрока — это непрерывная непустая последовательность символов внутри строки. Стока является палиндромом, если она читается одинаково в обоих направлениях...

ANDROMEDA 42

1. Обзор

Spring Boot и Angular образуют мощный тандем, который отлично подходит для разработки веб-приложений с минимальными затратами.

В этом руководстве мы будем использовать Spring Boot для реализации серверной части RESTful и Angular для создания интерфейса на основе JavaScript.

2. Приложение Spring Boot

Функциональность нашего демонстрационного веб-приложения будет действительно довольно простой. Он будет сужен до извлечения и отображения списка сущностей JPA из базы данных H2 в памяти и сохранения новых с помощью простой HTML-формы.

2.1. Зависимости Maven

Вот зависимости нашего проекта Spring Boot:

<dependency> 
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>

Обратите внимание, что мы включили spring-boot-starter-web , потому что мы будем использовать его для создания службы REST, и spring-boot-starter-jpa для реализации уровня сохраняемости.

Версия базы данных H2 также управляется родителем Spring Boot.

2.2. Класс сущности JPA

Чтобы быстро создать прототип слоя предметной области нашего приложения, давайте определим простой класс сущности JPA, который будет отвечать за моделирование пользователей:

@Entity
public class User {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private final String name;
private final String email;

// standard constructors / setters / getters / toString
}

2.3. Интерфейс пользовательского репозитория

Поскольку нам понадобятся базовые функции CRUD для сущностей User , мы также должны определить интерфейс UserRepository :

@Repository
public interface UserRepository extends CrudRepository<User, Long>{}

2.4. Контроллер REST

Теперь давайте реализуем REST API. В данном случае это просто простой REST-контроллер:

@RestController
@CrossOrigin(origins = "http://localhost:4200")
public class UserController {

// standard constructors

private final UserRepository userRepository;

@GetMapping("/users")
public List<User> getUsers() {
return (List<User>) userRepository.findAll();
}

@PostMapping("/users")
void addUser(@RequestBody User user) {
userRepository.save(user);
}
}

В определении класса UserController нет ничего сложного .

Конечно, здесь стоит отметить деталь реализации — использование аннотации @CrossOrigin . Как следует из названия, аннотация включает общий доступ к ресурсам между источниками (CORS) на сервере.

Этот шаг не всегда необходим, но поскольку мы развертываем наш интерфейс Angular на http://localhost:4200 , а наш сервер Boot на http://localhost:8080 , в противном случае браузер будет отклонять запросы от одного к другому. .

Что касается методов контроллера, getUser() извлекает все сущности пользователя из базы данных. Точно так же метод addUser() сохраняет новый объект в базе данных, который передается в теле запроса .

Для простоты мы намеренно исключили реализацию контроллера, запускающую проверку Spring Boot перед сохранением объекта. Однако в производственной среде мы не можем доверять только пользовательскому вводу, поэтому проверка на стороне сервера должна быть обязательной функцией.

2.5. Начальная загрузка приложения Spring Boot

Наконец, давайте создадим стандартный класс начальной загрузки Spring Boot и заполним базу данных несколькими объектами User :

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

@Bean
CommandLineRunner init(UserRepository userRepository) {
return args -> {
Stream.of("John", "Julie", "Jennifer", "Helen", "Rachel").forEach(name -> {
User user = new User(name, name.toLowerCase() + "@domain.com");
userRepository.save(user);
});
userRepository.findAll().forEach(System.out::println);
};
}
}

Теперь запустим приложение. Как и ожидалось, мы должны увидеть список сущностей пользователя , выводимых на консоль при запуске:

User{id=1, name=John, email=john@domain.com}
User{id=2, name=Julie, email=julie@domain.com}
User{id=3, name=Jennifer, email=jennifer@domain.com}
User{id=4, name=Helen, email=helen@domain.com}
User{id=5, name=Rachel, email=rachel@domain.com}

3. Угловое приложение

Теперь, когда наше демонстрационное приложение Spring Boot запущено и работает, мы можем создать простое приложение Angular, способное использовать API-интерфейс контроллера REST.

3.1. Установка углового интерфейса командной строки

Мы будем использовать Angular CLI , мощную утилиту командной строки, для создания нашего приложения Angular.

Angular CLI — чрезвычайно ценный инструмент, поскольку он позволяет нам создавать весь проект Angular с нуля, создавая компоненты, сервисы, классы и интерфейсы всего несколькими командами .

После того, как мы установили npm (диспетчер пакетов узлов), мы откроем командную консоль и введем команду:

npm install -g @angular/cli@1.7.4

Вот и все. Приведенная выше команда установит последнюю версию Angular CLI.

3.2. Строительные леса проекта с помощью Angular CLI

Мы можем сгенерировать нашу структуру приложения Angular с нуля, но, честно говоря, это подверженная ошибкам и трудоемкая задача, которой мы должны избегать во всех случаях.

Вместо этого мы позволим Angular CLI сделать всю тяжелую работу за нас. Итак, мы можем открыть командную консоль, затем перейти в папку, в которой мы хотим создать наше приложение, и ввести команду:

ng new angularclient

Новая команда сгенерирует всю структуру приложения в каталоге angularclient . ``

3.3. Точка входа приложения Angular

Если мы заглянем в папку angularclient , то увидим, что Angular CLI фактически создал для нас целый проект.

Файлы приложений Angular используют TypeScript , типизированный расширенный набор JavaScript, который компилируется в простой JavaScript. Однако точкой входа любого приложения Angular является старый добрый файл index.html .

Давайте отредактируем этот файл:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Spring Boot - Angular Application</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
crossorigin="anonymous">
</head>
<body>
<app-root></app-root>
</body>
</html>

Как мы видим выше, мы включили Bootstrap 4 , чтобы мы могли придать нашим компонентам пользовательского интерфейса приложения более причудливый вид. Конечно, можно подобрать другой набор пользовательского интерфейса из множества доступных.

Обратите внимание на настраиваемые теги <app-root></app-root> внутри раздела <body> . На первый взгляд они выглядят довольно странно, так как <app-root> не является стандартным элементом HTML 5.

Мы сохраним их там, так как <app-root> — это корневой селектор, который Angular использует для рендеринга корневого компонента приложения .

3.4. Корневой компонент app.component.ts

Чтобы лучше понять, как Angular привязывает HTML-шаблон к компоненту, давайте перейдем в каталог src/app и отредактируем TypeScript- файл app.component.ts , корневой компонент:

import { Component } from '@angular/core';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {

title: string;

constructor() {
this.title = 'Spring Boot - Angular Application';
}
}

По понятным причинам мы не будем углубляться в изучение TypeScript. Тем не менее, отметим, что файл определяет класс AppComponent , который объявляет заголовок поля строкового типа (в нижнем регистре). Определенно, это типизированный JavaScript.

Кроме того, конструктор инициализирует поле строковым значением, что очень похоже на то, что мы делаем в Java.

Наиболее важной частью является маркер метаданных @Component или декоратор , который определяет три элемента:

  1. selector — селектор HTML, используемый для привязки компонента к файлу шаблона HTML.
  2. templateUrl — файл шаблона HTML, связанный с компонентом.
  3. styleUrls — один или несколько файлов CSS, связанных с компонентом.

Как и ожидалось, мы можем использовать файлы app.component.html и app.component.css для определения шаблона HTML и стилей CSS корневого компонента.

Наконец, элемент selector привязывает весь компонент к селектору <app-root> , включенному в файл index.html .

3.5. Файл app.component.html _

Поскольку файл app.component.html позволяет нам определить HTML-шаблон корневого компонента, класс AppComponent , мы будем использовать его для создания базовой панели навигации с двумя кнопками.

Если мы нажмем первую кнопку, Angular отобразит таблицу, содержащую список сущностей пользователя , хранящихся в базе данных. Точно так же, если мы нажмем второй, он отобразит HTML-форму, которую мы можем использовать для добавления новых объектов в базу данных:

<div class="container">
<div class="row">
<div class="col-md-12">
<div class="card bg-dark my-5">
<div class="card-body">
<h2 class="card-title text-center text-white py-3">{{ title }}</h2>
<ul class="text-center list-inline py-3">
<li class="list-inline-item">
<a routerLink="/users" class="btn btn-info">List Users</a>
</li>
<li class="list-inline-item">
<a routerLink="/adduser" class="btn btn-info">Add User</a>
</li>
</ul>
</div>
</div>
<router-outlet></router-outlet>
</div>
</div>
</div>

Большая часть файла представляет собой стандартный HTML, с некоторыми оговорками, на которые стоит обратить внимание.

Первое — это выражение {{ title }} . Двойные фигурные скобки {{имя-переменной}} — это заполнитель, который Angular использует для выполнения интерполяции переменных .

Не будем забывать, что класс AppComponent инициализировал поле title значением Spring Boot — Angular Application . Таким образом, Angular будет отображать значение этого поля в шаблоне. Точно так же изменение значения в конструкторе будет отражено в шаблоне.

Второе, на что следует обратить внимание, это атрибут routerLink .

Angular использует этот атрибут для маршрутизации запросов через свой модуль маршрутизации (подробнее об этом позже). На данный момент достаточно знать, что модуль будет отправлять запрос к пути /users к определенному компоненту и запрос к /adduser к другому компоненту.

В каждом случае HTML-шаблон, связанный с соответствующим компонентом, будет отображаться в заполнителе <router-outlet></router-outlet> .

3.6. Пользовательский класс _

Поскольку наше приложение Angular будет извлекать и сохранять сущности пользователя в базе данных, давайте реализуем простую модель предметной области с помощью TypeScript.

Давайте откроем консоль терминала и создадим каталог модели :

ng generate class user

Angular CLI сгенерирует пустой класс User , поэтому давайте заполним его несколькими полями:

export class User {
id: string;
name: string;
email: string;
}

3.7. Служба UserService _

Теперь, когда наш клиентский доменный класс User уже установлен, мы можем реализовать класс службы, который выполняет запросы GET и POST к конечной точке http://localhost:8080/users .

Это позволит нам инкапсулировать доступ к REST-контроллеру в одном классе, который мы сможем повторно использовать во всем приложении .

Давайте откроем консольный терминал, затем создадим служебный каталог и в этом каталоге выполним следующую команду:

ng generate service user-service

Теперь давайте откроем файл user.service.ts , который только что создал Angular CLI, и проведем его рефакторинг:

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { User } from '../model/user';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class UserService {

private usersUrl: string;

constructor(private http: HttpClient) {
this.usersUrl = 'http://localhost:8080/users';
}

public findAll(): Observable<User[]> {
return this.http.get<User[]>(this.usersUrl);
}

public save(user: User) {
return this.http.post<User>(this.usersUrl, user);
}
}

Нам не нужен солидный опыт работы с TypeScript, чтобы понять, как работает класс UserService . Проще говоря, он инкапсулирует в многократно используемом компоненте все функции, необходимые для использования API контроллера REST, который мы реализовали ранее в Spring Boot.

Метод findAll() выполняет HTTP-запрос GET к конечной точке http://localhost:8080/users через HttpClient Angular `` . Метод возвращает экземпляр Observable , который содержит массив объектов User .

Аналогичным образом метод save() выполняет HTTP-запрос POST к конечной точке http://localhost:8080/users .

Указав тип User в методах запроса HttpClient , мы можем проще и эффективнее использовать внутренние ответы.

Наконец, давайте отметим использование маркера метаданных @Injectable() . Это сигнализирует о том, что сервис должен быть создан и внедрен через инжекторы зависимостей Angular .

3.8. Компонент UserListComponent _

В этом случае класс UserService является тонким промежуточным звеном между службой REST и уровнем представления приложения. Поэтому нам нужно определить компонент, отвечающий за отображение списка сущностей пользователя , сохраненных в базе данных.

Давайте откроем консоль терминала, затем создадим каталог списка пользователей и создадим компонент списка пользователей:

ng generate component user-list

Angular CLI сгенерирует пустой класс компонентов, реализующий интерфейс ngOnInit . Интерфейс объявляет метод ловушки ngOnInit() , который Angular вызывает после завершения создания экземпляра реализующего класса, а также после вызова его конструктора.

Давайте реорганизуем класс, чтобы он мог принимать экземпляр UserService в конструкторе:

import { Component, OnInit } from '@angular/core';
import { User } from '../model/user';
import { UserService } from '../service/user.service';

@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html',
styleUrls: ['./user-list.component.css']
})
export class UserListComponent implements OnInit {

users: User[];

constructor(private userService: UserService) {
}

ngOnInit() {
this.userService.findAll().subscribe(data => {
this.users = data;
});
}
}

Реализация класса UserListComponent не требует пояснений. Он просто использует метод findAll() службы UserService для извлечения всех сущностей, сохраненных в базе данных, и сохраняет их в поле пользователей .

Кроме того, нам нужно отредактировать HTML-файл компонента user-list.component.html, чтобы создать таблицу, отображающую список сущностей:

<div class="card my-5">
<div class="card-body">
<table class="table table-bordered table-striped">
<thead class="thead-dark">
<tr>
<th scope="col">#</th>
<th scope="col">Name</th>
<th scope="col">Email</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of users">
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td><a href="mailto:{{ user.email }}">{{ user.email }}</a></td>
</tr>
</tbody>
</table>
</div>
</div>

Следует отметить использование директивы *ngFor . Директива называется Repeater , и мы можем использовать ее для итерации содержимого переменной и итеративного рендеринга элементов HTML. В данном случае мы использовали его для динамического рендеринга строк таблицы.

Кроме того, мы использовали переменную интерполяцию для отображения идентификатора, имени и электронной почты каждого пользователя.

3.9. Компонент UserFormComponent _

Точно так же нам нужно создать компонент, который позволит нам сохранить новый объект пользователя в базе данных.

Давайте создадим каталог пользовательской формы и напечатаем следующее:

ng generate component user-form

Далее давайте откроем файл user-form.component.ts и добавим в класс UserFormComponent метод для сохранения объекта User :

import { Component } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { UserService } from '../service/user.service';
import { User } from '../model/user';

@Component({
selector: 'app-user-form',
templateUrl: './user-form.component.html',
styleUrls: ['./user-form.component.css']
})
export class UserFormComponent {

user: User;

constructor(
private route: ActivatedRoute,
private router: Router,
private userService: UserService) {
this.user = new User();
}

onSubmit() {
this.userService.save(this.user).subscribe(result => this.gotoUserList());
}

gotoUserList() {
this.router.navigate(['/users']);
}
}

В этом случае UserFormComponent также принимает экземпляр UserService в конструкторе, который метод onSubmit() использует для сохранения предоставленного объекта User .

Поскольку нам нужно повторно отобразить обновленный список сущностей после того, как мы сохранили новый, мы вызываем метод gotoUserList() после вставки, который перенаправляет пользователя на путь /users .

Кроме того, нам нужно отредактировать файл user-form.component.html и создать HTML-форму для сохранения нового пользователя в базе данных:

<div class="card my-5">
<div class="card-body">
<form (ngSubmit)="onSubmit()" #userForm="ngForm">
<div class="form-group">
<label for="name">Name</label>
<input type="text" [(ngModel)]="user.name"
class="form-control"
id="name"
name="name"
placeholder="Enter your name"
required #name="ngModel">
</div>
<div [hidden]="!name.pristine" class="alert alert-danger">Name is required</div>
<div class="form-group">
<label for="email">Email</label>
<input type="text" [(ngModel)]="user.email"
class="form-control"
id="email"
name="email"
placeholder="Enter your email address"
required #email="ngModel">
<div [hidden]="!email.pristine" class="alert alert-danger">Email is required</div>
</div>
<button type="submit" [disabled]="!userForm.form.valid"
class="btn btn-info">Submit</button>
</form>
</div>
</div>

На первый взгляд форма выглядит довольно стандартной, но за кулисами она инкапсулирует множество функций Angular.

Обратите внимание на использование директивы ngSubmit , которая вызывает метод onSubmit() при отправке формы `` `` .

Затем мы определили переменную шаблона #userForm , поэтому Angular автоматически добавляет директиву NgForm , которая позволяет нам отслеживать форму в целом .

Директива NgForm содержит элементы управления, которые мы создали для элементов формы с директивой ngModel и атрибутом name . Он также контролирует их свойства, в том числе их состояние.

Директива ngModel дает нам возможность двусторонней привязки данных между элементами управления формы и моделью домена на стороне клиента, классом User .

Это означает, что данные, введенные в поля ввода формы, будут поступать в модель и наоборот. Изменения в обоих элементах будут немедленно отражены посредством манипуляций с DOM.

Кроме того, ngModel позволяет нам отслеживать состояние каждого элемента управления формы и выполнять проверку на стороне клиента , добавляя различные классы CSS и свойства DOM к каждому элементу управления.

В приведенном выше HTML-файле мы использовали свойства, примененные к элементам управления формы, только для отображения окна предупреждения, когда значения в форме были изменены.

3.10. Файл app -routing.module.ts

Хотя компоненты функционируют изолированно, нам все равно нужно использовать механизм для их вызова, когда пользователь нажимает кнопки на панели навигации.

Здесь в игру вступает RouterModule . Давайте откроем файл app-routing.module.ts и настроим модуль, чтобы он мог отправлять запросы соответствующим компонентам:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { UserListComponent } from './user-list/user-list.component';
import { UserFormComponent } from './user-form/user-form.component';

const routes: Routes = [
{ path: 'users', component: UserListComponent },
{ path: 'adduser', component: UserFormComponent }
];

@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

Как мы видим выше, массив Routes указывает маршрутизатору, какой компонент отображать, когда пользователь щелкает ссылку или указывает URL-адрес в адресной строке браузера.

Маршрут состоит из двух частей:

  1. Путь — строка , соответствующая URL-адресу в адресной строке браузера.
  2. Компонент — компонент для создания, когда маршрут активен (навигация)

Если пользователь нажимает кнопку « Список пользователей », которая ведет к пути /users , или вводит URL-адрес в адресную строку браузера, маршрутизатор отобразит файл шаблона компонента UserListComponent в заполнителе <router-outlet> .

Аналогичным образом, если они нажмут кнопку « Добавить пользователя », будет отображен компонент UserFormComponent .

3.11. Файл app.module.ts _

Далее нам нужно отредактировать файл app.module.ts , чтобы Angular мог импортировать все необходимые модули, компоненты и сервисы.

Кроме того, нам нужно указать, какой провайдер мы будем использовать для создания и внедрения класса UserService . В противном случае Angular не сможет внедрить его в классы компонентов:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
import { UserListComponent } from './user-list/user-list.component';
import { UserFormComponent } from './user-form/user-form.component';
import { UserService } from './service/user.service';

@NgModule({
declarations: [
AppComponent,
UserListComponent,
UserFormComponent
],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule,
FormsModule
],
providers: [UserService],
bootstrap: [AppComponent]
})
export class AppModule { }

4. Запуск приложения

Наконец, мы готовы запустить наше приложение.

Для этого мы сначала запустим приложение Spring Boot, чтобы служба REST работала и прослушивала запросы.

После запуска приложения Spring Boot мы откроем командную консоль и введем следующую команду:

ng serve --open

Это запустит сервер активной разработки Angular, а также откроет браузер по адресу http://localhost:4200 .

Мы должны увидеть панель навигации с кнопками для просмотра существующих сущностей и для добавления новых. Если мы нажмем первую кнопку, мы должны увидеть под панелью навигации таблицу со списком сущностей, сохраненных в базе данных: Аналогично, нажатие второй кнопки отобразит HTML-форму для сохранения новой сущности:

./2fd5aa7a0795c11d657e403e1b6a96fc.png

./3f6179a3f1c89943aff04e190bc6648d.png

5. Вывод

В этой статье мы узнали, как создать базовое веб-приложение с помощью Spring Boot и Angular .

Как обычно, все примеры кода, показанные в этой статье, доступны на GitHub .