En enkel implementering av e-handel med våren

1. Oversikt over vår e-handelsapplikasjon

I denne opplæringen implementerer vi en enkel e-handelsapplikasjon. Vi utvikler en API ved hjelp av Spring Boot og et klientprogram som bruker APIet ved hjelp av Angular.

I utgangspunktet vil brukeren kunne legge til / fjerne produkter fra en produktliste til / fra en handlekurv og legge inn en bestilling.

2. Backend-del

For å utvikle API-en bruker vi den nyeste versjonen av Spring Boot. Vi bruker også JPA- og H2-databaser for utholdenhetssiden av ting.

For å lære mer om Spring Boot,du kan sjekke ut vår vårstartserie og hvis du vil For å bli kjent med å bygge et REST API, kan du sjekke ut en annen serie.

2.1. Maven avhengigheter

La oss forberede prosjektet vårt og importere de nødvendige avhengighetene til vårt pom.xml.

Vi trenger noen kjerneavhengighetsavhengigheter:

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

Deretter H2-databasen:

 com.h2database h2 1.4.197 kjøretid 

Og til slutt - Jackson-biblioteket:

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

Vi har brukt Spring Initializr til å raskt sette opp prosjektet med nødvendige avhengigheter.

2.2. Sette opp databasen

Selv om vi kunne bruke H2-database i minnet med Spring Boot, vil vi fortsatt gjøre noen justeringer før vi begynner å utvikle API-en.

Vi vil aktivere H2-konsoll i vår application.properties fil slik at vi faktisk kan sjekke tilstanden til databasen vår og se om alt går som vi forventer.

Det kan også være nyttig å logge SQL-spørsmål til konsollen mens du utvikler:

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

Etter å ha lagt til disse innstillingene, får vi tilgang til databasen på // localhost: 8080 / h2-konsoll ved hjelp av jdbc: h2: mem: e-handelb som JDBC URL og bruker sa uten passord.

2.3. Prosjektstrukturen

Prosjektet vil bli organisert i flere standardpakker, med Angular-applikasjon i frontendmappen:

├ ─ ─ p p p p ml ml ─ ─ ─ │ ─ ─ ─ ain ─ ─ ─ ─ ─ end ─ ─ ─ ─ j ava │ ─ ─ ─ ─ com com ─ ─ ─ ─ ─ ─ ba ─ ─ │ │ │ E-handelApplication.java │ │ ├───controller │ │ ├───dto │ │ ├─── Unntak │ │ ├───modell │ │ ├───positiv │ │ └───service │ │ ││└───resourcesourcesources│ources││││per││││││──per│per.perper─per─│per││per│││perper│atic│peraticaticaticaticstaticaticaticaticaticaticaticaticaticaticaticaticaticaticaticaticaticaticaticaticaticaticaticemplemplemplemplaticemplemplemplaticempl──empl─empl─emplempl empl└──emplemplemplemplemplemplemplemplempl── empl└─empl─emplemplemplemplemplempl─emplemplempl java

Vi bør merke oss at alle grensesnitt i depotpakken er enkle og utvider Spring Datas CrudRepository, så vi vil utelate å vise dem her.

2.4. Avvikshåndtering

Vi trenger en unntaksbehandler for API-en vår for å kunne håndtere eventuelle unntak.

Du kan finne mer informasjon om emnet i vår feilhåndtering for REST med vår og tilpasset feilmeldingshåndtering for REST API-artikler.

Her holder vi fokus på ConstraintViolationException og vår skikk ResourceNotFoundException:

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

2.5. Produkter

Hvis du trenger mer kunnskap om utholdenhet om våren, er det mange nyttige artikler i Spring Persistence-serien.

Søknaden vår vil støtte bare lese produkter fra databasen, så vi må legge til noen først.

La oss lage en enkel Produkt klasse:

