A Simple E-Commerce Implementation with Spring – 用Spring实现一个简单的电子商务

最后修改: 2018年 8月 6日

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

1. Overview of Our E-commerce Application

1.我们的电子商务应用概述

In this tutorial, we’ll implement a simple e-commerce application. We’ll develop an API using Spring Boot and a client application that will consume the API using Angular.

在本教程中,我们将实现一个简单的电子商务应用程序。我们将使用Spring Boot开发一个API,并使用Angular开发一个将消费该API的客户端应用程序。

Basically, the user will be able to add/remove products from a product list to/from a shopping cart and to place an order.

基本上,用户将能够从产品列表中添加/删除产品到/离开购物车,并下订单。

2. Backend Part

2. 后台部分

To develop the API, we’ll use the latest version of Spring Boot. We also use JPA and H2 database for the persistence side of things.

为了开发这个API,我们将使用最新版本的Spring Boot。我们还使用JPA和H2数据库来实现持久性方面的工作。

To learn more about Spring Boot, you could check out our Spring Boot series of articles and if you’d like to get familiar with building a REST API, please check out another series.

要了解有关Spring Boot的更多信息, 您可以查看我们的Spring Boot系列文章,如果您想熟悉构建REST API,请查看另一个系列

2.1. Maven Dependencies

2.1.Maven的依赖性

Let’s prepare our project and import the required dependencies into our pom.xml.

让我们准备我们的项目,并将所需的依赖性导入我们的pom.xml

We’ll need some core Spring Boot dependencies:

我们需要一些核心的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>

Then, the H2 database:

然后,H2数据库

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

And finally – the Jackson library:

最后–杰克逊图书馆

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

We’ve used Spring Initializr to quickly set up the project with needed dependencies.

我们使用Spring Initializr来快速设置项目的所需依赖性。

2.2. Setting Up the Database

2.2.设置数据库

Although we could use in-memory H2 database out of the box with Spring Boot, we’ll still make some adjustments before we start developing our API.

虽然我们可以用Spring Boot开箱即用的内存H2数据库,但在开始开发我们的API之前,我们还是要做一些调整。

We’ll enable H2 console in our application.properties file so we can actually check the state of our database and see if everything is going as we’d expect.

我们将在application.properties文件中启用H2控制台,这样我们就可以实际检查数据库的状态,看看一切是否如我们所期望的那样进行。

Also, it could be useful to log SQL queries to the console while developing:

另外,在开发时将SQL查询记录到控制台可能很有用。

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

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

After adding these settings, we’ll be able to access the database at http://localhost:8080/h2-console using jdbc:h2:mem:ecommercedb as JDBC URL and user sa with no password.

添加这些设置后,我们就可以使用jdbc:h2:mem:ecommercedb作为JDBC URL和没有密码的用户sa访问a href=”http://localhost:8080/h2-console”>http://localhost:8080/h2-console的数据库。

2.3. The Project Structure

2.3.项目结构

The project will be organized into several standard packages, with Angular application put in frontend folder:

该项目将被组织成几个标准包,其中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

We should note that all interfaces in repository package are simple and extend Spring Data’s CrudRepository, so we’ll omit to display them here.

我们应该注意到,存储库包中的所有接口都很简单,并且扩展了Spring Data的CrudRepository,所以我们在这里省略了展示它们。

2.4. Exception Handling

2.4.异常处理

We’ll need an exception handler for our API in order to properly deal with eventual exceptions.

我们需要为我们的API提供一个异常处理程序,以便正确处理最终出现的异常。

You can find more details about the topic in our Error Handling for REST with Spring and Custom Error Message Handling for REST API articles.

您可以在我们的用Spring处理REST的错误REST API的自定义错误消息处理文章中找到有关该主题的更多细节

Here, we keep a focus on ConstraintViolationException and our custom ResourceNotFoundException:

在这里,我们把重点放在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. Products

2.5.产品

If you need more knowledge about persistence in Spring, there is a lot of useful articles in Spring Persistence series.

如果你需要更多关于Spring持久性的知识,在Spring持久性系列中有很多有用的文章。

Our application will support only reading products from the database, so we need to add some first.

我们的应用程序将支持只从数据库读取产品,所以我们需要先添加一些。

