Springを使った簡単なEコマースの実装

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

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

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

2.バックエンドパート

APIを開発するには、最新バージョンのSpring Bootを使用します。私たちは物事の永続的な側面にもJPAとH2データベースを使います。

  • Spring Bootの詳細については、 Spring Bootシリーズの記事 を参照してください 。また、RESTの構築に慣れたい場合は API、https://www.baeldung.com/rest-with-spring-series/[その他のシリーズ] をご覧ください。

2.1. Mavenの依存関係

プロジェクトを準備して、必要な依存関係を pom.xml にインポートしましょう。

コアが必要です。 -data-jpa%22%20OR%20a%3A%22spring-boot-starter-web%22)[Spring Bootの依存関係]:

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

次に、https://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22com.h2database%22%20AND%20a%3A%22h2%22[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. データベースの設定

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

これらの設定を追加すると、 http://localhost:8080/h2-console にあるJDBC URLとパスワードなしの____jdbcを使用してデータベースにアクセスできるようになります。

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のhttps://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/CrudRepository.html[CrudRepositoryを拡張したものであることに注意してください。]なので、ここでは表示しません。

2.4. 例外処理

最終的な例外を正しく処理するためには、API用の例外ハンドラが必要です。

  • このトピックの詳細については、https://www.baeldung.com/exception-handling-for-rest-with-spring[春のRESTのエラー処理]およびhttps://www.baeldung.comを参照してください。 「REST APIのカスタムエラーメッセージ処理」の記事** /global-error-handler-in-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での永続化についてもっと知識が必要な場合は、https://www.baeldung.com/persistence-with-spring-series/[Spring Persistence series]** に役に立つ記事がたくさんあります。

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

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

商品リストをユーザーに公開するために必要なのは、データベースに商品をいくつか追加することだけです。したがって、メインアプリケーションクラスで Bean を作成するには、 CommandLineRunner クラスを使用します。

このようにして、アプリケーション起動時に製品をデータベースに挿入します。

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

ここでアプリケーションを起動すると、 http://localhost:8080/api/productsを介して製品リストを取得できます。追加したばかりの製品を含む 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") は衝突を避けるためのものです。

さらに、その注文の合計金額とその中の商品数を返す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);
    }
}

そして、コントローラーは /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部分を移動するときが来ました 。これを行うには、まずNPMと一緒にhttps://nodejs.org/en/[Node.js]をインストールし、その後https://cli.angular.io/[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 フォルダーから実行します。

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

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

独自のコンポーネントを作成する前に、まずプロジェクトに Bootstrap を追加して、ページをきれいに見せるようにしましょう。

これを実現するために必要なことはわずかです。 ** まず、インストールするためのコマンドを実行する必要があります。

npm install --save bootstrap

そして 実際にそれを使うようにAngularに言います 。このためには、 src/main/frontend/angular.json ファイルを開き、 __ node modules/bootstrap/dist/css/bootstrap.min.css __under “ styles” __プロパティを追加する必要があります。以上です。

3.3. コンポーネントとモデル

アプリケーション用のコンポーネントの作成を始める前に、まず、実際のアプリケーションの外観を確認しましょう。

リンク:/uploads/ecommerce-100x82.png%20100w[]

それでは、 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. 基本コンポーネント

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>

ここから他のコンポーネントもロードします。

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

ご覧のとおり、 Home リンクをクリックすると子コンポーネントがリセットされます。

親から子コンポーネント内のメソッドとフィールドにアクセスする必要があるため、子への参照を保持し、それらを 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との通信に関して一つのことを指摘する必要があります。今アプリケーションを実行すると、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",
    ...
  }

そして今、私たちは ng serve ** ではなく npm start でアプリケーションを起動することを心に留めておくべきです。

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

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

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

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

"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 commandを実行するだけで、これらすべてのコマンドを実行する必要があります。最終的な出力は、静的フォルダー内のパッケージ化されたアプリケーションになります。

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

5.まとめ

この記事では、簡単な電子商取引アプリケーションを作成しました。 Spring Bootを使ってバックエンドにAPIを作成し、それをAngularで作成したフロントエンドアプリケーションで使いました。必要なコンポーネントを作成し、それらを互いに通信させ、APIからデータを取得またはAPIに送信する方法を説明しました。

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

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