1. Eコマースアプリケーションの概要

このチュートリアルでは、簡単なeコマースアプリケーションを実装します。 Spring Boot を使用してAPIを開発し、Angularを使用してAPIを使用するクライアントアプリケーションを開発します。

基本的に、ユーザーは商品リストからショッピングカートに商品を追加/削除したり、注文したりすることができます。

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>

そして最後に– Jacksonライブラリ

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

Spring Initializr を使用して、必要な依存関係を持つプロジェクトをすばやくセットアップしました。

2.2. データベースの設定

Spring Bootを使用して、メモリ内のH2データベースをそのまま使用することもできますが、APIの開発を開始する前に、いくつかの調整を行います。

application.properties ファイルでH2コンソール有効にするので、データベースの状態を実際にチェックして、すべてが期待どおりに進んでいるかどうかを確認できます

また、開発中にSQLクエリをコンソールに記録すると便利な場合があります。

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

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

これらの設定を追加すると、 jdbc:h2:mem:ecommercedbをJDBCURLとして使用して、 http:// localhost:8080 /h2-consoleのデータベースにアクセスできるようになります。ユーザー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

リポジトリパッケージ内のすべてのインターフェイスは単純であり、Spring Dataの CrudRepository を拡張しているため、ここでは表示を省略していることに注意してください。

2.4. 例外処理

最終的な例外を適切に処理するには、APIの例外ハンドラーが必要です。

このトピックの詳細については、RESTを使用したRESTのエラー処理とRESTAPIのカスタムエラーメッセージ処理の記事を参照してください。

ここでは、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での永続性についてさらに知識が必要な場合は、SpringPersistenceシリーズに役立つ記事がたくさんあります。

私たちのアプリケーションはデータベースからの製品の読み取りのみをサポートするため、最初にいくつかを追加する必要があります。

簡単な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
}

ここでいくつか注意する必要があります。 確かに、最も注目すべきことの1つは、テーブルのデフォルト名を変更することを忘れないでください。 クラスにOrderという名前を付けたので、デフォルトではORDERという名前のテーブルを作成する必要があります。 ただし、これは予約済みのSQLワードであるため、競合を避けるために @Table(name =“orders”)を追加しました。

さらに、2つの @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 部分が冗長になるため、シリアル化したくないためです。

Product をユーザーに表示するだけでよいので、一時的な 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);
    }
}

また、Orderリクエストを処理するために/api/ordersにマッピングされたコントローラー。

最も重要なのは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アプリケーションが構築されたので、プロジェクト部分を移動します。 そのためには、最初に Node.js をNPMとともにインストールし、その後、 Angular CLI 、Angularのコマンドラインインターフェイスをインストールする必要があります。

公式ドキュメントにあるように、両方をインストールするのは本当に簡単です。

3.1. Angularプロジェクトの設定

前述したように、 AngularCLIを使用してアプリケーションを作成します。 物事をシンプルに保ち、すべてを1か所にまとめるために、Angularアプリケーションを / src / main /frontendフォルダー内に保持します。

作成するには、 / src / main フォルダーにあるターミナル(またはコマンドプロンプト)を開いて、次のコマンドを実行する必要があります。

ng new frontend

これにより、Angularアプリケーションに必要なすべてのファイルとフォルダーが作成されます。 ファイルpakage.jsonで、依存関係のどのバージョンがインストールされているかを確認できます。 このチュートリアルはAngularv6.0.3に基づいていますが、古いバージョン、少なくともバージョン4.3以降(ここで使用する HttpClientはAngular4.3で導入されました)で十分です。

特に明記されていない限り、すべてのコマンドを/frontendフォルダーから実行することに注意してください。

このセットアップは、ngserveコマンドを実行してAngularアプリケーションを起動するのに十分です。 デフォルトでは、 http:// localhost:4200 で実行され、そこに移動すると、ベースAngularアプリケーションがロードされていることがわかります。

3.2. ブートストラップの追加

独自のコンポーネントの作成に進む前に、まず Bootstrap をプロジェクトに追加して、ページの見栄えを良くしましょう。

これを達成するには、いくつかのことが必要です。 まず、コマンドを実行してインストールする必要があります

npm install --save bootstrap

そしてそして実際にそれを使用するようにAngularに言います。 このためには、ファイル src / main / frontend / angle.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. 基本コンポーネント

eコマースコンポーネントの上部に、右側にホームリンクが付いたナビゲーションバーを配置します。

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