Let’s create a simple Product class:

让我们创建一个简单的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
}

Although the user won’t have the opportunity to add products through the application, we’ll support saving a product in the database in order to prepopulate the product list.

虽然用户没有机会通过应用程序添加产品,但我们将支持在数据库中保存产品,以便预先填充产品列表。

A simple service will be sufficient for our needs:

一个简单的服务就能满足我们的需要。

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

A simple controller will handle requests for retrieving the list of products:

一个简单的控制器将处理检索产品列表的请求。

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

    // productService constructor injection

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

All we need now in order to expose the product list to the user – is to actually put some products in the database. Therefore, we’ll make a use of CommandLineRunner class to make a Bean in our main application class.

为了向用户展示产品列表,我们现在所需要的是把一些产品真正放到数据库中。因此,我们将使用CommandLineRunner类,在我们的主应用程序类中制作一个Bean

This way, we’ll insert products into the database during the application startup:

这样,我们将在应用程序启动期间将产品插入数据库。

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

If we now start our application, we could retrieve product list via http://localhost:8080/api/products. Also, if we go to http://localhost:8080/h2-console and log in, we’ll see that there is a table named PRODUCT with the products we’ve just added.

如果我们现在启动我们的应用程序,我们可以通过http://localhost:8080/api/products检索产品列表。另外,如果我们去http://localhost:8080/h2-console登录,我们会看到有一个名为PRODUCT的表,上面有我们刚添加的产品。

2.6. Orders

2.6.订单

On the API side, we need to enable POST requests to save the orders that the end-user will make.

在API方面,我们需要启用POST请求,以保存终端用户将做出的订单。

Let’s first create the model:

让我们先来创建这个模型。

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

We should note a few things here. Certainly one of the most noteworthy things is to remember to change the default name of our table. Since we named the class Order, by default the table named ORDER should be created. But because that is a reserved SQL word, we added @Table(name = “orders”) to avoid conflicts.

在这里我们应该注意几件事。当然,最值得注意的事情之一是要记住改变我们表的默认名称。由于我们将类命名为Order,默认情况下,应该创建名为ORDER的表。但由于这是一个保留的SQL词,我们添加了@Table(name = “orders”)来避免冲突。

Furthermore, we have two @Transient methods that will return a total amount for that order and the number of products in it. Both represent calculated data, so there is no need to store it in the database.

此外,我们有两个@Transient方法,将返回该订单的总金额和其中的产品数量。这两个方法都代表了计算出来的数据,所以没有必要将其存储在数据库中。

Finally, we have a @OneToMany relation representing the order’s details. For that we need another entity class:

最后,我们有一个@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
}

We have a composite primary key here:

我们有一个复合主键 这里

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

Those classes are nothing too complicated, but we should note that in OrderProduct class we put @JsonIgnore on the primary key. That’s because we don’t want to serialize Order part of the primary key since it’d be redundant.

这些类并不复杂,但是我们应该注意,在OrderProduct类中,我们把@JsonIgnore放在主键上。这是因为我们不想序列化主键的Order部分,因为这将是多余的。

We only need the Product to be displayed to the user, so that’s why we have transient getProduct() method.

我们只需要将Product显示给用户,所以这就是为什么我们有瞬时的getProduct()方法。

Next what we need is a simple service implementation:

接下来我们需要的是一个简单的服务实现。

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

And a controller mapped to /api/orders to handle Order requests.

还有一个映射到/api/orders的控制器,处理Order请求。

Most important is the create() method:

最重要的是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);
}

First of all, we accept a list of products with their corresponding quantities. After that, we check if all products exist in the database and then create and save a new order. We’re keeping a reference to the newly created object so we can add order details to it.

首先,我们接受一个产品清单及其相应的数量。之后,我们检查数据库中是否存在所有产品然后创建并保存一个新的订单。我们保留对新创建对象的引用,以便我们可以向其添加订单的详细信息。

Finally, we create a “Location” header.

最后,我们创建一个 “位置 “标题

The detailed implementation is in the repository – the link to it is mentioned at the end of this article.

详细的实现方式在资源库中–本文末尾提到了它的链接。

3. Frontend

3.前端

