Bygga en webbapplikation med fjäderkänga och vinkel

1. Översikt

Spring Boot och Angular bildar en kraftfull tandem som fungerar bra för att utveckla webbapplikationer med ett minimalt fotavtryck.

I den här handledningen använder vi Spring Boot för att implementera en RESTful backend och Angular för att skapa en JavaScript-baserad frontend.

2. Spring Boot-applikationen

Vår demo-webbapplikations funktionalitet kommer att vara ganska enkel. Det kommer bara att begränsas för att hämta och visa en lista över JPA-enheter från en H2-databas i minnet och bestå nya genom en vanlig HTML-form.

2.1. Maven-beroenden

Här är vårt Spring Boot-projekt beroende:

 org.springframework.boot spring-boot-starter-web   org.springframework.boot spring-boot-starter-data-jpa   com.h2database h2 runtime 

Observera att vi inkluderade spring-boot-starter-web eftersom vi använder den för att skapa REST-tjänsten och spring-boot-starter-jpa för att implementera uthållighetsskiktet.

H2-databasversionen hanteras också av Spring Boot-föräldern.

2.2. JPA-entitetsklassen

För att snabbt prototyper vår applikations domänlager, låt oss definiera en enkel JPA-entitetsklass, som kommer att ansvara för modellering av användare:

@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. Den UserRepository Interface

Eftersom vi behöver grundläggande CRUD funktionalitet på användar enheter måste vi också definiera en UserRepository gränssnitt:

@Repository public interface UserRepository extends CrudRepository{} 

2.4. REST-styrenheten

Låt oss nu implementera REST API. I det här fallet är det bara en enkel REST-kontroller.

@RestController @CrossOrigin(origins = "//localhost:4200") public class UserController { // standard constructors private final UserRepository userRepository; @GetMapping("/users") public List getUsers() { return (List) userRepository.findAll(); } @PostMapping("/users") void addUser(@RequestBody User user) { userRepository.save(user); } } 

Det finns ingenting som är inneboende komplext i definitionen av UserController- klassen.

Naturligtvis är den enda implementeringsdetalj som är värt att notera här användningen av @CrossOrigin- anteckningen . Som namnet antyder möjliggör anteckningen CORS (Cross-Origin Resource Sharing) på servern.

Det här steget är inte alltid nödvändigt. Eftersom vi distribuerar vår kantiga frontend till // localhost: 4200 och vår Boot-backend till // localhost: 8080 , skulle webbläsaren annars neka förfrågningar från en till en annan.

Om styrenheten metoder, getUser () hämtar alla användar enheter från databasen. På samma sätt fortsätter metoden addUser () en ny enhet i databasen som skickas i begäran.

För att hålla sakerna enkla lämnade vi medvetet bort kontrollerimplementeringen och utlöste Spring Boot-validering innan vi bestod en enhet. I produktionen kan vi dock inte lita på användarens inmatning, så validering på serversidan bör vara en obligatorisk funktion.

2.5. Bootstrapping Spring Boot Application

Slutligen, låt oss skapa en standard Spring Boot bootstrapping klass och fylla databasen med några användar enheter:

@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); }; } }

Nu, låt oss köra applikationen. Som väntat bör vi se en lista över användar enheter skrivs ut till konsolen vid start:

User{id=1, name=John, [email protected]} User{id=2, name=Julie, [email protected]} User{id=3, name=Jennifer, [email protected]} User{id=4, name=Helen, [email protected]} User{id=5, name=Rachel, [email protected]}

3. Vinkelapplikationen

Med vår demo Spring Boot-applikation igång, låt oss nu skapa en enkel Angular-applikation som kan konsumera REST-kontroller-API: et.

3.1. Vinkel CLI-installation

Vi använder Angular CLI, ett kraftfullt kommandoradsverktyg, för att skapa vår Angular-applikation.

Angular CLI är ett extremt värdefullt verktyg eftersom det gör att vi kan skapa ett helt Angular-projekt från grunden, generera komponenter, tjänster, klasser och gränssnitt med bara några få kommandon .

När vi väl har installerat npm (Node Package Manager) öppnar vi en kommandokonsol och skriver kommandot:

npm install -g @angular/[email protected]

Det är allt. Ovanstående kommando installerar den senaste versionen av Angular CLI.

3.2. Projektställningar med kantad CLI

I själva verket kan vi skapa vår vinklade applikationsstruktur från grunden. Men ärligt talat är detta en felbenägen och tidskrävande uppgift som vi i alla fall bör undvika.