コンポーネントのコンテンツを表示するには、 navbar クラスを使用しているため、app.component.cssにCSSを追加する必要があることに注意してください。 :

.container {
    padding-top: 65px;
}

最も重要な部分にコメントする前に、.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;
    }
}

ご覧のとおり、ホームリンクをクリックすると、子コンポーネントがリセットされます。 親から子コンポーネント内のメソッドとフィールドにアクセスする必要があるため、子への参照を保持し、 reset()メソッド内でそれらを使用します。

3.5. サービス

兄弟コンポーネントが相互に通信し、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
}

お気づきのように、ここには比較的単純なものがあります。 APIと通信するためにGETおよびPOSTリクエストを作成しています。 また、コンポーネント間で共有する必要のあるデータを監視可能にして、後でサブスクライブできるようにします。

それでも、APIとの通信に関して1つ指摘する必要があります。 ここでアプリケーションを実行すると、404を受け取り、データを取得しません。 これは、相対URLを使用しているため、Angularはデフォルトで http:// localhost:4200 / api / products を呼び出そうとし、バックエンドアプリケーションはで実行されているためです。 ] localhost:8080

もちろん、URLを localhost:8080 にハードコーディングすることもできますが、それは私たちがやりたいことではありません。 代わりに、異なるドメインで作業する場合は、/frontendフォルダーにproxy-conf.jsonという名前のファイルを作成する必要があります。

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

次に、 package.jsonを開き、scripts.startプロパティを次のように変更する必要があります。

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

そして今、私たちはngserveの代わりにnpmstartでアプリケーションを開始することを覚えておく必要があります。

3.6. 製品

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

また、商品をショッピングカートに追加するか、ショッピングカートから削除するオプションも必要です。

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

最後に、セクション3.4で説明した reset ()メソッドを作成します。

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

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>

また、対応するCSSファイルに単純なクラスを追加して、すべてがうまく収まるようにします。

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

3.7. ショッピングカート

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

注文が完了し、チェックアウトに進む必要があるときに、ここから親コンポーネントにイベントを送信します。 ここにも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ファイルは単純です:

<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. 注文

できる限りシンプルにし、 OrdersComponent で、プロパティをtrueに設定し、データベースに注文を保存することで、支払いをシミュレートします。 h2-console を使用するか、 http:// localhost:8080 / api/ordersを押すことで注文が保存されていることを確認できます。

ショッピングカートから商品リストと注文の合計金額を取得するには、ここでも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();
    }
}

そして最後に、ユーザーに情報を表示する必要があります。

<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. SpringBootとAngularアプリケーションのマージ

両方のアプリケーションの開発が完了しました。おそらく、私たちが行ったように、別々に開発する方が簡単です。 ただし、本番環境では、単一のアプリケーションを使用する方がはるかに便利なので、これら2つをマージしてみましょう。

ここで実行したいのは、 Webpackを呼び出すAngularアプリをビルドしてすべてのアセットをバンドルし、SpringBootアプリの/resources/staticディレクトリにプッシュすることです。 そうすれば、Spring Bootアプリケーションを実行してアプリケーションをテストし、これらすべてをパックして1つのアプリとしてデプロイできます。

これを可能にするには、scripts.buildの後にopen’package.json’で新しいスクリプトを再度追加する必要があります。

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

インストールしていないパッケージを使用しているので、それらをインストールしましょう。

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

rimraf コマンドはディレクトリを調べて新しいディレクトリを作成し(実際にクリーンアップします)、 copyfiles はファイルを配布フォルダー(Angularがすべてを配置する場所)からコピーします。 staticフォルダー。

ここで、 npm run buildコマンドを実行する必要があります。これにより、これらすべてのコマンドが実行され、最終的な出力は、静的フォルダーにパッケージ化されたアプリケーションになります。

次に、Spring Bootアプリケーションをポート8080で実行し、そこでアクセスしてAngularアプリケーションを使用します。

5. 結論

この記事では、簡単なeコマースアプリケーションを作成しました。 Spring Bootを使用してバックエンドでAPIを作成し、Angularで作成されたフロントエンドアプリケーションでそれを使用しました。 必要なコンポーネントを作成し、それらを相互に通信させ、APIとの間でデータを取得/送信する方法を示しました。

最後に、両方のアプリケーションを静的フォルダー内の1つのパッケージ化されたWebアプリにマージする方法を示しました。

いつものように、この記事で説明した完全なプロジェクトは、GitHubプロジェクトにあります。