Now that we have our Spring Boot application built up, it’s time to move the Angular part of the project. To do so, we’ll first have to install Node.js with NPM and, after that, an Angular CLI, a command line interface for Angular.

现在我们已经建立了我们的Spring Boot应用程序,现在是时候移动项目的Angular部分了。为此,我们首先要用NPM安装Node.js,然后再安装Angular CLI,这是Angular的命令行接口。

It’s really easy to install both of those as we could see in the official documentation.

正如我们在官方文档中看到的那样,安装这两个东西真的很容易。

3.1. Setting Up the Angular Project

3.1.设置Angular项目

As we mentioned, we’ll use Angular CLI to create our application. To keep things simple and have all in one place, we’ll keep our Angular application inside the /src/main/frontend folder.

正如我们提到的,我们将使用Angular CLI来创建我们的应用程序。为了让事情变得简单,并把所有东西放在一个地方,我们将把我们的Angular应用程序放在/src/main/frontend文件夹里。

To create it, we need to open a terminal (or command prompt) in the /src/main folder and run:

要创建它,我们需要在/src/main文件夹中打开一个终端(或命令提示符)并运行。

ng new frontend

This will create all the files and folders we need for our Angular application. In the file pakage.json, we can check which versions of our dependencies are installed. This tutorial is based on Angular v6.0.3, but older versions should do the job, at least versions 4.3 and newer (HttpClient that we use here was introduced in Angular 4.3).

这将创建我们的Angular应用程序需要的所有文件和文件夹。在文件pakage.json中,我们可以检查哪些版本的依赖项已经安装。本教程基于Angular v6.0.3,但更早的版本应该可以胜任,至少是4.3和更新的版本(我们在这里使用的HttpClient是在Angular 4.3中引入的)。

We should note that we’ll run all our commands from the /frontend folder unless stated differently.

我们应该注意,我们将从/frontend文件夹中运行所有的命令,除非另有说明。

This setup is enough to start the Angular application by running ng serve command. By default, it runs on http://localhost:4200 and if we now go there we’ll see base Angular application loaded.

这个设置足以让我们通过运行ng serve命令来启动Angular应用程序。默认情况下,它运行在http://localhost:4200,如果我们现在去那里,我们会看到基本的Angular应用程序被加载。

3.2. Adding Bootstrap

3.2.添加Bootstrap

Before we proceed with creating our own components, let’s first add Bootstrap to our project so we can make our pages look nice.

在我们继续创建自己的组件之前,首先让我们把Bootstrap添加到我们的项目中,这样我们就可以使我们的页面看起来很好。

We need just a few things to achieve this. First, we need to run a command to install it:

我们只需要几件事就能实现这个目标。首先,我们需要运行一个命令来安装它

npm install --save bootstrap

and then to say to Angular to actually use it. For this, we need to open a file src/main/frontend/angular.json and add node_modules/bootstrap/dist/css/bootstrap.min.css under “styles” property. And that’s it.

然后对Angular说,要实际使用它。为此,我们需要打开一个文件src/main/frontend/angular.json,并在node_modules/bootstrap/dist/css/bootstrap.min.css下添加“style”属性。就这样了。

3.3. Components and Models

3.3.组件和模型

Before we start creating the components for our application, let’s first check out how our app will actually look like:

在我们开始为我们的应用程序创建组件之前,让我们先看看我们的应用程序实际上是什么样子的。

ecommerce

Now, we’ll create a base component, named ecommerce:

现在,我们将创建一个基础组件,名为ecommerce

ng g c ecommerce

This will create our component inside the /frontend/src/app folder. To load it at application startup, we’ll include it into the app.component.html:

这将在/frontend/src/app文件夹中创建我们的组件。为了在应用程序启动时加载它,我们将将其包含在app.component.html中。

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

Next, we’ll create other components inside this base component:

接下来,我们将在这个基础组件内创建其他组件。

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

Certainly, we could’ve created all those folders and files manually if preferred, but in that case, we’d need to remember to register those components in our AppModule.

当然,如果愿意的话,我们可以手动创建所有这些文件夹和文件,但在这种情况下,我们需要记住在我们的AppModule中注册这些组件。

We’ll also need some models to easily manipulate our data:

