Lekcja 22. Projekt 3: Sklep internetowy (e-sklep) — katalog, wyszukiwanie i koszyk
Trudny / egzaminacyjnyPo co się tego uczymy?
Podstawa programowa INF.04.7.3 wprost wymienia "funkcjonalności e-sklepu" jako jeden z typów aplikacji, które powinieneś umieć zaprogramować. Ten projekt buduje WŁAŚNIE to — prawdziwy, kompletny sklep internetowy z katalogiem produktów podzielonym na kategorie, wyszukiwarką, i koszykiem zakupowym z podsumowaniem zamówienia.
Teoria
Wymagania projektu:
- Katalog produktów podzielony na kategorie, z możliwością filtrowania po kategorii;
- Wyszukiwarka — pole tekstowe filtrujące produkty po nazwie NA BIEŻĄCO, w miarę pisania;
- Koszyk — dodawanie produktów, zmiana ilości, usuwanie pozycji, widoczny na każdej stronie licznik produktów w koszyku;
- Podsumowanie zamówienia i "checkout" — ekran z listą zamówionych produktów, sumą do zapłaty, i formularzem danych do wysyłki (imię, adres) z walidacją.
Wyszukiwanie NA BIEŻĄCO — debounceTime. Filtrowanie produktów przy KAŻDYM naciśnięciu klawisza (bez czekania na kliknięcie przycisku "Szukaj") to standardowa funkcjonalność e-sklepu. Naiwna implementacja wysyłałaby żądanie do backendu przy KAŻDEJ literze — marnując zasoby. RxJS (biblioteka używana wewnętrznie przez Angular, poznana pobieżnie przy Observable w lekcji 12) udostępnia operator debounceTime(300), który CZEKA 300 milisekund od OSTATNIEGO naciśnięcia klawisza, zanim faktycznie wykona wyszukiwanie — jeśli użytkownik pisze dalej, poprzednie oczekiwanie jest anulowane. Efekt: żądanie wysyłane jest dopiero, gdy użytkownik faktycznie PRZESTANIE pisać (choćby na chwilę), a nie po każdej literze.
Filtrowanie po stronie backendu (query string). Zamiast pobierać WSZYSTKIE produkty i filtrować je w Angularze (co przy dużym katalogu byłoby nieefektywne), backend przyjmuje parametry zapytania: GET /api/produkty?kategoria=elektronika&szukaj=klawiatura — kontroler odczytuje je jako zwykłe parametry metody C# (ASP.NET Core SAM mapuje parametry query string na parametry akcji o tej samej nazwie) i filtruje zapytanie LINQ/EF Core PRZED wysłaniem wyniku.
Koszyk jako serwis Angular (dokładnie ten wzorzec z lekcji 10 i projektu restauracji, lekcja 23) — KoszykService przechowuje aktualnie dodane produkty i ich ilości, udostępnia metody dodaj(), usun(), obliczSume(), i jest wstrzykiwany zarówno w komponent nagłówka (pokazujący licznik), jak i w komponent samego koszyka/checkout.
Schemat
KatalogComponent
<input [(ngModel)]="fraza" (ngModelChange)="naZmianeSzukania($event)">
│
│ RxJS: this.szukajZmiana$.pipe(debounceTime(300)).subscribe(...)
▼
GET /api/produkty?kategoria=X&szukaj=Y
│
▼
ASP.NET Core: ProduktyController.GetWszystkie(string? kategoria, string? szukaj)
│
├── query = context.Produkty.AsQueryable();
├── if (kategoria != null) query = query.Where(p => p.Kategoria == kategoria);
├── if (szukaj != null) query = query.Where(p => p.Nazwa.Contains(szukaj));
│
▼
return Ok(await query.ToListAsync());
KoszykService (singleton)
dodaj(produkt) / usun(id) / obliczSume()
│ │
▼ ▼
NaglowekComponent KoszykComponent (checkout)
(licznik produktów) (lista + suma + formularz danych wysyłki)
Przykład z życia
Każdy sklep internetowy (Allegro, Amazon, mniejsze sklepy branżowe) działa dokładnie na tej zasadzie: katalog z kategoriami i wyszukiwarką, koszyk widoczny w nagłówku niezależnie od tego, którą podstronę akurat przeglądasz, i finalne podsumowanie przed złożeniem zamówienia — to jeden z najbardziej rozpoznawalnych i praktycznych wzorców w całym świecie aplikacji webowych.
katalog.component.ts
// katalog.component.ts - wyszukiwanie z debounceTime
import { Component, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { ProduktService, Produkt } from '../produkt.service';
@Component({
selector: 'app-katalog',
standalone: true,
templateUrl: './katalog.component.html'
})
export class KatalogComponent implements OnInit {
produkty: Produkt[] = [];
fraza = '';
private szukajZmiana$ = new Subject<string>();
constructor(private produktService: ProduktService) {}
ngOnInit(): void {
this.szukajZmiana$.pipe(debounceTime(300)).subscribe(fraza => {
this.pobierzProdukty(fraza);
});
this.pobierzProdukty('');
}
naZmianeSzukania(fraza: string): void {
this.szukajZmiana$.next(fraza);
}
private pobierzProdukty(fraza: string): void {
this.produktService.szukaj(fraza).subscribe(dane => this.produkty = dane);
}
}
Controllers/ProduktyController.cs
// Controllers/ProduktyController.cs - filtrowanie przez query string
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace SklepApp.Server.Controllers;
[ApiController]
[Route("api/[controller]")]
public class ProduktyController : ControllerBase
{
private readonly SklepContext _context;
public ProduktyController(SklepContext context) => _context = context;
[HttpGet]
public async Task<ActionResult<List<Produkt>>> GetWszystkie(
[FromQuery] string? kategoria, [FromQuery] string? szukaj)
{
var zapytanie = _context.Produkty.AsQueryable();
if (!string.IsNullOrWhiteSpace(kategoria))
{
zapytanie = zapytanie.Where(p => p.Kategoria == kategoria);
}
if (!string.IsNullOrWhiteSpace(szukaj))
{
zapytanie = zapytanie.Where(p => p.Nazwa.Contains(szukaj));
}
return Ok(await zapytanie.ToListAsync());
}
}
Komentarz i wyjaśnienie kodu
Subject<string> z RxJS to "kran", do którego można "wlewać" wartości (.next(fraza)) i który jednocześnie jest Observable (można się do niego .subscribe()) — naZmianeSzukania wywoływana przy KAŻDYM naciśnięciu klawisza "wlewa" aktualną frazę, a subskrypcja z debounceTime(300) reaguje dopiero, gdy przez 300ms nic nowego nie "wlano".
[FromQuery] string? kategoria w kontrolerze C# JAWNIE oznacza, że ten parametr ma pochodzić z query string adresu URL (?kategoria=...) — w praktyce ASP.NET Core często wykrywa to automatycznie dla prostych typów jak string/int, ale jawne oznaczenie [FromQuery] jest czytelniejsze i jednoznaczne.
_context.Produkty.AsQueryable() zwraca zapytanie, które JESZCZE nie zostało wykonane — kolejne wywołania .Where(...) DOKŁADAJĄ warunki do tego samego zapytania SQL, a dopiero .ToListAsync() na końcu faktycznie wysyła je do bazy danych; dzięki temu filtrowanie po kategorii i frazie łączy się w JEDNO, efektywne zapytanie SQL, zamiast wielu osobnych.
Ćwiczenie samodzielne
Zbuduj katalog produktów z kategoriami i wyszukiwarką z debounceTime jak w przykładzie. Sprawdź w Narzędziach deweloperskich (zakładka Sieć), że żądanie do backendu wysyłane jest DOPIERO po chwili przerwy w pisaniu, a nie po każdej literze.
Zadania do pracy własnej
Dodaj do katalogu rozwijaną listę (
<select>) z kategoriami, filtrującą produkty razem z wyszukiwarką tekstową.Zbuduj
KoszykServicei komponent koszyka pokazujący dodane produkty z możliwością zmiany ilości i usunięcia pozycji, z licznikiem widocznym w nagłówku (współdzielony serwis, wzorzec z lekcji 10).Zbuduj pełny ekran "checkout" — formularz reaktywny (lekcja 11) z danymi wysyłki (imię, adres, telefon) i walidacją, wysyłający kompletne zamówienie (lista produktów + dane klienta) do backendu jako jedno żądanie POST, zapisujące zamówienie w bazie przez EF Core (analogicznie do modelu
Zamowienie/PozycjaZamowieniaz projektu restauracji, lekcja 23) i pokazujące ekran potwierdzenia z numerem zamówienia.
Typowe błędy
Wysyłanie żądania do backendu przy KAŻDYM naciśnięciu klawisza bez debounceTime — przy szybkim pisaniu generuje dziesiątki niepotrzebnych żądań sieciowych, obciążając niepotrzebnie backend.
Filtrowanie CAŁEGO katalogu po stronie Angulara zamiast na backendzie (przez query string) — działa dla małych katalogów testowych, ale nie skaluje się do prawdziwego sklepu z tysiącami produktów, gdzie pobranie WSZYSTKICH produktów za każdym razem byłoby bardzo nieefektywne.
Przechowywanie koszyka WYŁĄCZNIE w pamięci komponentu (nie w serwisie) — powoduje utratę danych koszyka przy przejściu na inną "stronę" (trasę) aplikacji, bo Angular niszczy komponent po opuszczeniu trasy; serwis (singleton, lekcja 10) przetrwa całą sesję użytkownika.
Nawiązanie do egzaminu zawodowego
To bezpośrednia realizacja "funkcjonalności e-sklepu", wymienionej WPROST w podstawie programowej INF.04.7.3 obok portalu społecznościowego, serwisu ogłoszeniowego i serwisu rezerwacyjnego. Kolejny projekt (lekcja 23) to system zamówień restauracji — pokrewna domena, ale z większym naciskiem na relacje między tabelami w bazie danych.