«1. Обзор нашего приложения для электронной коммерции

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

По сути, пользователь сможет добавлять/удалять товары из списка товаров в/из корзины и размещать заказ.

2. Серверная часть

Для разработки API мы будем использовать последнюю версию Spring Boot. Мы также используем базу данных JPA и H2 для обеспечения устойчивости.

Чтобы узнать больше о Spring Boot, вы можете ознакомиться с нашей серией статей о Spring Boot, а если вы хотите познакомиться с созданием REST API, ознакомьтесь с другой серией.

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

Давайте подготовим наш проект и импортируем необходимые зависимости в наш pom.xml.

Нам понадобятся некоторые основные зависимости Spring Boot:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <version>2.2.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.2.2.RELEASE</version>
</dependency>

Затем база данных H2:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.197</version>
    <scope>runtime</scope>
</dependency>

И, наконец, библиотека Джексона:

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.9.6</version>
</dependency>

Мы использовали Spring Initializr для быстрой настройки проекта с необходимыми зависимостями.

2.2. Настройка базы данных

Хотя мы могли бы использовать встроенную базу данных H2 в памяти с Spring Boot, мы все же внесем некоторые коррективы, прежде чем приступить к разработке нашего API.

Мы включим консоль H2 в нашем файле application.properties, чтобы мы могли фактически проверить состояние нашей базы данных и посмотреть, все ли идет так, как мы ожидали.

Кроме того, при разработке может быть полезно логировать запросы SQL к консоли:

spring.datasource.name=ecommercedb
spring.jpa.show-sql=true

#H2 settings
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

После добавления этих настроек мы сможем получить доступ к базе данных по адресу http://localhost:8080/h2- консоль с использованием jdbc:h2:mem:ecommercedb в качестве URL-адреса JDBC и пользователя sa без пароля.

2.3. Структура проекта

Проект будет организован в несколько стандартных пакетов, а приложение Angular будет помещено в папку внешнего интерфейса:

├───pom.xml            
├───src
    ├───main
    │   ├───frontend
    │   ├───java
    │   │   └───com
    │   │       └───baeldung
    │   │           └───ecommerce
    │   │               │   EcommerceApplication.java
    │   │               ├───controller 
    │   │               ├───dto  
    │   │               ├───exception
    │   │               ├───model
    │   │               ├───repository
    │   │               └───service
    │   │                       
    │   └───resources
    │       │   application.properties
    │       ├───static
    │       └───templates
    └───test
        └───java
            └───com
                └───baeldung
                    └───ecommerce
                            EcommerceApplicationIntegrationTest.java

Следует отметить, что все интерфейсы в пакете репозитория просты и расширяют CrudRepository Spring Data, поэтому мы не буду показывать их здесь.

2.4. Обработка исключений

Нам понадобится обработчик исключений для нашего API, чтобы правильно обрабатывать возможные исключения.

Вы можете найти более подробную информацию по этой теме в наших статьях Обработка ошибок для REST с помощью Spring и Пользовательская обработка сообщений об ошибках для REST API.

Здесь мы сосредоточимся на ConstraintViolationException и нашем пользовательском ResourceNotFoundException:

@RestControllerAdvice
public class ApiExceptionHandler {

    @SuppressWarnings("rawtypes")
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<ErrorResponse> handle(ConstraintViolationException e) {
        ErrorResponse errors = new ErrorResponse();
        for (ConstraintViolation violation : e.getConstraintViolations()) {
            ErrorItem error = new ErrorItem();
            error.setCode(violation.getMessageTemplate());
            error.setMessage(violation.getMessage());
            errors.addError(error);
        }
        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }

    @SuppressWarnings("rawtypes")
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorItem> handle(ResourceNotFoundException e) {
        ErrorItem error = new ErrorItem();
        error.setMessage(e.getMessage());

        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }
}

2.5. Продукты

Если вам нужно больше знаний о сохраняемости в Spring, есть много полезных статей в серии Spring Persistence.

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

Давайте создадим простой класс Product:

@Entity
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotNull(message = "Product name is required.")
    @Basic(optional = false)
    private String name;

    private Double price;

    private String pictureUrl;

    // all arguments contructor 
    // standard getters and setters
}

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

Для наших нужд будет достаточно простого сервиса:

@Service
@Transactional
public class ProductServiceImpl implements ProductService {

    // productRepository constructor injection

    @Override
    public Iterable<Product> getAllProducts() {
        return productRepository.findAll();
    }