Istället låter vi Angular CLI göra det hårda arbetet för oss. Så, låt oss öppna en kommandokonsol, navigera sedan till den mapp där vi vill att vår applikation ska skapas och skriv kommandot:

ng new angularclient

Det nya kommandot genererar hela applikationsstrukturen i katalogen angularclient .

3.3. Vinkelapplikationens ingångspunkt

Om vi ​​tittar in i mappen angularclient ser vi att Angular CLI effektivt har skapat ett helt projekt åt oss.

Angular's application files use TypeScript, a typed superset of JavaScript that compiles to plain JavaScript. However, the entry point of any Angular application is a plain old index.html file.

Let's edit this file, as follows:

    Spring Boot - Angular Application         

As we can see above, we included Bootstrap 4, so we can give our application UI components a more fancy look. Of course, it's possible to pick up another UI kit from the bunch available out there.

Please notice the custom tags inside the section. At first sight, they look rather weird, as is not a standard HTML 5 element.

Let's keep them right there, as is the root selector that Angular uses for rendering the application's root component.

3.4. The app.component.ts Root Component

To better understand how Angular binds an HTML template to a component, let's go to the src/app directory and edit the app.component.ts TypeScript file – the root component:

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'; } }

For obvious reasons, we'll not dive deep into learning TypeScript. Even so, let's notice that the file defines an AppComponent class, which declares a field title of type string (lower-cased). Definitively, it's typed JavaScript.

Additionally, the constructor initializes the field with a string value, which is pretty similar to what we do in Java.

The most relevant part is the @Component metadata marker or decorator, which defines three elements:

  1. selector – the HTML selector used to bind the component to the HTML template file
  2. templateUrl – the HTML template file associated with the component
  3. styleUrls – one or more CSS files associated with the component

As expected, we can use the app.component.html and app.component.css files to define the HTML template and the CSS styles of the root component.

Finally, the selector element binds the whole component to the selector included in the index.html file.

3.5. The app.component.html File

Since the app.component.html file allows us to define the root component's HTML template — the AppComponent class — we'll use it for creating a basic navigation bar with two buttons.

If we click the first button, Angular will display a table containing the list of User entities stored in the database. Similarly, if we click the second one, it will render an HTML form, which we can use for adding new entities to the database:

{{ title }}

  • List Users
  • Add User

The bulk of the file is standard HTML, with a few caveats worth noting.

The first one is the {{ title }} expression. The double curly braces {{ variable-name }} is the placeholder that Angular uses for performing variable interpolation.

Let's keep in mind that the AppComponent class initialized the title field with the value Spring Boot – Angular Application. Thus, Angular will display the value of this field in the template. Likewise, changing the value in the constructor will be reflected in the template.

The second thing to note is the routerLink attribute.

Angular uses this attribute for routing requests through its routing module (more on this later). For now, it's sufficient to know that the module will dispatch a request to the /users path to a specific component and a request to /adduser to another component.

In each case, the HTML template associated with the matching component will be rendered within the placeholder.

3.6. The User Class

Since our Angular application will fetch from and persist User entities in the database, let's implement a simple domain model with TypeScript.

Let's open a terminal console and create a model directory:

ng generate class user

Angular CLI will generate an empty User class. Let's populate it with a few fields:

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

3.7. The UserService Service

With our client-side domain User class already set, let's now implement a service class that performs GET and POST requests to the //localhost:8080/users endpoint.

This will allow us to encapsulate access to the REST controller in a single class, which we can reuse throughout the entire application.

Let's open a console terminal, then create a service directory, and within that directory, issue the following command:

ng generate service user-service

Now, let's open the user.service.ts file that Angular CLI just created and refactor it:

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 = '//localhost:8080/users'; } public findAll(): Observable { return this.http.get(this.usersUrl); } public save(user: User) { return this.http.post(this.usersUrl, user); } }

We don't need a solid background on TypeScript to understand how the UserService class works. Simply put, it encapsulates within a reusable component all the functionality required to consume the REST controller API that we implemented before in Spring Boot.

The findAll() method performs a GET HTTP request to the //localhost:8080/users endpoint via Angular's HttpClient. The method returns an Observable instance that holds an array of User objects.

Likewise, the save() method performs a POST HTTP request to the //localhost:8080/users endpoint.

By specifying the type User in the HttpClient‘s request methods, we can consume back-end responses in an easier and more effective way.

Lastly, let's notice the use of the @Injectable() metadata marker. This signals that the service should be created and injected via Angular's dependency injectors.

3.8. The UserListComponent Component

In this case, the UserService class is the thin middle-tier between the REST service and the application's presentation layer. Therefore, we need to define a component responsible for rendering the list of User entities persisted in the database.