我们还需要一些模型来轻松操作我们的数据。

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[] = [];
}

The last model mentioned matches our OrderForm on the backend.

最后提到的模型与我们后端的OrderForm相匹配。

3.4. Base Component

3.4.基础部分

At the top of our ecommerce component, we’ll put a navbar with the Home link on the right:

在我们的ecommerce组件的顶部,我们将放置一个导航条,右边是主页链接。

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

We’ll also load other components from here:

我们还将从这里加载其他组件。

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

We should keep in mind that, in order to see the content from our components, since we are using the navbar class, we need to add some CSS to the app.component.css:

我们应该记住,为了从我们的组件中看到内容,因为我们使用的是navbar类,我们需要在app.component.css中添加一些CSS。

.container {
    padding-top: 65px;
}

Let’s check out the .ts file before we comment most important parts:

在我们评论最重要的部分之前,让我们看看.ts文件。

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

As we can see, clicking on the Home link will reset child components. We need to access methods and a field inside child components from the parent, so that’s why we are keeping references to the children and use those inside the reset() method.

正如我们所见,点击Home链接将重置子组件。我们需要从父级访问子组件中的方法和字段,所以这就是为什么我们要保留对子组件的引用,并在reset()方法中使用这些引用。

3.5. The Service

3.5.该服务

In order for siblings components to communicate with each other and to retrieve/send data from/to our API, we’ll need to create a service:

为了让兄弟组件相互通信并从/向我们的API检索/发送数据,我们需要创建一个服务。

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

Relatively simple things are in here, as we could notice. We’re making a GET and a POST requests to communicate with the API. Also, we make data we need to share between components observable so we can subscribe to it later on.

正如我们所注意到的,这里有相对简单的东西。我们做了一个GET和一个POST请求来与API通信。此外,我们还使我们需要在组件之间共享的数据变得可观察,这样我们就可以在以后订阅它。

Nevertheless, we need to point out one thing regarding the communication with the API. If we run the application now, we would receive 404 and retrieve no data. The reason for this is that, since we are using relative URLs, Angular by default will try to make a call to http://localhost:4200/api/products and our backend application is running on localhost:8080.

然而,我们需要指出关于与API的通信的一件事。如果我们现在运行这个应用程序,我们会收到404,并且没有检索到任何数据。原因是,由于我们使用的是相对URL,Angular默认会尝试调用http://localhost:4200/api/products,而我们的后端应用程序是在localhost:8080上运行。

We could hardcode the URLs to localhost:8080, of course, but that’s not something we want to do. Instead, when working with different domains, we should create a file named proxy-conf.json in our /frontend folder:

当然,我们可以将URL硬编码为localhost:8080,但这并不是我们想要做的。相反,当使用不同的域名时,我们应该在/frontend文件夹中创建一个名为proxy-conf.json的文件

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

And then we need to open package.json and change scripts.start property to match:

然后我们需要打开package.json,改变scripts.start属性,使之相匹配。

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

And now we just should keep in mind to start the application with npm start instead ng serve.

而现在我们只需要记住用npm start而不是ng serve来启动应用程序。

3.6. Products

3.6.产品

In our ProductsComponent, we’ll inject the service we made earlier and load the product list from the API and transform it into the list of ProductOrders since we want to append a quantity field to every product:

在我们的ProductsComponent中,我们将注入我们先前制作的服务,从API加载产品列表,并将其转换为ProductOrders的列表,因为我们想给每个产品附加一个数量字段。

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

We also need an option to add the product to the shopping cart or to remove one from it:

我们还需要一个将产品添加到购物车或从购物车中删除产品的选项。

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

Finally, we’ll create a reset() method we mentioned in Section 3.4:

最后,我们将创建一个我们在第3.4节提到的reset()方法。

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

We’ll iterate through the product list in our HTML file and display it to the user:

我们将遍历HTML文件中的产品列表,并将其显示给用户。

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

We’ll also add a simple class to corresponding CSS file so everything could fit nicely:

我们还将在相应的CSS文件中添加一个简单的类,这样一切都可以很好地适应。

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

3.7. Shopping Cart

3.7.购物篮