    @Override
    public Product getProduct(long id) {
        return productRepository
          .findById(id)
          .orElseThrow(() -> new ResourceNotFoundException("Product not found"));
    }

    @Override
    public Product save(Product product) {
        return productRepository.save(product);
    }
}

Простой контроллер будет обрабатывать запросы на получение списка продуктов:

@RestController
@RequestMapping("/api/products")
public class ProductController {

    // productService constructor injection

    @GetMapping(value = { "", "/" })
    public @NotNull Iterable<Product> getProducts() {
        return productService.getAllProducts();
    }
}

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

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

@Bean
CommandLineRunner runner(ProductService productService) {
    return args -> {
        productService.save(...);
        // more products
}

Если мы сейчас запустим наше приложение, мы сможем получить список продуктов через http://localhost:8080/api/products. Кроме того, если мы перейдем на http://localhost:8080/h2-console и войдем в систему, мы увидим, что есть таблица с именем PRODUCT с продуктами, которые мы только что добавили.

2.6. Заказы

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

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

@Entity
@Table(name = "orders")
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @JsonFormat(pattern = "dd/MM/yyyy")
    private LocalDate dateCreated;

    private String status;

    @JsonManagedReference
    @OneToMany(mappedBy = "pk.order")
    @Valid
    private List<OrderProduct> orderProducts = new ArrayList<>();

    @Transient
    public Double getTotalOrderPrice() {
        double sum = 0D;
        List<OrderProduct> orderProducts = getOrderProducts();
        for (OrderProduct op : orderProducts) {
            sum += op.getTotalPrice();
        }
        return sum;
    }

    @Transient
    public int getNumberOfProducts() {
        return this.orderProducts.size();
    }

    // standard getters and setters
}

Здесь следует отметить несколько вещей. Конечно, одна из самых примечательных вещей — не забыть изменить имя по умолчанию для нашей таблицы. Поскольку мы назвали класс Order, по умолчанию должна быть создана таблица с именем ORDER. Но поскольку это зарезервированное слово SQL, мы добавили @Table(name = «orders»), чтобы избежать конфликтов.

«Кроме того, у нас есть два метода @Transient, которые возвращают общую сумму для этого заказа и количество продуктов в нем. Оба представляют собой расчетные данные, поэтому нет необходимости хранить их в базе данных.

Наконец, у нас есть отношение @OneToMany, представляющее детали заказа. Для этого нам нужен другой класс сущности:

@Entity
public class OrderProduct {

    @EmbeddedId
    @JsonIgnore
    private OrderProductPK pk;

    @Column(nullable = false)
	private Integer quantity;

    // default constructor

    public OrderProduct(Order order, Product product, Integer quantity) {
        pk = new OrderProductPK();
        pk.setOrder(order);
        pk.setProduct(product);
        this.quantity = quantity;
    }

    @Transient
    public Product getProduct() {
        return this.pk.getProduct();
    }

    @Transient
    public Double getTotalPrice() {
        return getProduct().getPrice() * getQuantity();
    }

    // standard getters and setters

    // hashcode() and equals() methods
}

Здесь у нас есть составной первичный ключ:

@Embeddable
public class OrderProductPK implements Serializable {