Let's open a terminal console, then create a user-list directory and generate a user list component:

ng generate component user-list

Angular CLI will generate an empty component class that implements the ngOnInit interface. The interface declares a hook ngOnInit() method, which Angular calls after it has finished instantiating the implementing class, and after calling its constructor, too.

Let's refactor the class so that it can take a UserService instance in the constructor:

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; }); } } 

The implementation of the UserListComponent class is pretty self-explanatory. It simply uses the UserService's findAll() method to fetch all the entities persisted in the database and stores them in the users field.

Additionally, we need to edit the component's HTML file, user-list.component.html, to create the table that displays the list of entities:


    
# Name Email
{{ user.id }} {{ user.name }} {{ user.email }}

Notice the use of the *ngFor directive. The directive is called a repeater, and we can use it for iterating over the contents of a variable and iteratively rendering HTML elements. In this case, we used it for dynamically rendering the table's rows.

In addition, we used variable interpolation for showing the id,name, and email of each user.

3.9. The UserFormComponent Component

Similarly, we need to create a component that allows us to persist a new User object in the database.

Let's create a user-form directory and type the following:

ng generate component user-form 

Next, let's open the user-form.component.ts file and add to the UserFormComponent class a method for saving a User object:

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']); } }

In this case, UserFormComponent also takes a UserService instance in the constructor, which the onSubmit() method uses for saving the supplied User object.

Since we need to redisplay the updated list of entities once we have persisted a new one, we call the gotoUserList() method after the insertion, which redirects the user to the /users path.

In addition, we need to edit the user-form.component.html file and create the HTML form for persisting a new user in the database:

 Name Name is required Email Email is required Submit 

At a glance, the form looks pretty standard. But it encapsulates a lot of Angular's functionality behind the scenes.

Let's notice the use of the ngSubmit directive, which calls the onSubmit() method when the form is submitted.

Next, we have defined the template variable #userForm, so Angular adds automatically an NgForm directive, which allows us to keep track of the form as a whole.

The NgForm directive holds the controls that we created for the form elements with an ngModel directive and a name attribute and also monitors their properties, including their state.

The ngModel directive gives us two-way data binding functionality between the form controls and the client-side domain model – the User class.

This means that data entered in the form input fields will flow to the model – and the other way around. Changes in both elements will be reflected immediately via DOM manipulation.

Additionally, ngModel allows us to keep track of the state of each form control and perform client-side validation, by adding to each control different CSS classes and DOM properties.

In the above HTML file, we used the properties applied to the form controls only to display an alert box when the values in the form have been changed.

3.10. The app-routing.module.ts File

Although the components are functional in isolation, we still need to use a mechanism for calling them when the user clicks the buttons in the navigation bar.

This is where the RouterModule comes into play. So, let's open the app-routing.module.ts file, and configure the module, so it can dispatch requests to the matching components:

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 { } 

As we can see above, the Routes array instructs the router which component to display when a user clicks a link or specifies a URL into the browser address bar.

A route is composed of two parts:

  1. Path – a string that matches the URL in the browser address bar
  2. Component – the component to create when the route is active (navigated)

If the user clicks the List Users button, which links to the /users path, or enters the URL in the browser address bar, the router will render the UserListComponent component's template file in the placeholder.

Likewise, if they click the Add User button, it will render the UserFormComponent component.

3.11. The app.module.ts File

Next, we need to edit the app.module.ts file, so Angular can import all the required modules, components, and services.

Additionally, we need to specify which provider we'll use for creating and injecting the UserService class. Otherwise, Angular won't be able to inject it into the component classes:

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. Running the Application

Finally, we're ready to run our application.

To accomplish this, let's first run the Spring Boot application, so the REST service is alive and listening for requests.

När Spring Boot-applikationen har startat, låt oss öppna en kommandokonsol och skriva följande kommando:

ng serve --open

Detta startar Angular's live-utvecklingsserver och öppnar också webbläsaren på // localhost: 4200 .

Vi bör se navigeringsfältet med knapparna för att lista befintliga enheter och för att lägga till nya. Om vi ​​klickar på den första knappen borde vi se en tabell under navigeringsfältet med listan över enheter kvar i databasen:

Klicka på den andra knappen för att visa HTML-formuläret för att bestå en ny enhet:

5. Sammanfattning

I den här handledningen lärde vi oss hur man bygger en grundläggande webbapplikation med Spring Boot och Angular .

Som vanligt är alla kodprover som visas i denna handledning tillgängliga på GitHub.