In the ShoppingCart component, we’ll also inject the service. We’ll use it to subscribe to the changes in the ProductsComponent (to notice when the product is selected to be put in the shopping cart) and then update the content of the cart and recalculate the total cost accordingly:

ShoppingCart组件中,我们也将注入服务。我们将用它来订阅ProductsComponent中的变化(注意产品何时被选中放入购物车),然后更新购物车的内容并相应地重新计算总成本。

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

We are sending an event to the parent component from here when the order is finished and we need to go to the checkout. There is the reset() method in here also:

当订单完成,我们需要去结账时,我们从这里向父组件发送一个事件。这里也有reset()方法。

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

HTML file is simple:

HTML文件很简单。

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

3.8. Orders

3.8.订单

We’ll keep things as simple as we can and in the OrdersComponent simulate paying by setting the property to true and saving the order in the database. We can check that the orders are saved either via h2-console or by hitting http://localhost:8080/api/orders.

我们将尽可能地保持简单,在OrdersComponent中通过设置属性为 “true “来模拟支付,并将订单保存在数据库中。我们可以通过h2-console或点击http://localhost:8080/api/orders来检查订单是否被保存。

We need the EcommerceService here as well in order to retrieve the product list from the shopping cart and the total amount for our order:

我们在这里也需要EcommerceService,以便从购物车中检索产品列表和订单的总金额。

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

And finally we need to display info to the user:

最后,我们需要向用户显示信息。

<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!> You successfully made the order.
</div>

4. Merging Spring Boot and Angular Applications

4.合并Spring Boot和Angular应用程序

We finished development of both our applications and it is probably easier to develop it separately as we did. But, in production, it would be much more convenient to have a single application so let’s now merge those two.

我们完成了两个应用程序的开发,像我们这样分开开发可能更容易。但是,在生产中,有一个单一的应用程序会更方便,所以现在让我们把这两个合并。

What we want to do here is to build the Angular app which calls Webpack to bundle up all the assets and push them into the /resources/static directory of the Spring Boot app. That way, we can just run the Spring Boot application and test our application and pack all this and deploy as one app.

我们在这里要做的是构建Angular应用程序,调用Webpack来捆绑所有资产,并将其推送到Spring Boot应用程序的/resources/static目录中。这样,我们就可以直接运行Spring Boot应用程序,测试我们的应用程序,并将所有这些打包,作为一个应用程序部署。

To make this possible, we need to open ‘package.json‘ again add some new scripts after scripts.build:

为了实现这一点,我们需要打开’package.json‘,再次在scripts.build之后添加一些新的脚本。

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

We’re using some packages that we don’t have installed, so let’s install them:

我们正在使用一些没有安装的软件包,所以我们来安装它们。

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

The rimraf command is gonna look at the directory and make a new directory (cleaning it up actually), while copyfiles copies the files from the distribution folder (where Angular places everything) into our static folder.

rimraf命令将查看目录并建立一个新的目录(实际上是清理它),而copyfiles将文件从分发文件夹(Angular放置一切的地方)复制到我们的static文件夹。

Now we just need to run npm run build command and this should run all those commands and the ultimate output will be our packaged application in the static folder.

现在我们只需要运行npm run build命令,这应该会运行所有这些命令,最终的输出将是我们在静态文件夹中打包的应用程序

Then we run our Spring Boot application at the port 8080, access it there and use the Angular application.

然后我们在8080端口运行我们的Spring Boot应用程序,在那里访问它并使用Angular应用程序。

5. Conclusion

5.总结

In this article, we created a simple e-commerce application. We created an API on the backend using Spring Boot and then we consumed it in our frontend application made in Angular. We demonstrated how to make the components we need, make them communicate with each other and retrieve/send data from/to the API.

在这篇文章中,我们创建了一个简单的电子商务应用。我们使用Spring Boot在后端创建了一个API,然后我们在Angular制作的前端应用程序中使用它。我们演示了如何制作我们需要的组件,使它们相互通信并从/向API检索/发送数据。

Finally, we showed how to merge both applications into one, packaged web app inside the static folder.

最后,我们展示了如何将两个应用程序合并成一个,在静态文件夹内打包的网络应用。

As always, the complete project that we described in this article can be found in the GitHub project.

一如既往,我们在本文中描述的完整项目可以在GitHub项目中找到。