This is an old revision of the document!
Asincronicitatea este capacitatea unei aplicații de a rula multiple procese în paralel, fără să fie blocată de procesele care durează mai mult sau care necesită așteptare. În React.js, asincronicitatea este importantă pentru că aplicațiile sunt construite în mare parte în jurul evenimentelor și a datelor care se schimbă în timp real. Prin urmare, este important să putem rula procese asincrone în timp ce aplicația rămâne responsive.
De exemplu, atunci când facem o cerere către o bază de date pentru a obține informații pentru a afișa pe o pagină, aceasta poate dura ceva timp. În acest timp, aplicația nu ar trebui să fie blocată sau să înceteze să funcționeze. În schimb, React.js poate utiliza asincronicitatea pentru a rula procesul în fundal, fără să blocheze restul aplicației.
În plus, asincronicitatea este importantă în React.js pentru a gestiona evenimente și animații în timp real. De exemplu, atunci când utilizatorul face clic pe un buton pentru a efectua o acțiune, aplicația trebuie să răspundă rapid pentru a da utilizatorului feedback. Prin utilizarea asincronicității, aplicația poate rula procesul în fundal fără a bloca restul aplicației și fără a încetini răspunsul.
În concluzie, asincronicitatea este importantă pentru a asigura o performanță bună și o experiență de utilizare plăcută. Prin rularea proceselor asincrone, aplicația poate rămâne responsive și poate gestiona evenimente și date în timp real.
În React.js, putem utiliza API-uri asincrone pentru a obține și a procesa date de la servere externe. Acest lucru poate fi făcut folosind funcții precum fetch(), axios sau XMLHttpRequest pentru a efectua cereri HTTP către un server. API-urile asincrone sunt esențiale în dezvoltarea aplicațiilor moderne, deoarece permit obținerea de date în timp real fără a fi nevoie de încărcarea unei întregi pagini.
Pentru a utiliza API-uri asincrone în React.js, trebuie să creăm o componentă care să utilizeze o astfel de funcție. O abordare comună este de a utiliza fetch() pentru a efectua o cerere GET către server pentru a obține datele. În momentul în care datele sunt primite, putem folosi metoda setState() pentru a actualiza starea componentei cu noile date.
import React, { Component } from 'react'; class MyComponent extends Component { constructor(props) { super(props); this.state = { data: [], isLoading: true, error: null, }; } componentDidMount() { fetch('https://jsonplaceholder.typicode.com/todos') .then(response => response.json()) .then(data => this.setState({ data: data, isLoading: false })) .catch(error => this.setState({ error, isLoading: false })); } render() { const { data, isLoading, error } = this.state; if (error) { return <p>{error.message}</p>; } if (isLoading) { return <p>Loading...</p>; } return ( <div> <h1>Lista de sarcini:</h1> <ul> {data.map(task => ( <li key={task.id}>{task.title}</li> ))} </ul> </div> ); } } export default MyComponent;
În concluzie, utilizarea API-urilor asincrone în React.js este esențială pentru a obține date din servere externe și pentru a actualiza componentele în timp real fără a fi nevoie de încărcarea unei întregi pagini.
Material UI este o bibliotecă de componente pentru React.js, care oferă o colecție de componente UI pre-stilizate pentru a ajuta la crearea de interfețe utilizator moderne și atractive. Aceste componente sunt bazate pe stilurile și principiile de design ale Google Material Design, care au fost dezvoltate pentru a oferi un aspect coerent și familiar aplicațiilor web și mobile.
Material UI oferă o varietate de componente, inclusiv butoane, casete de selectare, câmpuri de text, tab-uri, bare de navigare, bare laterale, ferestre modale, diagrame și multe altele. Aceste componente sunt ușor de utilizat și oferă o varietate de opțiuni de personalizare pentru a se potrivi cu designul și aspectul dorit.
Material UI oferă, de asemenea, o varietate de teme predefinite și opțiuni de personalizare, care permit utilizatorilor să își personalizeze aspectul aplicației în funcție de nevoile și preferințele lor. De exemplu, puteți schimba culorile de accent, fonturile, dimensiunile și multe altele.
În plus, Material UI este bine documentată și oferă o comunitate activă de utilizatori care contribuie la dezvoltarea și îmbunătățirea acesteia. De asemenea, este compatibilă cu multe alte biblioteci și cadre de lucru populare, cum ar fi Redux, Next.js, TypeScript și multe altele. Pentru a utiliza Material UI într-un proiect React.js, trebuie să instalați mai întâi biblioteca utilizând un manager de pachete, cum ar fi npm sau yarn. După aceea, puteți importa componentele dorite în componentele React și le puteți utiliza așa cum doriți.
De exemplu, pentru a utiliza un buton Material UI, puteți importa componenta Buton din biblioteca Material UI și o puteți utiliza în componenta React în felul următor:
import { Button } from '@material-ui/core'; function MyButton() { return ( <Button variant="contained" color="primary"> Apasă-mă </Button> ); }
OpenAPI, anterior cunoscut sub numele de Swagger, este un set de specificații și instrumente open source care permit dezvoltatorilor să definească, să documenteze și să utilizeze servicii web RESTful.
Prin utilizarea OpenAPI, dezvoltatorii pot crea documentații clare și ușor de înțeles pentru serviciile lor web, ceea ce face mai ușor pentru utilizatorii acestor servicii să le utilizeze și să le integreze în propriile aplicații. De asemenea, OpenAPI permite dezvoltatorilor să genereze automat codul clientului pentru interacțiunea cu un API, ceea ce poate reduce timpul și efortul necesar pentru a crea clientul API-ului.
OpenAPI este utilizat de multe companii mari, inclusiv Amazon, Google și Microsoft, și este susținut de comunitatea open source.
OpenAPI Generator este un instrument open source care permite generarea automată a codului clientului și al serverului pentru interacțiunea cu un API web, pe baza unei specificații OpenAPI. Acesta poate fi utilizat cu o varietate de limbaje de programare și platforme, inclusiv Java, JavaScript, Python, Ruby, C # și multe altele.
OpenAPI Generator poate fi folosit pentru a genera codul clientului API, care poate fi inclus în aplicația dvs. React.js. Acest lucru vă permite să interacționați cu API-ul utilizând metodele și tipurile de date definite în specificația OpenAPI, fără a fi necesară scrierea manuală a codului clientului.
În plus, OpenAPI Generator poate fi utilizat și pentru a genera codul serverului API, care poate fi folosit pentru a crea propriul dvs. API, bazat pe specificația OpenAPI. Acest lucru poate fi util dacă doriți să oferiți un API personalizat pentru propriile aplicații sau pentru a permite altor dezvoltatori să utilizeze serviciile dvs. web prin intermediul unui API.
OpenAPI Generator este disponibil ca o bibliotecă Java și poate fi instalat și utilizat ca o unealtă de linie de comandă. De asemenea, există și integrări disponibile pentru diferite platforme de dezvoltare, cum ar fi Maven, Gradle, Node.js și altele.
Pentru a utiliza OpenAPI Generator într-un proiect React.js, urmați acești pași:
npm install @openapitools/openapi-generator-cli -g
openapi-generator generate -i swagger.yaml -g javascript -o client
Alternativ, pentru scheletul de laborator, porniți BE-ul si folosiți comanda:
openapi-generator-cli generate -i http://localhost:5000/swagger/v1/swagger.json -g typescript-fetch -o .\src\Api\ --additional-properties=supportsES6=true
In primul rand, trebuie sa aveti bine pusa la punct teoria si practica discutata la urmatoarele materii:
Motivele sunt urmatoarele:
Codul poate fi accesat pe linkul nostru de gitlab.
Asa cum am spus la inceputul anului, aveti un canal special dedicat pentru Discutii de laborator. Orice intrebare aveti cu privire la laborator, va rog sa o puneti acolo, pentru a putea sa va lamurim.
Multa lume considera ca backendul este pur si simplu o colectie de rute expuse de un server, prin care acesta primeste informatii si furnieaza informatii. Pe scurt, un blackbox.
Un backend este acea componenta software dintr-o aplicatie care coordoneaza business logic-ul unei aplicatii. Chiar daca utilizatorii interactioneaza cu frontendul, frontendul poate efectua doar actiunile care sunt permise de backend. Asadar, backendul reprezinta nucleul logicii de domeniu a oricarei aplicatii.
Backendurile pot fi scrise in multe moduri. Noi ne vom axa pe clasicul Web API REST.
Cea mai simpla structura pe care puteam sa o folosim este 3-tier architecture. Asa cum a fost prezentat la curs, este cea mai simpla de folosit, dar si cea mai simpla de transformat in dezastru.
Am ales sa construim codul folosind o combinatie intre Clean Architecture, Tactical Domain Driven Design si Vertical Slicing. Motivatia pentru fiecare alegere este urmatoarea:
Fiecare nivel din Clean este reprezentat de cate un proiect de .NET (.csproj). Proiectele sunt toate scrise sub umbrela unei solutii .NET (.sln).
Pentru a adauga un proiect nou intr-o solutie existenta, click dreapta pe solutie → Add New Project
Nivelul Core este cel mai important, deoarece descrie logica de domeniu din proiect. Aici reprezentam toate deciziile de business care se iau in aplicatia noastra. Este primul nivel pe care l-am scris.
Observati ca avem 3 foldere principale:
Clasele si interfatele scrise in SeedWork sunt folosite pentru a limita libertatea programatorului si pentru a reduce duplicarea codului. Chiar daca are conotatie negativa, acest lucru aduce beneficii, deoarece standardieaza modul in care cod nou este adaugat in aplicatie. Deoarece C# este un limbaj OOP, am profitat la maxim de capabilitatile de mostenire si genericitate pe care ni le ofera.
Entity reprezinta o clasa de baza pentru entitatile din baza de date. In aceasta clasa avem definite cheia primara (Id), si 2 campuri pentru audit: createdAt si updatedAt.
public abstract class Entity { public int Id { get; set; } public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } }
IAggregateRoot este o interfata goala, cu rol de marker. Aceasta va marca obiectele care sunt AggregateRoot (Tactical DDD).
public interface IAggregateRoot { }
DomainOfAggregate este o clasa abstracta, care primeste ca argument generic un IAggregateRoot. Rolul sau este sa fie un blueprint pentru cele doua obiecte de domeniu cu care lucram, BooksDomain si UsersRentalsDomain.
public abstract class DomainOfAggregate<TAggregate> where TAggregate : Entity, IAggregateRoot { private protected readonly TAggregate aggregate; public DomainOfAggregate(TAggregate aggregate) { this.aggregate = aggregate; } }
ICreateAggregateComand este o interfata marker pentru comenzile care creaza obiecte de domeniu.
public interface ICreateAggregateCommand { }
IRepositoryOfAggregate este o interfata generica, ce accepta ca argumente un IAggregateRoot si o ICreateAggregateCommand. Aceasta interfata expune cele 3 metode de care avem nevoie in repository:
public interface IRepositoryOfAggregate<TAggregate, TAggregateCreateCommand> where TAggregate : Entity, IAggregateRoot where TAggregateCreateCommand : ICreateAggregateCommand { Task AddAsync(TAggregateCreateCommand command, CancellationToken cancellationToken); Task<DomainOfAggregate<TAggregate>?> GetAsync(int aggregateId, CancellationToken cancellationToken); Task SaveAsync(CancellationToken cancellationToken); }
Aici am definit clasele care reprezinta datele din sistemul nostru. Aceste clase sunt simple, fara functionalitate. Ele vor fi folosite de sistemul de configurare al EntityFramework.
In plus, entitatile DDD mostenesc clasa abstracta Entity, in timp ce radacinile de agregat mostenesc si interfata IAggregateRoot.
public class Books : Entity, IAggregateRoot { public Books(string name, string author, string genre, int maximumDaysForRental, bool isBooked = false) { Name = name; Author = author; Genre = genre; MaximumDaysForRental = maximumDaysForRental; IsBooked = isBooked; } public string Name { get; set; } public string Author { get; set; } public string Genre { get; set; } public int MaximumDaysForRental { get; set; } public bool IsBooked { get; set; } public virtual ICollection<Keywords> Keywords { get; set; } = new List<Keywords>(); }
Avem si un value object, si anume Keywords. Odata cu .NET 6, putem folosi tipul record pentru a descrie value objects in Entity Framework. Diferenta intre class si record este ca record este imutabil.
public record Keywords { public Keywords(string name, string description) { Name = name; Description = description; } public string Name { get; init; } public string Description { get; init; } }
Cel mai important folder, aici avem business logic-ul aplicatiei impartit intre cele doua radicini de agregat, books si users rentals.
Fiecare dintre cele doua sub foldere are toate informatiile necesare pentru desfasurarea business logic-ului aferent. Cel mai simplu, books, are doar 3 clase:
public class BooksDomain : DomainOfAggregate<Books> { public BooksDomain(Books aggregate) : base(aggregate) { } public void UpdateBook(string name, string author, string genre, int maximumDaysForRental, ICollection<Keywords> keywords) { aggregate.Name = name; aggregate.Author = author; aggregate.Genre = genre; aggregate.MaximumDaysForRental = maximumDaysForRental; aggregate.Keywords = keywords; } public bool BookCanBeRented(int rentalDays) => !aggregate.IsBooked && rentalDays <= aggregate.MaximumDaysForRental; public void MarkBookAsRented() => aggregate.IsBooked = true; public void MarkBookAsAvailable() => aggregate.IsBooked = false; }
public interface IBooksRepository : IRepositoryOfAggregate<Books, InsertBookInLibraryCommand> { public Task DeleteBookAsync(Books book, CancellationToken cancellationToken); }
public record InsertBookInLibraryCommand : ICreateAggregateCommand { public string Name { get; init; } public string Author { get; init; } public string Genre { get; init; } public int MaximumDaysForRental { get; init; } public IEnumerable<KeyWordDto> Keywords { get; init; } = new List<KeyWordDto>(); public InsertBookInLibraryCommand(string name, string author, string genre, int maximumDaysForRental, IEnumerable<KeyWordDto> keywords) { Name = name; Author = author; Genre = genre; MaximumDaysForRental = maximumDaysForRental; Keywords = keywords; } } public record KeyWordDto (string Name, string Description);
Acest fisier are definit cele doua records care reprezinta comanda efectiva si DTO-ul pentru Keywords.
Dupa ce am terminat de scris nivelul core, am scris nivelul de infrastructura. Rolul acestui nivel este sa se ocupe de infrastructura proiectului, precum baze de date, sisteme de logging, etc….
In cadrul proiectului nostru, in acest nivel ne ocupam doar de baza de date, adica de configurarea entitatilor (de EF) si de implementarea celor 2 repositories.
Asa cum am spus initial, singura depedenta pe care o are nivelul de infrastructura este asupra nivelului Core:
Cel mai important fisier din acest nivel, reprezinta o implementare a DbContext, care reprezinta clasa esentiala de functionare a EntityFramework.
public class BookLibraryContext : DbContext { public BookLibraryContext(DbContextOptions<BookLibraryContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfigurationsFromAssembly(typeof(BooksConfiguration).Assembly); } public DbSet<Users> Users => Set<Users>(); public DbSet<Books> Books => Set<Books>(); public DbSet<Rentals> Rentals => Set<Rentals>(); }
Contextul va fi folosit in nivelul Api pentru a interactiona cu baza de date in cazul citirilor, cat si nivelul infrastructure in implementarea repository-urilor.
Aici am definit configuratiile pentru baza de date, sub forma de clase de configurare. Aceste clase de configurare sunt interpretate de EntityFramework, datorita
modelBuilder.ApplyConfigurationsFromAssembly(typeof(BooksConfiguration).Assembly);
scris in context.
O clasa de configurare trebuie sa mosteneasca IEntityTypeConfiguration pentru o anumita entitate si sa implementeze metoda Configure.
internal class BooksConfiguration : EntityConfiguration<Books> { public override void Configure(EntityTypeBuilder<Books> builder) { builder .Property(x => x.Name) .IsRequired(); builder .Property(x => x.Genre) .IsRequired(); builder .Property(x => x.Author) .IsRequired(); builder .OwnsMany(x => x.Keywords); base.Configure(builder); } }
internal abstract class EntityConfiguration<TEntity> : IEntityTypeConfiguration<TEntity> where TEntity : Entity { public virtual void Configure(EntityTypeBuilder<TEntity> builder) { builder .Property(x => x.CreatedAt) .ValueGeneratedOnAdd(); builder .Property(x => x.UpdatedAt) .ValueGeneratedOnAddOrUpdate(); } }
Aici am definit implementarile interfetelor de repository, declarate in Core.
Asa cum am spus mai sus, repository-urile noastre doar adauga un agregat in sistem, il returneaza, si salveaza starea sistemului.
public class BooksRepository : IBooksRepository { private readonly BookLibraryContext context; public BooksRepository(BookLibraryContext context) { this.context = context; } public async Task AddAsync(InsertBookInLibraryCommand command, CancellationToken cancellationToken) { var keywords = command.Keywords.Select(x => new Keywords(x.Name, x.Description)).ToList(); var book = new Books(command.Name, command.Author, command.Genre, command.MaximumDaysForRental); book.Keywords = keywords; await context.Books.AddAsync(book, cancellationToken); await SaveAsync(cancellationToken); } public async Task<DomainOfAggregate<Books>?> GetAsync(int aggregateId, CancellationToken cancellationToken) { var book = await context.Books .FirstOrDefaultAsync(x => x.Id == aggregateId, cancellationToken); if (book == null) { return null; } return new BooksDomain(book); } public async Task DeleteBookAsync(Books book, CancellationToken cancellationToken) { context.Books.Remove(book); await SaveAsync(cancellationToken); } public Task SaveAsync(CancellationToken cancellationToken) { return context.SaveChangesAsync(cancellationToken); } }
Migrarile reprezinta un snapshot al bazei de date pe baza configurarii din cod (context + entity configurations). Vom discuta la un laborator urmator despre asta.