    @JsonBackReference
    @ManyToOne(optional = false, fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;

    @ManyToOne(optional = false, fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id")
    private Product product;

    // standard getters and setters

    // hashcode() and equals() methods
}

В этих классах нет ничего сложного, но мы должны отметить, что в классе OrderProduct мы помещаем @JsonIgnore в первичный ключ. ключ. Это потому, что мы не хотим сериализовать часть Order первичного ключа, так как это было бы избыточно.

Нам нужно, чтобы продукт отображался пользователю, поэтому у нас есть временный метод getProduct().

Далее нам нужна простая реализация службы:

@Service
@Transactional
public class OrderServiceImpl implements OrderService {

    // orderRepository constructor injection

    @Override
    public Iterable<Order> getAllOrders() {
        return this.orderRepository.findAll();
    }
	
    @Override
    public Order create(Order order) {
        order.setDateCreated(LocalDate.now());
        return this.orderRepository.save(order);
    }

    @Override
    public void update(Order order) {
        this.orderRepository.save(order);
    }
}

И контроллер, сопоставленный с /api/orders для обработки запросов Order.

Наиболее важным является метод create():

@PostMapping
public ResponseEntity<Order> create(@RequestBody OrderForm form) {
    List<OrderProductDto> formDtos = form.getProductOrders();
    validateProductsExistence(formDtos);
    // create order logic
    // populate order with products

    order.setOrderProducts(orderProducts);
    this.orderService.update(order);

    String uri = ServletUriComponentsBuilder
      .fromCurrentServletMapping()
      .path("/orders/{id}")
      .buildAndExpand(order.getId())
      .toString();
    HttpHeaders headers = new HttpHeaders();
    headers.add("Location", uri);

    return new ResponseEntity<>(order, headers, HttpStatus.CREATED);
}

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

Наконец, мы создаем заголовок Location.

Подробная реализация находится в репозитории — ссылка на него указана в конце этой статьи.

3. Внешний интерфейс

Теперь, когда у нас создано приложение Spring Boot, пришло время переместить Angular часть проекта. Для этого нам сначала нужно установить Node.js с NPM, а затем — Angular CLI, интерфейс командной строки для Angular.

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

3.1. Настройка проекта Angular

Как мы уже упоминали, мы будем использовать Angular CLI для создания нашего приложения. Чтобы все было просто и все было в одном месте, мы будем хранить наше приложение Angular в папке /src/main/frontend.

Чтобы создать его, нам нужно открыть терминал (или командную строку) в папке /src/main и запустить:

ng new frontend

Это создаст все файлы и папки, которые нам нужны для нашего приложения Angular. В файле pakage.json мы можем проверить, какие версии наших зависимостей установлены. Это руководство основано на Angular v6.0.3, но более старые версии должны работать, по крайней мере версии 4.3 и новее (HttpClient, который мы используем здесь, был введен в Angular 4.3).

Следует отметить, что мы будем запускать все наши команды из папки /frontend, если не указано иное.

Этой настройки достаточно, чтобы запустить приложение Angular, выполнив команду ng serve. По умолчанию он работает на http://localhost:4200, и если мы сейчас перейдем туда, мы увидим загруженное базовое приложение Angular.

3.2. Добавление Bootstrap

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

Для этого нам нужно всего несколько вещей. Во-первых, нам нужно запустить команду для его установки:

npm install --save bootstrap

, а затем сказать Angular, чтобы он действительно использовал его. Для этого нам нужно открыть файл src/main/frontend/angular.json и добавить node_modules/bootstrap/dist/css/bootstrap.min.css в свойство «styles». И это все.

3.3. Компоненты и модели

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

Теперь мы создадим базовый компонент с именем ecommerce:

ng g c ecommerce

Это создаст наш компонент в папке /frontend/src/app. Чтобы загрузить его при запуске приложения, мы включим его в app.component.html:

<div class="container">
    <app-ecommerce></app-ecommerce>
</div>

Далее создадим другие компоненты внутри этого базового компонента:

ng g c /ecommerce/products
ng g c /ecommerce/orders
ng g c /ecommerce/shopping-cart

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

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

export class Product {
    id: number;
    name: string;
    price: number;
    pictureUrl: string;

    // all arguments constructor
}
export class ProductOrder {
    product: Product;
    quantity: number;

    // all arguments constructor
}
export class ProductOrders {
    productOrders: ProductOrder[] = [];
}

Последняя упомянутая модель соответствует нашей OrderForm на бэкенде.

3.4. Базовый компонент

<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
    <div class="container">
        <a class="navbar-brand" href="#">Baeldung Ecommerce</a>
        <button class="navbar-toggler" type="button" data-toggle="collapse" 
          data-target="#navbarResponsive" aria-controls="navbarResponsive" 
          aria-expanded="false" aria-label="Toggle navigation" 
          (click)="toggleCollapsed()">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div id="navbarResponsive" 
            [ngClass]="{'collapse': collapsed, 'navbar-collapse': true}">
            <ul class="navbar-nav ml-auto">
                <li class="nav-item active">
                    <a class="nav-link" href="#" (click)="reset()">Home
                        <span class="sr-only">(current)</span>
                    </a>
                </li>
            </ul>
        </div>
    </div>
</nav>

«В верхней части нашего компонента электронной коммерции мы поместим навигационную панель со ссылкой «Главная» справа:

<div class="row">
    <div class="col-md-9">
        <app-products #productsC [hidden]="orderFinished"></app-products>
    </div>
    <div class="col-md-3">
        <app-shopping-cart (onOrderFinished)=finishOrder($event) #shoppingCartC 
          [hidden]="orderFinished"></app-shopping-cart>
    </div>
    <div class="col-md-6 offset-3">
        <app-orders #ordersC [hidden]="!orderFinished"></app-orders>
    </div>
</div>

Мы также будем загружать другие компоненты отсюда:

.container {
    padding-top: 65px;
}

Мы должны помнить, что , чтобы увидеть содержимое наших компонентов, поскольку мы используем класс navbar, нам нужно добавить немного CSS в app.component.css:

@Component({
    selector: 'app-ecommerce',
    templateUrl: './ecommerce.component.html',
    styleUrls: ['./ecommerce.component.css']
})
export class EcommerceComponent implements OnInit {
    private collapsed = true;
    orderFinished = false;

    @ViewChild('productsC')
    productsC: ProductsComponent;

    @ViewChild('shoppingCartC')
    shoppingCartC: ShoppingCartComponent;

    @ViewChild('ordersC')
    ordersC: OrdersComponent;

    toggleCollapsed(): void {
        this.collapsed = !this.collapsed;
    }

    finishOrder(orderFinished: boolean) {
        this.orderFinished = orderFinished;
    }

    reset() {
        this.orderFinished = false;
        this.productsC.reset();
        this.shoppingCartC.reset();
        this.ordersC.paid = false;
    }
}

Давайте проверим файл .ts, прежде чем комментировать большинство важные части:

Как мы видим, щелчок по ссылке Home сбросит дочерние компоненты. Нам нужно получить доступ к методам и полю внутри дочерних компонентов от родителя, поэтому мы сохраняем ссылки на дочерние компоненты и используем их внутри метода reset().

3.5. Служба

@Injectable()
export class EcommerceService {
    private productsUrl = "/api/products";
    private ordersUrl = "/api/orders";

    private productOrder: ProductOrder;
    private orders: ProductOrders = new ProductOrders();

    private productOrderSubject = new Subject();
    private ordersSubject = new Subject();
    private totalSubject = new Subject();

    private total: number;

    ProductOrderChanged = this.productOrderSubject.asObservable();
    OrdersChanged = this.ordersSubject.asObservable();
    TotalChanged = this.totalSubject.asObservable();

    constructor(private http: HttpClient) {
    }

    getAllProducts() {
        return this.http.get(this.productsUrl);
    }

    saveOrder(order: ProductOrders) {
        return this.http.post(this.ordersUrl, order);
    }

    // getters and setters for shared fields
}

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

Здесь относительно простые вещи, как мы могли заметить. Мы делаем запросы GET и POST для связи с API. Кроме того, мы делаем данные, которыми мы должны делиться между компонентами, наблюдаемыми, чтобы мы могли подписаться на них позже.

Тем не менее, мы должны отметить одну вещь, касающуюся связи с API. Если мы запустим приложение сейчас, мы получим 404 и не получим никаких данных. Причина этого в том, что, поскольку мы используем относительные URL-адреса, Angular по умолчанию попытается сделать вызов http://localhost:4200/api/products, а наше серверное приложение работает на localhost:8080.

{
    "/api": {
        "target": "http://localhost:8080",
        "secure": false
    }
}

Мы могли бы жестко запрограммировать URL-адреса на localhost:8080, конечно, но это не то, что мы хотим делать. Вместо этого при работе с разными доменами мы должны создать файл с именем proxy-conf.json в нашей папке /frontend:

"scripts": {
    ...
    "start": "ng serve --proxy-config proxy-conf.json",
    ...
  }

Затем нам нужно открыть package.json и изменить свойство scripts.start, чтобы оно соответствовало: ~ ~~

И теперь мы просто должны иметь в виду, что приложение нужно запускать с помощью npm start вместо ng serve.

3.6. Products

export class ProductsComponent implements OnInit {
    productOrders: ProductOrder[] = [];
    products: Product[] = [];
    selectedProductOrder: ProductOrder;
    private shoppingCartOrders: ProductOrders;
    sub: Subscription;
    productSelected: boolean = false;

    constructor(private ecommerceService: EcommerceService) {}

    ngOnInit() {
        this.productOrders = [];
        this.loadProducts();
        this.loadOrders();
    }

    loadProducts() {
        this.ecommerceService.getAllProducts()
            .subscribe(
                (products: any[]) => {
                    this.products = products;
                    this.products.forEach(product => {
                        this.productOrders.push(new ProductOrder(product, 0));
                    })
                },
                (error) => console.log(error)
            );
    }

    loadOrders() {
        this.sub = this.ecommerceService.OrdersChanged.subscribe(() => {
            this.shoppingCartOrders = this.ecommerceService.ProductOrders;
        });
    }
}

В наш ProductsComponent мы добавим сервис, который мы сделали ранее, и загрузим список продуктов из API и преобразуем его в список ProductOrders, поскольку мы хотим добавить поле количества к каждому продукту:

addToCart(order: ProductOrder) {
    this.ecommerceService.SelectedProductOrder = order;
    this.selectedProductOrder = this.ecommerceService.SelectedProductOrder;
    this.productSelected = true;
}

removeFromCart(productOrder: ProductOrder) {
    let index = this.getProductIndex(productOrder.product);
    if (index > -1) {
        this.shoppingCartOrders.productOrders.splice(
            this.getProductIndex(productOrder.product), 1);
    }
    this.ecommerceService.ProductOrders = this.shoppingCartOrders;
    this.shoppingCartOrders = this.ecommerceService.ProductOrders;
    this.productSelected = false;
}

~~ ~ Нам также нужна возможность добавить товар в корзину или удалить его из нее:

reset() {
    this.productOrders = [];
    this.loadProducts();
    this.ecommerceService.ProductOrders.productOrders = [];
    this.loadOrders();
    this.productSelected = false;
}

Наконец, мы создадим метод reset(), о котором мы упоминали в Разделе 3.4:

<div class="row card-deck">
    <div class="col-lg-4 col-md-6 mb-4" *ngFor="let order of productOrders">
        <div class="card text-center">
            <div class="card-header">
                <h4>{{order.product.name}}</h4>
            </div>
            <div class="card-body">
                <a href="#"><img class="card-img-top" src={{order.product.pictureUrl}} 
                    alt=""></a>
                <h5 class="card-title">${{order.product.price}}</h5>
                <div class="row">
                    <div class="col-4 padding-0" *ngIf="!isProductSelected(order.product)">
                        <input type="number" min="0" class="form-control" 
                            [(ngModel)]=order.quantity>
                    </div>
                    <div class="col-4 padding-0" *ngIf="!isProductSelected(order.product)">
                        <button class="btn btn-primary" (click)="addToCart(order)"
                                [disabled]="order.quantity <= 0">Add To Cart
                        </button>
                    </div>
                    <div class="col-12" *ngIf="isProductSelected(order.product)">
                        <button class="btn btn-primary btn-block"
                                (click)="removeFromCart(order)">Remove From Cart
                        </button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

Мы пройдемся по списку продуктов в нашем HTML-файле и отобразим его пользователю:

.padding-0 {
    padding-right: 0;
    padding-left: 1;
}

Мы также добавим простой класс в соответствующий файл CSS, чтобы все могло быть красиво:

3.7 . Корзина покупок

export class ShoppingCartComponent implements OnInit, OnDestroy {
    orderFinished: boolean;
    orders: ProductOrders;
    total: number;
    sub: Subscription;

    @Output() onOrderFinished: EventEmitter<boolean>;

    constructor(private ecommerceService: EcommerceService) {
        this.total = 0;
        this.orderFinished = false;
        this.onOrderFinished = new EventEmitter<boolean>();
    }

    ngOnInit() {
        this.orders = new ProductOrders();
        this.loadCart();
        this.loadTotal();
    }

    loadTotal() {
        this.sub = this.ecommerceService.OrdersChanged.subscribe(() => {
            this.total = this.calculateTotal(this.orders.productOrders);
        });
    }

    loadCart() {
        this.sub = this.ecommerceService.ProductOrderChanged.subscribe(() => {
            let productOrder = this.ecommerceService.SelectedProductOrder;
            if (productOrder) {
                this.orders.productOrders.push(new ProductOrder(
                    productOrder.product, productOrder.quantity));
            }
            this.ecommerceService.ProductOrders = this.orders;
            this.orders = this.ecommerceService.ProductOrders;
            this.total = this.calculateTotal(this.orders.productOrders);
        });
    }

    ngOnDestroy() {
        this.sub.unsubscribe();
    }
}

В компоненте ShoppingCart мы также добавим сервис. Мы будем использовать его, чтобы подписаться на изменения в ProductsComponent (чтобы узнать, когда продукт выбран для помещения в корзину), а затем обновить содержимое корзины и соответствующим образом пересчитать общую стоимость:

finishOrder() {
    this.orderFinished = true;
    this.ecommerceService.Total = this.total;
    this.onOrderFinished.emit(this.orderFinished);
}

reset() {
    this.orderFinished = false;
    this.orders = new ProductOrders();
    this.orders.productOrders = []
    this.loadTotal();
    this.total = 0;
}

Отсюда мы отправляем событие родительскому компоненту, когда заказ готов и нам нужно перейти на кассу. Здесь также есть метод reset():

<div class="card text-white bg-danger mb-3" style="max-width: 18rem;">
    <div class="card-header text-center">Shopping Cart</div>
    <div class="card-body">
        <h5 class="card-title">Total: ${{total}}</h5>
        <hr>
        <h6 class="card-title">Items bought:</h6>

        <ul>
            <li *ngFor="let order of orders.productOrders">
                {{ order.product.name }} - {{ order.quantity}} pcs.
            </li>
        </ul>

        <button class="btn btn-light btn-block" (click)="finishOrder()"
             [disabled]="orders.productOrders.length == 0">Checkout
        </button>
    </div>
</div>

HTML-файл простой:

3.8. Заказы

Мы постараемся сделать все настолько простым, насколько это возможно, и в OrdersComponent смоделируем оплату, установив для свойства значение true и сохранив заказ в базе данных. Мы можем проверить, что заказы сохранены либо через h2-консоль, либо нажав http://localhost:8080/api/orders.

export class OrdersComponent implements OnInit {
    orders: ProductOrders;
    total: number;
    paid: boolean;
    sub: Subscription;

    constructor(private ecommerceService: EcommerceService) {
        this.orders = this.ecommerceService.ProductOrders;
    }

    ngOnInit() {
        this.paid = false;
        this.sub = this.ecommerceService.OrdersChanged.subscribe(() => {
            this.orders = this.ecommerceService.ProductOrders;
        });
        this.loadTotal();
    }

    pay() {
        this.paid = true;
        this.ecommerceService.saveOrder(this.orders).subscribe();
    }
}

Здесь нам также нужен EcommerceService, чтобы получить список товаров из корзины и общую сумму нашего заказа:

<h2 class="text-center">ORDER</h2>
<ul>
    <li *ngFor="let order of orders.productOrders">
        {{ order.product.name }} - ${{ order.product.price }} x {{ order.quantity}} pcs.
    </li>
</ul>
<h3 class="text-right">Total amount: ${{ total }}</h3>

<button class="btn btn-primary btn-block" (click)="pay()" *ngIf="!paid">Pay</button>
<div class="alert alert-success" role="alert" *ngIf="paid">
    <strong>Congratulation!</strong> You successfully made the order.
</div>

И, наконец, нам нужно отобразить информацию для пользователя:

~ ~~ 4. Объединение приложений Spring Boot и Angular

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

«Здесь мы хотим создать приложение Angular, которое вызывает Webpack для объединения всех ресурсов и помещения их в каталог /resources/static приложения Spring Boot. Таким образом, мы можем просто запустить приложение Spring Boot, протестировать наше приложение, упаковать все это и развернуть как одно приложение.

"postbuild": "npm run deploy",
"predeploy": "rimraf ../resources/static/ && mkdirp ../resources/static",
"deploy": "copyfiles -f dist/** ../resources/static",

Чтобы сделать это возможным, нам нужно снова открыть «package.json» и добавить несколько новых скриптов после scripts.build:

npm install --save-dev rimraf
npm install --save-dev mkdirp
npm install --save-dev copyfiles

Мы используем некоторые пакеты, которые у нас не установлены, так что давайте их установим:

Команда rimraf просматривает каталог и создает новый каталог (фактически очищая его), в то время как copyfiles копирует файлы из папки с дистрибутивом (куда Angular все помещает) в наш статический папка.

Теперь нам просто нужно запустить команду npm run build, и она должна запустить все эти команды, и конечным результатом будет наше упакованное приложение в статической папке.

Затем мы запускаем наше приложение Spring Boot на порту 8080, получаем к нему доступ и используем приложение Angular.

5. Заключение

В этой статье мы создали простое приложение для электронной коммерции. Мы создали API на серверной части с помощью Spring Boot, а затем использовали его в нашем внешнем приложении, созданном на Angular. Мы продемонстрировали, как сделать нужные нам компоненты, заставить их общаться друг с другом и получать/отправлять данные из/в API.

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