@Entity offentlig klasse Produkt {@Id @GeneratedValue (strategi = GenerationType.IDENTITY) privat Lang id; @NotNull (melding = "Produktnavn er obligatorisk.") @Basic (valgfritt = falskt) privat strengnavn; privat dobbel pris; private String pictureUrl; // alle argumenter contructor // standard getters og setters}

Selv om brukeren ikke har muligheten til å legge til produkter gjennom applikasjonen, støtter vi lagring av et produkt i databasen for å forhåndsutfylle produktlisten.

En enkel tjeneste vil være tilstrekkelig for våre behov:

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

En enkel kontroller vil håndtere forespørsler om å hente listen over produkter:

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

Alt vi trenger nå for å eksponere produktlisten for brukeren - er å faktisk legge noen produkter i databasen. Derfor bruker vi CommandLineRunner klasse å lage en Bønne i vår viktigste applikasjonsklasse.

På denne måten vil vi sette inn produkter i databasen under oppstart av applikasjonen:

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

Hvis vi nå starter søknaden vår, kan vi hente produktlisten via // localhost: 8080 / api / produkter. Også, hvis vi går til // localhost: 8080 / h2-konsoll og logg inn, vi får se at det er en tabell som heter PRODUKT med produktene vi nettopp har lagt til.

2.6. Ordrene

På API-siden må vi aktivere POST-forespørsler for å lagre bestillingene som sluttbrukeren vil gjøre.

La oss først lage modellen:

@Entity @Table (name = "orders") public class Order {@Id @GeneratedValue (strategy = GenerationType.IDENTITY) private Long id; @JsonFormat (mønster = "dd / MM / åååå") privat LocalDate dateCreated; privat strengstatus; @JsonManagedReference @OneToMany (mappedBy = "pk.order") @Valid private Liste orderProducts = ny ArrayList (); @ Transient public Double getTotalOrderPrice () {dobbel sum = 0D; Liste orderProducts = getOrderProducts (); for (OrderProduct op: orderProducts) {sum + = op.getTotalPrice (); } retur sum; } @ Transient public int getNumberOfProducts () {returner this.orderProducts.size (); } // standard getters and setters}

Vi bør merke oss noen få ting her. Det er absolutt noe av det mest bemerkelsesverdige å gjøre husk å endre standardnavnet på tabellen vår. Siden vi kalte klassen Rekkefølge, som standard tabellen som heter REKKEFØLGE skal opprettes. Men fordi det er et reservert SQL-ord, la vi til @Table (name = “orders”) for å unngå konflikter.

Videre har vi to @Flyktig metoder som returnerer et totalt beløp for bestillingen og antall produkter i den. Begge representerer beregnede data, så det er ikke nødvendig å lagre dem i databasen.

Endelig har vi en @OneToMany forhold som representerer ordens detaljer. For det trenger vi en annen enhetsklasse:

@Entity offentlig klasse OrderProduct {@EmbeddedId @JsonIgnorer privat OrderProductPK pk; @Column (nullable = false) privat heltal; // standard konstruktør offentlig OrderProduct (ordreordre, produktprodukt, heltall) {pk = ny OrderProductPK (); pk.setOrder (rekkefølge); pk.setProduct (produkt); this.quantity = kvantitet; } @ Transient public Product getProduct () {returner this.pk.getProduct (); } @ Transient public Double getTotalPrice () {return getProduct (). GetPrice () * getQuantity (); } // standard getters og setter // hashcode () og equals () metoder}

Vi har en sammensatt primærnøkkelher:

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

Disse klassene er ikke noe for kompliserte, men vi bør merke oss det i OrderProduct klasse vi legger @JsonIgnore på hovednøkkelen. Det er fordi vi ikke vil serialisere Rekkefølge del av hovednøkkelen siden den ville være overflødig.

Vi trenger bare Produkt skal vises for brukeren, så det er derfor vi har forbigående getProduct () metode.

Det vi trenger er en enkel implementering av tjenester:

@Service @Transactional public class OrderServiceImpl implementerer OrderService {// orderRepository konstruktørinjeksjon @Override public Iterable getAllOrders () {returner this.orderRepository.findAll (); } @Override public Order create (Order order) {order.setDateCreated (LocalDate.now ()); returner this.orderRepository.save (rekkefølge); } @Override offentlig ugyldig oppdatering (bestillingsordre) {this.orderRepository.save (order); }}

Og en kontroller kartlagt til / api / bestillinger å håndtere Rekkefølge forespørsler.

Det viktigste er skape() metode:

@PostMapping public ResponseEntity create (@RequestBody OrderForm form) {List formDtos = form.getProductOrders (); validateProductsExistence (formDtos); // lage ordrelogikk // fylle ordre med produkter order.setOrderProducts (orderProducts); this.orderService.update (rekkefølge); 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 av alt, vi godtar en liste over produkter med tilsvarende mengder. Etter det, vi sjekker om alle produktene finnes i databasen og opprett og lagre en ny ordre. Vi holder en referanse til det nylig opprettede objektet, slik at vi kan legge til bestillingsdetaljer i det.

Endelig, vi oppretter en "Location" -hode.

Den detaljerte implementeringen er i depotet - lenken til det er nevnt på slutten av denne artikkelen.

3. Frontend

Nå som vi har vår Spring Boot-applikasjon bygget opp, er det på tide å flytte den vinklede delen av prosjektet. For å gjøre det, må vi først installere Node.js med NPM og deretter en Angular CLI, et kommandolinjegrensesnitt for Angular.

Det er veldig enkelt å installere begge som vi kunne se i den offisielle dokumentasjonen.

3.1. Sette opp vinkelprosjektet

Som vi nevnte, vil vi bruke Vinkel CLI for å lage søknaden vår. For å holde ting enkelt og ha alt på ett sted, holder vi vår Angular-applikasjon inne i / src / main / frontend mappe.

For å opprette den, må vi åpne en terminal (eller ledetekst) i / src / main mappe og kjør:

ng ny frontend

Dette vil opprette alle filene og mappene vi trenger for Angular-applikasjonen. I filen pakage.json, kan vi sjekke hvilke versjoner av avhengighetene våre som er installert. Denne opplæringen er basert på Angular v6.0.3, men eldre versjoner bør gjøre jobben, i det minste versjoner 4.3 og nyere (HttpClient som vi bruker her ble introdusert i Angular 4.3).

Vi bør merke oss det vi kjører alle kommandoene våre fra / frontend mappe med mindre annet er angitt.

Dette oppsettet er nok til å starte Angular-applikasjonen ved å kjøre ng server kommando. Som standard kjører den på // lokal vert: 4200 og hvis vi nå drar dit, ser vi at Angular-applikasjonen er lastet inn.

3.2. Legger til Bootstrap

Før vi fortsetter med å lage våre egne komponenter, la oss først legge til Støvelhempe til prosjektet vårt slik at vi kan få sidene våre til å se fine ut.

Vi trenger bare noen få ting for å oppnå dette. Først må vikjør en kommando for å installere den:

npm install - lagre bootstrap

og deretter å si til Angular å faktisk bruke den. For dette må vi åpne en fil src / main / frontend / angular.json og legg til node_modules / bootstrap / dist / css / bootstrap.min.css under “Stiler” eiendom. Og det er det.

3.3. Komponenter og modeller

Før vi begynner å lage komponentene for applikasjonen vår, la oss først sjekke ut hvordan appen vår faktisk vil se ut:

Nå oppretter vi en basiskomponent med navnet e-handel:

ng g c e-handel

Dette vil skape komponenten vår i / frontend / src / app mappe. For å laste den ved oppstart av applikasjonen, vil viinkluderer detinn i det app.component.html:

Deretter lager vi andre komponenter i denne basiskomponenten:

ng g c / e-handel / produkter ng g c / e-handel / bestillinger ng g c / e-handel / handlekurv

Gjerne kunne vi ha opprettet alle disse mappene og filene manuelt hvis det er foretrukket, men i så fall må vi husk å registrere disse komponentene i vår AppModule.

Vi trenger også noen modeller for å enkelt manipulere dataene våre:

eksportklasse Produkt {id: nummer; navn: streng; pris: antall; pictureUrl: streng; // alle argumentkonstruktører}
eksportklasse ProductOrder {produkt: Produkt; antall: antall; // alle argumentkonstruktører}
eksportklasse ProductOrders {productOrders: ProductOrder [] = []; }

Den siste nevnte modellen samsvarer med vår Bestillingsskjema på backend.

3.4. Basekomponent

På toppen av vår e-handel komponent, legger vi en navbar med Hjem-lenken til høyre:

 Baeldung Netthandel 
  • Hjem (nåværende)

Vi laster også andre komponenter herfra:

Vi må huske på at, for å se innholdet fra komponentene våre, siden vi bruker navbar klasse, må vi legge til litt CSS i app.component.css:

.container {padding-top: 65px; }

La oss sjekke ut .ts filen før vi kommenterer de viktigste delene:

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

Som vi kan se, klikker du på Hjem link vil tilbakestille underordnede komponenter. Vi trenger tilgang til metoder og et felt inne i barnekomponenter fra foreldrene, så det er derfor vi holder referanser til barna og bruker dem i nullstille() metode.

3.5. Tjenesten

For at søsken komponenter for å kommunisere med hverandreog å hente / sende data fra / til API-en vår, må vi opprette en tjeneste:

@Injectable () eksportklasse EcommerceService {private productsUrl = "/ api / products"; private ordersUrl = "/ api / orders"; privat produktOrder: ProductOrder; private ordrer: Produktbestillinger = nye produktbestillinger (); privat produktOrderSubject = nytt emne (); private ordersSubject = nytt emne (); privat totalSubject = nytt emne (); privat totalt: antall; 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 (order: ProductOrders) {return this.http.post (this.ordersUrl, order); } // getters og setters for delte felt}

Relativt enkle ting er her inne, som vi kunne merke. Vi lager en GET og en POST-forespørsel om å kommunisere med API. Vi gjør også data vi trenger å dele mellom komponenter observerbare, slik at vi kan abonnere på det senere.

Likevel må vi påpeke en ting angående kommunikasjonen med API. Hvis vi kjører applikasjonen nå, mottar vi 404 og henter ingen data. Årsaken til dette er at siden vi bruker relative nettadresser, vil Angular som standard prøve å ringe til // localhost: 4200 / api / produkter og backend-søknaden kjører på lokal vert: 8080.

Vi kunne hardkode URL-ene til lokal vert: 8080, selvfølgelig, men det er ikke noe vi vil gjøre. I stedet, når vi jobber med forskjellige domener, bør vi opprette en fil med navnet proxy-conf.json i vår / frontend mappe:

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

Og så må vi åpen pakke.json og endre scripts.start eiendom å passe sammen:

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

Og nå skal vi bare husk å starte applikasjonen med npm start i stedet ng server.

3.6. Produkter

I vår ProdukterKomponent, vil vi injisere tjenesten vi laget tidligere og laste produktlisten fra API og forvandle den til listen over Produktbestillinger siden vi vil legge til et antall felt til hvert produkt:

eksportklasse ProductsComponent implementerer OnInit {productOrders: ProductOrder [] = []; produkter: Produkt [] = []; selectedProductOrder: ProductOrder; privat shoppingCartBestillinger: Produktbestillinger; sub: Abonnement; 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 ( produkt, 0));})}, (feil) => konsoll.logg (feil)); } loadOrders () {this.sub = this.ecommerceService.OrdersChanged.subscribe (() => {this.shoppingCartOrders = this.ecommerceService.ProductOrders;}); }}

Vi trenger også et alternativ for å legge produktet i handlekurven eller fjerne det fra det:

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

Til slutt lager vi en nullstille() metoden vi nevnte i avsnitt 3.4:

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

Vi vil gjenta produktlisten i HTML-filen og vise den til brukeren:

{{order.product.name}}

$ {{order.product.price}}

3.8. Ordrene

Vi vil holde ting så enkle som mulig og i Bestillinger Komponent simuler å betale ved å sette eiendommen til sant og lagre bestillingen i databasen. Vi kan sjekke at ordrene er lagret enten via h2-konsoll eller ved å slå // localhost: 8080 / api / ordrer.

Vi trenger E-handelstjeneste her også for å hente produktlisten fra handlekurven og det totale beløpet for bestillingen vår:

eksportklasse OrdersComponent implementerer OnInit {orders: ProductOrders; totalt: antall; betalt: boolsk; sub: Abonnement; 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 (); } betal () {this.paid = true; this.ecommerceService.saveOrder (this.orders). abonner (); }}

Og til slutt må vi vise informasjon til brukeren:

REKKEFØLGE

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

Totalt beløp: $ {{total}}

Betale Gratulerer! Du gjorde bestillingen.

4. Slå sammen fjærstøvler og vinkelapplikasjoner

Vi avsluttet utviklingen av begge applikasjonene våre, og det er sannsynligvis lettere å utvikle det separat slik vi gjorde. Men i produksjon ville det være mye mer praktisk å ha en enkelt applikasjon, så la oss nå slå sammen de to.

Det vi ønsker å gjøre her er å bygg Angular-appen som kaller Webpack for å pakke sammen alle eiendelene og skyve dem inn i / ressurser / statisk katalogen til Spring Boot-appen. På den måten kan vi bare kjøre Spring Boot-applikasjonen og teste applikasjonen vår og pakke alt dette og distribuere som en app.

For å gjøre dette mulig, må vi åpen 'pakke.json‘Legg igjen til noen nye skript etter skript.bygge:

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

Vi bruker noen pakker som vi ikke har installert, så la oss installere dem:

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

De rimraf kommandoen skal se på katalogen og lage en ny katalog (rydde opp faktisk), mens kopifiler kopierer filene fra distribusjonsmappen (der Angular plasserer alt) til vår statisk mappe.

Nå trenger vi bare løpe npm run build kommandoen, og dette skal kjøre alle disse kommandoene, og den ultimate utgangen vil være vårt pakkede program i den statiske mappen.

Så kjører vi vår Spring Boot-applikasjon ved port 8080, får tilgang til den der og bruker Angular-applikasjonen.

5. Konklusjon

I denne artikkelen opprettet vi en enkel e-handelsapplikasjon. Vi opprettet en API på backend ved hjelp av Spring Boot, og deretter konsumerte vi den i vår frontend-applikasjon laget i Angular. Vi demonstrerte hvordan vi kan lage komponentene vi trenger, få dem til å kommunisere med hverandre og hente / sende data fra / til API.

Til slutt viste vi hvordan vi kan slå sammen begge applikasjonene i en, pakket webapp i den statiske mappen.

Som alltid kan hele prosjektet som vi beskrev i denne artikkelen bli funnet i GitHub-prosjektet.


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