En simpel implementering af e-handel med foråret

1. Oversigt over vores e-handelsapplikation

I denne vejledning implementerer vi en simpel e-handelsapplikation. Vi udvikler en API ved hjælp af Spring Boot og en klientapplikation, der bruger API ved hjælp af Angular.

Dybest set vil brugeren være i stand til at tilføje / fjerne produkter fra en produktliste til / fra en indkøbskurv og placere en ordre.

2. Backend-del

For at udvikle API'en bruger vi den nyeste version af Spring Boot. Vi bruger også JPA- og H2-database til vedholdende side af tingene.

For at lære mere om Spring Boot,kan du tjekke vores Spring Boot-serie af artikler og hvis du vil for at blive fortrolig med at opbygge en REST API, skal du tjekke en anden serie.

2.1. Maven afhængigheder

Lad os forberede vores projekt og importere de krævede afhængigheder til vores pom.xml.

Vi har brug for nogle grundlæggende Spring Boot-afhængigheder:

 org.springframework.boot spring-boot-starter-data-jpa 2.2.2.RELEASE org.springframework.boot spring-boot-starter-web 2.2.2.RELEASE 

Derefter H2-databasen:

 com.h2database h2 1.4.197 runtime 

Og endelig - Jackson-biblioteket:

 com.fasterxml.jackson.datatype jackson-datatype-jsr310 2.9.6 

Vi har brugt Spring Initializr til hurtigt at oprette projektet med de nødvendige afhængigheder.

2.2. Opsætning af databasen

Selvom vi kunne bruge H2-database i hukommelsen ud af kassen med Spring Boot, foretager vi stadig nogle justeringer, inden vi begynder at udvikle vores API.

Godt aktivere H2-konsol i vores application.properties fil så vi kan faktisk kontrollere tilstanden i vores database og se om alt går som vi havde forventet.

Det kan også være nyttigt at logge SQL-forespørgsler til konsollen, mens du udvikler:

spring.datasource.name = ecommercedb spring.jpa.show-sql = true # H2-indstillinger spring.h2.console.enabled = true spring.h2.console.path = / h2-console

Efter tilføjelse af disse indstillinger kan vi få adgang til databasen på // localhost: 8080 / h2-konsol ved brug af jdbc: h2: mem: ecommercedb som JDBC URL og bruger sa uden adgangskode.

2.3. Projektets struktur

Projektet vil blive organiseret i flere standardpakker med Angular-applikation sat i frontend-mappen:

├ ─ ─ p p p p p ─ ─ ─ m │ ─ ├ ─ ─ ain ─ ─ ─ ─ ─ front end ─ ─ ─ ─ j j ava ─ ─ ─ ─ ─ com com │ ─ ─ ─ ─ │ │ │ E-handelApplication.java │ │ ├───controller │ │ ├───dto │ │ ├─── undtagelse │ │ ├───model │ │ ├───positiv │ │ └───service │ │ │ └─── ressourcer │ │ ansøgning. Ejendomme │ ├───statisk │ └─── templates └───test └───java └───com └───baeldung └───ecommerce EcommerceApplicationIntegrationTest. java

Vi skal bemærke, at alle grænseflader i arkivpakken er enkle og udvider Spring Datas CrudRepository, så vi udelader at vise dem her.

2.4. Undtagelse Håndtering

Vi har brug for en undtagelsesbehandler til vores API for korrekt at kunne håndtere eventuelle undtagelser.

Du kan finde flere detaljer om emnet i vores fejlhåndtering til REST med forår og brugerdefineret fejlmeddelelseshåndtering til REST API-artikler.

Her holder vi fokus på ConstraintViolationException og vores skik ResourceNotFoundException:

@RestControllerAdvice public class ApiExceptionHandler {@SuppressWarnings ("rawtypes") @ExceptionHandler (ConstraintViolationException.class) public ResponseEntity handle (ConstraintViolationException e) {ErrorResponse errors = new ErrorResponse (); for (ConstraintViolation overtrædelse: e.getConstraintViolations ()) {ErrorItem error = new ErrorItem (); error.setCode (violation.getMessageTemplate ()); error.setMessage (violation.getMessage ()); fejl.addError (fejl); } returner ny ResponseEntity (fejl, HttpStatus.BAD_REQUEST); } @SuppressWarnings ("rawtypes") @ExceptionHandler (ResourceNotFoundException.class) public ResponseEntity handle (ResourceNotFoundException e) {ErrorItem error = new ErrorItem (); error.setMessage (e.getMessage ()); returner ny ResponseEntity (fejl, HttpStatus.NOT_FOUND); }}

2.5. Produkter

Hvis du har brug for mere viden om vedholdenhed i foråret, er der mange nyttige artikler i Spring Persistence-serier.

Vores ansøgning understøtter læser kun produkter fra databasen, så vi skal først tilføje nogle.

Lad os oprette en simpel Produkt klasse:

@Entity offentlig klasse Produkt {@Id @GeneratedValue (strategi = GenerationType.IDENTITY) privat Lang id; @NotNull (message = "Produktnavn er påkrævet.") @Basic (valgfrit = falsk) privat strengnavn; privat dobbelt pris; private String pictureUrl; // alle argumenter contructor // standard getters og setters}

Selvom brugeren ikke har mulighed for at tilføje produkter gennem applikationen, understøtter vi at gemme et produkt i databasen for at forudbestille produktlisten.

En simpel service vil være tilstrækkelig til vores behov:

@Service @Transactional public class ProductServiceImpl implementerer ProductService {// productRepository-konstruktionsinjektion @Override public Iterable getAllProducts () {return productRepository.findAll (); } @Override public Product getProduct (long id) {return productRepository .findById (id) .orElseThrow (() -> new ResourceNotFoundException ("Produkt ikke fundet")); } @ Override public Product save (Product product) {return productRepository.save (product); }}

En simpel controller håndterer anmodninger om at hente listen over produkter:

@RestController @RequestMapping ("/ api / products") public class ProductController {// productService constructor injection @GetMapping (value = {"", "/"}) public @NotNull Iterable getProducts () {return productService.getAllProducts (); }}

Alt, hvad vi har brug for nu for at eksponere produktlisten for brugeren - er faktisk at placere nogle produkter i databasen. Derfor bruger vi CommandLineRunner klasse at lave en Bønne i vores vigtigste applikationsklasse.

På denne måde indsætter vi produkter i databasen under opstart af applikationen:

@Bean CommandLineRunner-løber (ProductService productService) {return args -> {productService.save (...); // flere produkter}

Hvis vi nu starter vores ansøgning, kan vi hente produktlisten via // localhost: 8080 / api / produkter. Også, hvis vi går til // localhost: 8080 / h2-konsol og log ind, vi ser, at der er en tabel med navnet PRODUKT med de produkter, vi lige har tilføjet.

2.6. Ordre:% s

På API-siden er vi nødt til at aktivere POST-anmodninger for at gemme de ordrer, som slutbrugeren vil foretage.

Lad os først oprette modellen:

@Entity @Table (navn = "ordrer") offentlig klasse Bestil {@Id @GeneratedValue (strategi = GenerationType.IDENTITY) privat Lang id; @JsonFormat (mønster = "dd / MM / åååå") privat LocalDate dateCreated; privat strengstatus; @JsonManagedReference @OneToMany (mappedBy = "pk.order") @Valid private Liste orderProducts = ny ArrayList (); @Transient offentlig Dobbelt getTotalOrderPrice () {dobbelt sum = 0D; Liste orderProducts = getOrderProducts (); for (OrderProduct op: orderProducts) {sum + = op.getTotalPrice (); } returneringssum } @ Transient public int getNumberOfProducts () {returner this.orderProducts.size (); } // standard getters og setters}

Vi skal bemærke et par ting her. Bestemt en af ​​de mest bemærkelsesværdige ting er at husk at ændre standardnavnet på vores tabel. Siden vi navngav klassen Bestille, som standard den navngivne tabel BESTILLE skal oprettes. Men fordi det er et reserveret SQL-ord, tilføjede vi @Table (name = “orders”) for at undgå konflikter.

Desuden har vi to @Transient metoder, der returnerer et samlet beløb for den ordre og antallet af produkter i den. Begge repræsenterer beregnede data, så der er ikke behov for at gemme dem i databasen.

Endelig har vi en @OneToMany forhold, der repræsenterer ordrens detaljer. Til det har vi brug for en anden enhedsklasse:

@Entity offentlig klasse OrderProduct {@EmbeddedId @JsonIgnorer privat OrderProductPK pk; @Column (nullable = false) privat heltalsmængde; // standard konstruktør offentlig OrderProduct (ordreordre, produktprodukt, heltal) {pk = ny OrderProductPK (); pk.setOrder (rækkefølge); pk.setProduct (produkt); denne.mængde = mængde; } @ Transient offentligt produkt getProduct () {returner this.pk.getProduct (); } @ Transient public Double getTotalPrice () {return getProduct (). GetPrice () * getQuantity (); } // standard getters og setter // hashcode () og lig () metoder}

Vi har en sammensat primærnøgleher:

@Embeddable public class OrderProductPK implementerer Serializable {@JsonBackReference @ManyToOne (valgfri = false, fetch = FetchType.LAZY) @JoinColumn (name = "order_id") privat ordreordre; @ManyToOne (valgfri = falsk, hent = FetchType.LAZY) @JoinColumn (name = "product_id") privat produktprodukt; // standard getters og setter // hashcode () og lig () metoder}

Disse klasser er ikke noget for komplicerede, men vi skal bemærke det i OrderProduct klasse vi sætter @JsonIgnorer på den primære nøgle. Det er fordi vi ikke ønsker at serie Bestille del af den primære nøgle, da den ville være overflødig.

Vi har kun brug for Produkt skal vises for brugeren, så det er derfor, vi har forbigående getProduct () metode.

Næste, hvad vi har brug for, er en simpel serviceimplementering:

@Service @Transactional public class OrderServiceImpl implementerer OrderService {// orderRepository-konstruktionsinjektion @Override public Iterable getAllOrders () {returner this.orderRepository.findAll (); } @Override public Order create (Order order) {order.setDateCreated (LocalDate.now ()); returner this.orderRepository.save (ordre); } @Override offentlig ugyldig opdatering (ordreordre) {this.orderRepository.save (ordre); }}

Og en controller kortlagt til / api / ordrer at håndtere Bestille anmodninger.

Det vigtigste er skab() metode:

@PostMapping public ResponseEntity create (@RequestBody OrderForm form) {List formDtos = form.getProductOrders (); validateProductsExistence (formDtos); // opret ordrelogik // udfyld ordre med produkter order.setOrderProducts (orderProducts); this.orderService.update (ordre); String uri = ServletUriComponentsBuilder .fromCurrentServletMapping () .path ("/ orders / {id}") .buildAndExpand (order.getId ()) .toString (); HttpHeaders headers = nye HttpHeaders (); headers.add ("Location", uri); returner ny ResponseEntity (ordre, overskrifter, HttpStatus.CREATED); }

Først og fremmest, vi accepterer en liste over produkter med deres tilsvarende mængder. Efter det, vi kontrollerer, om alle produkter findes i databasen og derefter oprette og gemme en ny ordre. Vi holder en henvisning til det nyoprettede objekt, så vi kan tilføje ordredetaljer til det.

Langt om længe, vi opretter en "Location" -overskrift.

Den detaljerede implementering findes i arkivet - linket til det er nævnt i slutningen af ​​denne artikel.

3. Frontend

Nu hvor vores Spring Boot-applikation er bygget op, er det tid til at flytte den vinklede del af projektet. For at gøre dette skal vi først installere Node.js med NPM og derefter en Angular CLI, en kommandolinjegrænseflade til Angular.

Det er virkelig nemt at installere begge, som vi kunne se i den officielle dokumentation.

3.1. Opsætning af vinkelprojektet

Som vi nævnte, bruger vi Vinkel CLI for at oprette vores applikation. For at holde tingene enkle og have alt på ét sted, holder vi vores Angular-applikation inde i / src / main / frontend folder.

For at oprette det skal vi åbne en terminal (eller kommandoprompt) i / src / main mappe og kør:

ng ny frontend

Dette opretter alle de filer og mapper, vi har brug for til vores Angular-applikation. I filen pakage.json, kan vi kontrollere, hvilke versioner af vores afhængigheder der er installeret. Denne tutorial er baseret på Angular v6.0.3, men ældre versioner skal gøre jobbet, i det mindste version 4.3 og nyere (HttpClient som vi bruger her blev introduceret i Angular 4.3).

Det skal vi bemærke vi kører alle vores kommandoer fra / frontend folder medmindre andet er angivet.

Denne opsætning er nok til at starte Angular-applikationen ved at køre ng server kommando. Som standard kører det // lokal vært: 4200 og hvis vi nu går der, vil vi se basen Angular-applikationen indlæst.

3.2. Tilføjelse af Bootstrap

Før vi fortsætter med at oprette vores egne komponenter, skal vi først tilføje Bootstrap til vores projekt, så vi kan få vores sider til at se pæne ud.

Vi har kun brug for et par ting for at opnå dette. Først skal vikør en kommando for at installere den:

npm install - gem bootstrap

og derefter at sige til Angular at faktisk bruge det. Til dette er vi nødt til at åbne en fil src / main / frontend / angular.json og tilføj node_modules / bootstrap / dist / css / bootstrap.min.css under “Stilarter” ejendom. Og det er det.

3.3. Komponenter og modeller

Før vi begynder at oprette komponenterne til vores applikation, skal vi først se, hvordan vores app faktisk ser ud:

Nu opretter vi en basiskomponent med navnet e-handel:

ng g c e-handel

Dette vil skabe vores komponent inde i / frontend / src / app folder. For at indlæse det ved opstart af applikationen skal viinkluderer detind i app.component.html:

Dernæst opretter vi andre komponenter inde i denne basiskomponent:

ng g c / e-handel / produkter ng g c / e-handel / ordrer ng g c / e-handel / indkøbskurv

Bestemt kunne vi have oprettet alle disse mapper og filer manuelt, hvis det foretrækkes, men i så fald skulle vi have brug for det husk at registrere disse komponenter i vores AppModule.

Vi har også brug for nogle modeller for nemt at manipulere vores data:

eksportklasse Produkt {id: nummer; navn: streng; pris: antal; pictureUrl: streng; // alle argumenter konstruktør}
eksportklasse ProductOrder {produkt: Produkt; antal: antal; // alle argumenter konstruktør}
eksportklasse ProductOrders {productOrders: ProductOrder [] = []; }

Den sidst nævnte model matcher vores Bestillingsformular på backend.

3.4. Basekomponent

Øverst i vores e-handel komponent, lægger vi en navbar med hjemmekoblingen til højre:

 Baeldung e-handel 
  • Hjem (nuværende)

Vi indlæser også andre komponenter herfra:

Vi skal huske på, at for at se indholdet fra vores komponenter, da vi bruger navbar klasse, skal vi tilføje noget CSS til app.component.css:

.container {padding-top: 65px; }

Lad os se på .ts fil, før vi kommenterer de vigtigste dele:

@Component ({selector: 'app-ecommerce', templateUrl: './ecommerce.component.html', styleUrls: ['./ecommerce.component.css']}) eksportklasse EcommerceComponent implementerer OnInit {private collapsed = true; orderFinished = false; @ViewChild ('produkterC') produkterC: ProdukterKomponent; @ViewChild ('shoppingCartC') shoppingCartC: ShoppingCartComponent; @ViewChild ('ordersC') ordersC: OrdersComponent; toggleCollapsed (): ugyldig {this.collapsed =! this.collapsed; } finishOrder (orderFinished: boolean) {this.orderFinished = orderFinished; } nulstil () {this.orderFinished = false; this.productsC.reset (); this.shoppingCartC.reset (); this.ordersC.paid = false; }}

Som vi kan se, klik på Hjem link nulstiller underordnede komponenter. Vi har brug for at få adgang til metoder og et felt inde i underordnede komponenter fra forældrene, så det er derfor, vi holder henvisninger til børnene og bruger dem inden i Nulstil() metode.

3.5. Servicen

For at søskendes komponenter til at kommunikere med hinandenog at hente / sende data fra / til vores API, skal vi oprette en tjeneste:

@Injectable () eksportklasse EcommerceService {private productsUrl = "/ api / produkter"; private ordersUrl = "/ api / orders"; privat produktOrder: ProductOrder; private ordrer: Produktbestillinger = nye produktbestillinger (); privat productOrderSubject = nyt emne (); private ordersSubject = nyt emne (); privat totalSubject = nyt emne (); privat total: antal; ProductOrderChanged = this.productOrderSubject.asObservable (); OrdersChanged = this.ordersSubject.asObservable (); TotalChanged = this.totalSubject.asObservable (); konstruktør (privat http: HttpClient) {} getAllProducts () {returner this.http.get (this.productsUrl); } saveOrder (ordre: ProductOrders) {returner this.http.post (this.ordersUrl, order); } // getters og settere til delte felter}

Der er relativt enkle ting herinde, som vi kunne bemærke. Vi laver en GET- og en POST-anmodning om at kommunikere med API'en. Vi gør også data, som vi skal dele mellem komponenter, observerbare, så vi kan abonnere på det senere.

Ikke desto mindre er vi nødt til at påpege en ting vedrørende kommunikationen med API'en. Hvis vi kører applikationen nu, modtager vi 404 og henter ingen data. Årsagen til dette er, at da vi bruger relative webadresser, vil Angular som standard forsøge at ringe til // localhost: 4200 / api / produkter og vores backend-applikation kører lokal vært: 8080.

Vi kunne hardcode webadresserne til lokal vært: 8080Selvfølgelig, men det er ikke noget, vi vil gøre. I stedet, når vi arbejder med forskellige domæner, skal vi oprette en fil med navnet proxy-conf.json i vores / frontend folder:

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

Og så skal vi åben pakke.json og ændre scripts.start ejendom at matche:

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

Og nu skal vi bare husk at starte applikationen med npm start i stedet ng server.

3.6. Produkter

I vores ProdukterKomponent, vi indsprøjter den service, vi lavede tidligere, og indlæser produktlisten fra API'en og omdanner den til listen over Produktbestillinger da vi vil tilføje et kvantitetsfelt til hvert produkt:

eksportklasse ProductsComponent implementerer OnInit {productOrders: ProductOrder [] = []; produkter: Produkt [] = []; selectedProductOrder: ProductOrder; privat shoppingKortbestillinger: Produktbestillinger; sub: Abonnement; productSelected: boolean = false; konstruktør (privat ecommerceService: EcommerceService) {} ngOnInit () {this.productOrders = []; this.loadProducts (); this.loadOrders (); } loadProducts () {this.ecommerceService.getAllProducts (). abonner ((products: any []) => {this.products = products; this.products.forEach (product => {this.productOrders.push (new ProductOrder ( produkt, 0));})}, (fejl) => konsol.log (fejl)); } loadOrders () {this.sub = this.ecommerceService.OrdersChanged.subscribe (() => {this.shoppingCartOrders = this.ecommerceService.ProductOrders;}); }}

Vi har også brug for en mulighed for at føje produktet til indkøbskurven eller fjerne en fra den:

addToCart (ordre: ProductOrder) {this.ecommerceService.SelectedProductOrder = ordre; this.selectedProductOrder = this.ecommerceService.SelectedProductOrder; this.productSelected = true; } removeFromCart (productOrder: ProductOrder) {let index = this.getProductIndex (productOrder.product); hvis (indeks> -1) {this.shoppingCartOrders.productOrders.splice (this.getProductIndex (productOrder.product), 1); } this.ecommerceService.ProductOrders = this.shoppingCartOrders; this.shoppingCartOrders = this.ecommerceService.ProductOrders; this.productSelected = false; }

Endelig opretter vi en Nulstil() metode, vi nævnte i afsnit 3.4:

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

Vi gentager produktlisten i vores HTML-fil og viser den for brugeren:

{{order.product.name}}

$ {{order.product.price}}

3.8. Ordre:% s

Vi holder tingene så enkle som vi kan og i OrdrerKomponent simuler betaling ved at indstille ejendommen til sand og gemme ordren i databasen. Vi kan kontrollere, at ordrene gemmes enten via h2-konsol eller ved at ramme // localhost: 8080 / api / ordrer.

Vi har brug for E-handelstjeneste også her for at hente produktlisten fra indkøbskurven og det samlede beløb for vores ordre:

eksportklasse OrdersComponent implementerer OnInit {ordrer: ProductOrders; i alt: antal; betalt: boolsk; sub: Abonnement; konstruktør (privat ecommerceService: EcommerceService) {this.orders = this.ecommerceService.ProductOrders; } ngOnInit () {this.paid = false; this.sub = this.ecommerceService.OrdersChanged.subscribe (() => {this.orders = this.ecommerceService.ProductOrders;}); this.loadTotal (); } betal () {this.paid = true; this.ecommerceService.saveOrder (this.orders). abonner (); }}

Og endelig er vi nødt til at vise info til brugeren:

BESTILLE

  • {{order.product.name}} - $ {{order.product.price}} x {{order.quantity}} stk.

Samlet beløb: $ {{total}}

Betale Tillykke! Du har bestilt ordren.

4. Flet fjederstøvle og kantede applikationer

Vi afsluttede udviklingen af ​​begge vores applikationer, og det er sandsynligvis lettere at udvikle det separat som vi gjorde. Men i produktionen ville det være meget mere praktisk at have en enkelt applikation, så lad os nu flette disse to.

Hvad vi vil gøre her er at bygge den Angular-app, der kalder Webpack for at samle alle aktiverne og skubbe dem ind i / ressourcer / statisk katalog over Spring Boot-appen. På den måde kan vi bare køre Spring Boot-applikationen og teste vores applikation og pakke alt dette og implementere som en app.

For at gøre dette muligt skal vi åben 'pakke.json'Tilføj igen nogle nye scripts efter scripts.bygge:

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

Vi bruger nogle pakker, som vi ikke har installeret, så lad os installere dem:

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

Det rimraf kommandoen vil se på biblioteket og oprette en ny mappe (rent faktisk rydde op), mens kopifiler kopierer filerne fra distributionsmappen (hvor Angular placerer alt) i vores statisk folder.

Nu skal vi bare løb npm køre build kommando, og dette skal køre alle disse kommandoer, og den ultimative output vil være vores pakkede applikation i den statiske mappe.

Derefter kører vi vores Spring Boot-applikation i port 8080, får adgang til den der og bruger Angular-applikationen.

5. Konklusion

I denne artikel oprettede vi en simpel e-handelsapplikation. Vi oprettede en API på backend ved hjælp af Spring Boot, og derefter forbrugte vi den i vores frontend-applikation lavet i Angular. Vi demonstrerede, hvordan vi laver de komponenter, vi har brug for, får dem til at kommunikere med hinanden og hente / sende data fra / til API'en.

Endelig viste vi, hvordan vi flettede begge applikationer til en, pakket webapp i den statiske mappe.

Som altid kan det komplette projekt, som vi beskrev i denne artikel, findes i GitHub-projektet.


$config[zx-auto] not found$config[zx-overlay] not found