Interakcje2 W poprzednim wpisie omówiliśmy szablon, a teraz zaczniemy go przerabiać dodając nową usługę REST API w ASP.NET CORE i nowe widoki w Angular. Zanim jednak do tego przejdziemy musimy omówić pewną pułapkę związaną z przechowywaniem plików statycznych w cache. Powiemy też o konfiguracji w ASP.NET CORE.
O co chodzi?
Jak pamiętasz utworzyliśmy prosty plik HTML w folderze wwwroot. Uruchamiając serwer pod adresem : https://localhost:44382/hello.html zobaczysz następujący widok
Teraz co się stanie, gdy zmienimy ten plik HTML.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
JESTEM PRZED MVC I SPA
<p>To jest test app.UseStaticFiles();</p>
<p>A co z Cache?</p>
</body>
</html>
Tutaj możesz zobaczyć różne rezultat. Wszystko zależy, od jakiej przeglądarki korzystasz i jakie ona ma ustawienia statycznego cachowania plików oraz tego, czy Visual Studio w tle nie przebudowuje plików.
Na chwilę obecnego żadnego cache nie zadeklarowaliśmy w kodzie w C# więc każda zmiana kodu w statyczny pliku powinna być widoczna. Powiem Ci jednak, że programując w ASP.NET CORE miałem za dużo przygód z cachowaniem statycznych plików i miałem czasem wrażenie, że te zachowanie jest losowe.
Później jednak się okazało, że jeden z middleware robił Cache bez mojej wiedzy i potem się dziwiłem,że dopiero po twardy odświeżeniu strony widzę swoje zmiany.
Caching statycznych plików jest czymś wspaniałym dla serwerów produkcyjnych. Jednakże nie chcesz tego mieć, gdy budujesz aplikację. Nie chcesz marnować 2-3 minut nad myśleniem czy nowy plik styli CSS jest już załadowany, czy w cache siedzi stary. Jest to równie ważne, gdy mówimy tutaj o Angularze, a on przecież operuje na statycznych plikach.
Właśnie, a jak to jest z Angularem? Z każdym razem, gdy wprowadzisz zmianę do plików .ts serwer w pamięci AngularCliServer automatycznie przebuduje pliki za Ciebie. Sprawa jednak nadal się komplikuje z innymi statycznymi plikami jak HTML, favicons, obrazkami , stylami CSS
Dlatego warto to ustawić by mieć pewność, że mamy pełną kontrolę nad tym zachowaniem
W czasach ASP.NET 4 wyłączenie cache w pliku web.config wygląda tak :
<caching enabled="false" />
<staticContent>
<clientCache cacheControlMode="DisableCache" />
</staticContent>
<httpProtocol>
<customHeaders>
<add name="Cache-Control" value="no-cache, no-store" />
<add name="Pragma" value="no-cache" />
<add name="Expires" value="-1" />
</customHeaders>
</httpProtocol>
Później dzieliłeś plik na web.config i na web.debug.config.
To były jednak stare czasy. Jak kto zrobić ASP.NET CORE. Mamy dwa pliki konfiguracyjne i w zależności od środowiska inny plik się załaduje. Nie chcemy umieszczać statycznej konfiguracji wewnątrz kodu.
Najpierw dodajmy parametry do pliku appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"StaticFiles": {
"Headers": {
"CacheControl": "max-age=3600",
"Pragma": "cache",
"Expires": null
}
}
}
Na produkcji warto ustawić cache. Natomiast na środowisku pracy chcemy włączyć cache całkowicie więc dodajemy takie parametry do pliku appsettings.Development.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"StaticFiles": {
"Headers": {
"CacheControl": "no-cache, no-store",
"Pragma": "no-cache",
"Expires": "-1"
}
}
}
Później w pliku Startup.cs w metodzie Configure i deklaracji UseStaticFile ustawiamy cache w taki sposób.
Teraz przy każdym zapytaniu o pliku statycznym przeglądarki otrzymają odpowiednie nagłówki z informacją jak mają przechowywać pliki.
app.UseStaticFiles(new StaticFileOptions()
{
OnPrepareResponse = (context) =>
{
// Retrieve cache configuration from appsettings.json
context.Context.Response.Headers["Cache-Control"] =
Configuration["StaticFiles:Headers:Cache-Control"];
context.Context.Response.Headers["Pragma"] =
Configuration["StaticFiles:Headers:Pragma"];
context.Context.Response.Headers["Expires"] =
Configuration["StaticFiles:Headers:Expires"];
}
});
Co możemy zrobić tutaj lepiej. Za każdym razem, gdy chcemy wyjmować parametry konfiguracyjne korzystamy z obiektu IConfiguration i wpisujemy skomplikowany napis adresujący się do specyficznej wartości.
Jeśli piszesz prostą aplikację to moim zdaniem tyle wystarczy. W prawdziwych aplikacjach biznesowych na podstawie moich doświadczeń w firmie ubezpieczeniowej mogę Ci powiedzieć, że taki parametrów konfiguracyjnych może być co najmniej 300.
Możesz sobie wyobrazić bałagan, który może powstać przy takiej ilości parametrów. Jak więc można to napisać lepiej. Przydałby by się tutaj klasy reprezentujące tą konfigurację.
Są na to, aż 3 sposoby. Można skorzystać z interfejsu IOptions<T>. Można stworzyć swoją własną klasę i uniknąć IOptions<T>. Możesz też stworzyć singleton w trakcie konfiguracji wstrzykiwania zależności.
Rozwiązanie ma być jednak przejrzyste i przyjęło się, że rozwiązanie IOptions jest najbardziej popularne.
Do projektu dodałem następującą klasę. Ma ona trzy właściwości :
public class StaticFilesConfiguration
{
public string CacheControl { get; set; }
public string Pragma { get; set; }
public string Expires { get; set; }
}
Teraz w metodzie ConfigureServices deklaruje mapowanie tej klasy na odpowiednią sekcję w konfiguracji.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<StaticFilesConfiguration>
(Configuration.GetSection("StaticFiles:Headers"));
Teraz będę tej konfiguracji potrzebował w metodzie Configure. Dodaje więc typ generyczny IOptions jako parametry do metody Configure. ASP.NET CORE za mnie wstrzyknie definicję z mapowanej klasy.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env,
IOptions<StaticFilesConfiguration> options)
{
Teraz deklaruje użyje tej klasy w ustawieniach middleware UseStaticFiles.
app.UseStaticFiles(new StaticFileOptions()
{
OnPrepareResponse = (context) =>
{
StaticFilesConfiguration headers = options.Value;
context.Context.Response.Headers["Cache-Control"] =
headers.CacheControl;
context.Context.Response.Headers["Pragma"] =
headers.Pragma;
context.Context.Response.Headers["Expires"] =
headers.Expires;
}
});
Jestem na środowisku pracowniczym więc otrzymam parametry z pliku appsetting.Development.json
Teraz możesz przetestować jak cache działa. Możesz wymusić inne środowisko pracy taki kodem.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env,
IOptions<StaticFilesConfiguration> options)
{
env.EnvironmentName = "Production";
Sam parametr możesz także zmienić w ustawieniach projektu.
Gdy będziesz publikować aplikację na produkcję ten parametr automatycznie się zmieni.
Bierzemy się za Angulara
Zacznijmy od skasowania Compomentów Angulara, które nie będą nam potrzebne. Myślę, że to będzie dobry trening dla Ciebie.
Kasujemy folder counter i folder fetch-data. Visual Studio, jeżeli nie ma czkawki to powinno Ci zasygnalizować błędy w TypeScript. W końcu odwołujemy się, do których plików już nie ma.
Jeżeli nie masz błędów nie panikuj. Visual Studio może mieć problem z komunikacją z elementami Angulara.
Wszystkie te błędy znajdują się w pliku app.module.ts. Tam są referencję do wszystkich plików TypeScript używanych przez Angulara. Błędy powinny być podkreślone na czerwono.
Usuwamy więc te linijki kodu. Najpierw z sekcji import usuwamy deklarację tych Komponentów. Później usuwamy deklarację w @NgModule. Usuwamy także z RouterMoudle ścieżki do tych Componentów.
Oto kod przed:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { NavMenuComponent } from './nav-menu/nav-menu.component';
import { HomeComponent } from './home/home.component';
import { CounterComponent } from './counter/counter.component';
import { FetchDataComponent } from './fetch-data/fetch-data.component';
@NgModule({
declarations: [
AppComponent,
NavMenuComponent,
HomeComponent,
CounterComponent,
FetchDataComponent
],
imports: [
BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
HttpClientModule,
FormsModule,
RouterModule.forRoot([
{ path: '', component: HomeComponent, pathMatch: 'full' },
{ path: 'counter', component: CounterComponent },
{ path: 'fetch-data', component: FetchDataComponent },
])
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
i po:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { NavMenuComponent } from './nav-menu/nav-menu.component';
import { HomeComponent } from './home/home.component';
@NgModule({
declarations: [
AppComponent,
NavMenuComponent,
HomeComponent,
],
imports: [
BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
HttpClientModule,
FormsModule,
RouterModule.forRoot([
{ path: '', component: HomeComponent, pathMatch: 'full' },
])
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Przy okazji takie czyszczenie może Cię nauczyć jak dodawać nowe Komponenty do Angulara.
Co robi kod w AppModule
Moduły Angulara zwane także NgModules zostały przestawione w Angularze 2 RC5 i są one genialny sposobem na organizację aplikacji. Na pewno lepszym niż kontrolery w Angularze 1.X.X.
Każdy moduł może być zbiorem : Components (komponentów), directives (dyrektyw) i innych modułów.
Musi istnieć przynajmniej jeden moduł i tym modułem głównym jest AppModule.
Kod AppModule można rozbić na dwie części : Jedna to lista stwierdzeń import, która wskazuję na wszystkie referencję do plików TypeScript wymagane przez daną aplikację.
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { NavMenuComponent } from './nav-menu/nav-menu.component';
import { HomeComponent } from './home/home.component';
Jak widzisz mamy tutaj definicję do bazowego modułu NgModule, mamy tutaj moduł do formularzy i mamy tutaj moduł do wysłania zapytań HTTP. Nie zapomnijmy o module służącym do obsługi podstron w Angularze.
Później są nasze komponenty.W drugiej części tego pliku w definicji @NgModule mamy tablice, która zawiera w sobie zbiór :
- components (komponentów)
- directives, (dyrektyw)
- pipes (nie mam pojęcia jak to przetłumaczyć)
- modules, (modułów)
- provider (prowajderów - czyli polishonenglishon)
- i tak dalej
W parametrze bootstrap deklarujemy, od którego Komponentu ma się uruchomić aplikacja i jest to AppComponent.
@NgModule({
declarations: [
AppComponent,
NavMenuComponent,
HomeComponent,
CounterComponent,
FetchDataComponent
],
imports: [
BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
HttpClientModule,
FormsModule,
RouterModule.forRoot([
{ path: '', component: HomeComponent, pathMatch: 'full' },
{ path: 'counter', component: CounterComponent },
{ path: 'fetch-data', component: FetchDataComponent },
])
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Aktualizacja NavMenu
To nie koniec zmian. Naszych Componentów już nie ma, ale wciąż są do nich linki w pasku nawigacyjnym.
Idziemy więc teraz do pliku "/ClientApp/app/components/nav-menu/navmenu.component.html" gdzie znajduję się układ naszego paska nawigacyjnego.
<li class="nav-item" [routerLinkActive]="['link-active']">
<a class="nav-link text-dark" [routerLink]="['/counter']"
>Counter</a
>
</li>
<li class="nav-item" [routerLinkActive]="['link-active']">
<a class="nav-link text-dark" [routerLink]="['/fetch-data']"
>Fetch data</a
>
</li>
Znajdujemy elementy listy, które odwołują się do tych skasowanych komponentów i po skasowaniu mamy już jest poprawną listę hiperlinków.
Teraz zmienimy stronę tytułową naszej strony by było to coś więcej niż Hello World. Idziemy do pliku /ClientApp/src/app/components/home/home.component.html
<h1>Zobacz Losową książkę</h1>
<p>Nie wiesz co przeczytać!</p>
<p>Ja Ci wylosuję dobrą książkę</p>
<p>Odmienię twoje życie</p>
Zmieniamy HTML-a i zaraz po zapisie Angular powinien się odświeżyć w twojej przeglądarce.
Teraz po stronie back-end kasujemy plik w Controllers/WeatherForecast.cs
Jesteśmy teraz gotowi na stworzenie swojej własnej aplikacji. Teraz gdy mamy minimalną, ale działającą aplikację .NET CORE 3.2 i Angular 9 możemy ją odrobinie rozbudować.
Interakcja pomiędzy Back-end, a Front-end
Skupimy się na interakcji pomiędzy Back-end, a Front-Endem. Zobaczymy jak stworzyć obsługę zapytania HTTP do ASP.NET Core przy użyciu kontrolerów. Przykład będzie bardzo prosty byśmy się nie zgubili.
Najpierw muszę dodać nowy kontroler. Klikam więc na folder i z opcji wybieram Add -> Controller...
W ASP.NET CORE kontrolery mogą robić mnóstwo rzeczy dlatego w następnym oknie masz tyle opcji i szablonów. Możesz np. utworzyć kontroler już z gotowym widokiem .cshtml tylko nam to nie jest potrzebne.
Wybierz opcję MVC Controller - Empty
Każdy kontroler musi mieć w nazwie słowo Controller. Dlatego swój kontroler nazywam BookController. Jak się domyślisz chce zwrócić losową książkę dla użytkownika.
Wciskasz OK i masz następujący kod przed sobą.
public class BookController : Controller
{
public IActionResult Index()
{
return View();
}
}
Ten domyślny kod chce zwrócić widok, ale my nie pracujemy na widokach więc trzeba ten kod zmienić.
Chcemy zwracać dane w formacie JSON. Będzie to notacja, która będzie zrozumiała dla Angulara.
Chcemy też coś zrobić ze ścieżką do naszego kontrolera. Jak widzisz domyślnie ścieżka idzie tak : {nazwa kontrolera bez słowa controller}/{akcja}
Wynika to z tego ustawiania w klasie startup.cs
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
Czy możemy nadpisać to ustawienie nie grzebiąc w tych domyślnych ustawieniach? No oczywiście, że tak. Przy pomocy atrybutów możemy powiedzieć, że dany kontroler powinien pod innym adresem.
[Route("api/[controller]")]
public class BookController : Controller
Możemy np. powiedzieć by ten kontroler pracował pod adrsem : api/{nazwa kontrolera bez słowa controller}/{akcja}
Ja jednak chce pozbyć się słowa book więc umieszczam taki kod:
[Route("api")]
public class BookController : Controller
Teraz jak zwrócić JSON-a w tym kontrolerze? Najpierw stworzę klasę reprezentującą książkę. Utworzyłem więc sobie folder "Models" a w nim dodałem klasę Book
Klasa ta ma tylko dwa pola : Tytuł książki i jego autora.
public class Book
{
public string Title { get; set; }
public string Author { get; set; }
}
W kontrolerze zmieniam nazwę metody, dodaje instancje klasy book, oraz zwracam nowy typ akcji, jakim jest JsonResult.
[Route("api")]
public class BookController : Controller
{
public IActionResult Random()
{
Book book = new Book();
book.Author = "Bibeault Katz";
book.Title = "jQuery in Action";
return new JsonResult(book);
}
}
Sprawdźmy teraz czy moje prymitywne REST API działa. Pod adresem api/Random powinien zostać zwrócony JSON. Możesz zauważyć https://localhost:44382/api będzie działać, ale nie https://localhost:44382/api/random
Dlaczego? Po deklaracji atrybutu Route na samym początku kontrolera oczekuje on także podania nazwy dla każdej metody. Ten przykład akurat działa, ponieważ domyślnie Route bierze pierwszą metodę z kontrolera i zwraca wyniki.
Jednakże przy większej ilości metod zacznie on wrzucać wyjątek.
Dlatego musimy podać ścieżkę też, dla której nasze metody zostaną obsłużone. Oto przykład jak ten problem rozwiązać. Stworzyłem metodę, która niby zwróci losową książkę oraz metodę, która zwróci wszystkie książki.
[Route("api")]
public class BookController : Controller
{
[Route("")]
public IActionResult Random()
{
Book book = new Book();
book.Author = "Bibeault Katz";
book.Title = "jQuery in Action";
return new JsonResult(book);
}
[Route("all")]
public IActionResult AllBooks()
{
List<Book> list = new List<Book>();
Book book = new Book();
book.Author = "Bibeault Katz";
book.Title = "jQuery in Action";
Book book2 = new Book();
book2.Author = "Robin Sen";
book2.Title = "Android w Akcji";
list.Add(book); list.Add(book2);
return new JsonResult(list);
}
}
Sprawdźmy każdy adres tej metody.
Mamy już gotowy przykład po stronie back-end. Chce ci jednak jeszcze pokazać inny sposób na stworzenie pod pewnym adresem metody, która zwróci JSON, a jest nim middleware.
Middleware, jako alternatywa?
Jak pamiętasz middleware to są te komponenty które deklarujemy w potoku w metodzie Configure w pliku. Startup.cs
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});
Jak widzisz ponownie one określają także jakie zachowanie ma być pod jaką ścieżką. Przykładowo tutaj mówie o obsłudze MVC, a zaraz potem obsługuję aplikacja SPA Angular.
Nic nie stoi na przeszkodzie abyś mógł napisać swój middleware i go uruchomić w tym potoku.
public class RequestBookMiddleware
{
private readonly RequestDelegate _next;
public RequestBookMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
if (context.Request.Path.StartsWithSegments(new PathString("/healthtest")))
{
Book book = new Book();
book.Author = "Bibeault Katz";
book.Title = "jQuery in Action";
context.Response.StatusCode = 200;
context.Response.ContentType = "application/json";
string jsonString = JsonConvert.SerializeObject(book);
await context.Response.WriteAsync(jsonString, Encoding.UTF8);
// to stop futher pipeline execution
return;
}
// Call the next delegate/middleware in the pipeline
await _next.Invoke(context);
}
}
Oto przykład middleware, który zwróci książkę pod adresem /healthtest. Jeśli będzie inna ścieżka to przesyłam zapytanie HTTP dalej do potoku. Pozostaje go dodać do potoku i masz alternatywny styl obsługi zapytań HTTP.
app.UseMiddleware(typeof(RequestBookMiddleware));
Tworzenie jednak swoich middleware zostawić na bardziej zaawansowane przykłady. Może chciałbyś stworzyć middleware, który modyfikuje odpowiednio zapytanie HTTP.
Zdecydowanie styl MVC , jeśli chodzi o budowanie API REST-owych, które zwraca JSON jest dużo lepsze. Przynajmniej teraz masz dodatkowy wgląd jak mechanizm potoków zapytań HTTP działa ASP.NET CORE z middleware i kontrolerami.
Odebrać zapytanie Angularem
Tyle, jeżeli chodzi o Back-end, a teraz jak te JSON złapać i obsłużyć w Angularze?
Najpierw musi stworzyć nowy Component, który wyświetli na ten widok. Jak pamiętasz będzie folder składający się z trzech plików.
Component : plik .ts napisany w TypeScript który będzie zawierał komponent klasy, jak i referencję do wszystkich moduli , funkcji i zmiennych, które będą tam występować.
Szablon albo Template : plik .html, który jest rozszerzeniem składni Angular Template Syntax. Definiuje on UI naszego komponentu.
Style : plik .css, który będzie zawierały style przeznaczone tylko do tego komponentu
Jeżeli jesteś przystosowanych do wzorców model-view-controller MVC lub model-view-viewmodel MVVM to możesz powiedzieć, że Component Angulara to wzorzec Controller/viewmodel gdzie szablon HTML reprezentuje viewmodel
Tworzymy więc folder, a w nim następujące pliki.
- random-book.component.ts
- random-book.component.html
- random-book.component.css
Visual Studio oferuje szablony do każdego z tych plików więc nie widzę byś miał jakikolwiek problem:
Mamy foldery więc pora je uzupełnić
random-book.component.ts
Oto kod źródłowy :
import { Component, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-random-book',
templateUrl: './random-book.component.html',
styleUrls: ['./random-book.component.css']
})
export class RandomBookComponent {
public result: Book;
constructor(
private http: HttpClient,
@Inject('BASE_URL') private baseUrl: string) {
}
ngOnInit() {
this.http.get<Book>(this.baseUrl + 'api').subscribe(result => {
this.result = result;
}, error => console.error(error));
}
}
interface Book {
name: string;
author: string;
}
Co tutaj się dzieje :
Na początku pliku deklaruje import a tam mówie, z jakich directives, pipes, services i innych Components będę korzystał.
Konstruktorze mojego komponentu, tworzy instancje service HttpClient. W nim także w zmiennej baseUrl robię wstrzykiwanie zależności . BASE_URL znajduje się w pliku /ClientApp/src/main.ts. Lepiej mieć ustawienie w jednym miejscu i w każdym komponencie. BASE_URL określa adres bazowy URL naszej aplikacji. To tego bazowego adresu dodam słowo "api" abyśmy pobrali książkę.
Na końcu kodu też widzisz definicje książki w TypeScript. Odpowiedź JSON zostanie zmieniona na obiekt JavaScript tego interfejsu. Serializacja wykonuje się automatycznie.
Zanim pójdziemy dalej musimy omówić następujące tematy:
- Imports
- Dependency Injection
- ngOnInit
- Konstruktor
- HttpClient
- Observables
- Interfejsy
Importy modułów
Określenia import służą do pobrania innych modułów JavaScript. Koncepcja modułów pojawiła się w ECMAScript (JavaScript) 2015 roku i zostało przyjęte także przez TypeScript
Moduł Angulara więc jest bazowo kolekcją zmiennych, funkcji, klas zgrupowanych w innej klasie. Każdy moduł ma swoją własną przestrzeń (scope) . Czyli elementy są widoczne, tylko wewnątrz swojego modułu. No, chyba że użyjesz słowa export to wtedy jasno mówisz, że dany element może być zaimportowany gdzieś indziej.
Aby skorzystać z zmiennych, funkcji,klas, interfejsów w danym module to musisz użyć słowa import.
Jest to jak przestrzeń nazw w C# przy użyciu słowa using.
Dodatkowo jak widzisz sam Angular jest taki modułem, który muszę importować. Łatwo je rozpoznać, ponieważ zaczynają się one od @.
Gdybyś chciał poczytać jak ten dekorator działa tutaj masz link :
https://angular.io/guide/ngmodules
A tutaj masz więcej informacji na temat architektury NgModule i architektury modułów w JavaScript
https://angular.io/guide/architecture-modules#ngmodules-and-javascript-modules
Wstrzykiwanie zależności, czyli Dependency injection
Mówiliśmy o wstrzykiwaniu zależności już wcześniej. Ta technika występuje w końcu i ASP.NET CORE, jak i w Angularze.
Czym jest Dependency injection? Ta technika występuje w klasach. Klasy potrzebują usług czy obiektów do swojego działania. Aby mieć programowanie modułowe i oddzielone od siebie niestety nie możemy pisać takiego kodu:
public TestClass() {
var object = new Element();
object.doStuff();
}
Tworzenie obiektu w klasie samo sobie nie jest niczym zły, ale tworzy problem, gdy wiele klas tworzy tak zmienne, obiekty. Zmiana kodu w jednym miejscu zrobiłaby lawinę bałaganu. Działanie klasy TestClass jest powiązane z klasą Element zbyt bezpośrednio.
Teraz jak by to wyglądało w przypadku wstrzykiwania zależności. Zamiast tworzenia instancji elementu możemy go przesłać do konstruktora.
public TestClass(Element el) {
el.doStuff();
}
Inny mechanizm, inna klasa zajmie tym co tam dokładnie ma się znajdować.
W Angularze mechanizm Dependency injection daje odpowiednie parametry do klasy w czasie jej tworzenia. W przykładzie z klasą RandomBookComponent użyliśmy tej techniki do wstrzyknięcia instancji usługi HttpClient i zmiennej baseUrl.
Będziemy mieć do nich dostęp w całej klasie. Są one w polach private więc tylko w naszej klasie skorzystamy z tych instancji. Podobnie jest w C#. Co jest prywatne jest prywatne.
ngOnInit
ngOnInit jest to metoda, która uruchomi się, gdy klasa RandomBookComponent jest tworzona. Każdy Component w Angularze ma swój cykl życia i do niego są także odpowiednie metody. Mamy np. metody, gdy HTML jest niszczony lub tworzony.
Można to przyrównać do zdarzeń w C#. Oto ich lista:
- ngOnChanges() : Odpowiada, gdy Angular ustawia bądź resetuje dane powiązane z elementami widoku HTML. W tej metodzie znajduje się obiekt SimpleChanges, który określa obecny i poprzedni stan danej właściwości. Wywołany przed ngOnInit i za każdym razem, gdy dana z powiązana elementem widoku HTML się zmienia
- ngOnInit() : Uruchamia się przy tworzeniu komponentu, gdy wykonało się pierwsze powiązanie danych z widokiem
- ngDoCheck() : Uruchamia się przy zmianie poza Angularem. Możesz ją wywołać wewnątrz metody ngOnChanges() i ngOnInit() i po nich się ona uruchomi.
- ngAfterContentInit() : Odpowiada, gdy zawartość zostanie wczytana do końca
- ngAfterContentChecked() : Odpowiada po sprawdzeniu zawartości przez Angular.
- ngAfterViewInit() : Odpowiada, gdy widok i wszystkie inne widoki po drodze zostaną załadowane
- ngAfterViewChecked : Gdy Angular sprawdzi widok i inne widoki po drodze wtedy ta metoda się uruchomi
- ngOnDestory() : Gdy Angular pozbywa się danego komponentu wtedy uruchamia się ta metoda. Dzieje się to przy zmianie strony w SPA w Angularze
Te wszystkie zdarzenia mogą zostać użyte przez Ciebie.
Dlaczego korzystamy z HttpClient w zdarzeniu ngOnInit(),żeby to zrozumieć musi omówić konstruktor.
Konstruktor klasy
Wszystkie klasy w TypeScript mają swój konstruktor i jest to metoda constuctor(). Angular najpierw go wywoła,a potem będziemy mogli przejść do cyklu zdarzeń Angular danego komponentu.
Konstruktorze, więc najpierw wykonany wstrzykiwanie zależności, a potem te obiekty będą już istnieć dla metody ngOnInit(). Dodatkowo warto zaznaczyć, że nie możesz wykonać wstrzykiwania zależności w cyklu zdarzeń. Angular nie ma obsługi takie funkcjonalności.
HttpClient
Musimy mieć możliwość wysłania i odbierania danych JSON do naszych kontrolerów w ASP.NET CORE. HttpClient pojawił się po raz pierwszy w Angularze 4.3.0-RC0. Nowa implementacja HttpClient znana jako @angular/http zastąpiła starą funkcjonalność i tymczasowo była ona w innej paczce. My piszemy aplikację od zera wiec nie jest to nasze zmartwienie.
Stare API Angulara miała wiele ograniczeń:
- JSON nie był domyślnym formatem. Trzeba było jawnie ustawiać nagłówki i parsować JSON-a.
- Nie było łatwego dostępu do potoku zapytań i odpowiedzi HTTP.
- Nie było natywnego sposobu na przesyłanie obiektów jako zapytań HTTP. Otrzymywać obiekty bezpośrednio też nie mogłeś. Chociaż mogłeś rzutować JSON-a na interfejs.
Dobra wiadomość jest taka, że nowe API potrafi to wszystko. Testowanie i łapanie błędów też jest dużo łatwiejsze.
Observables
Observables to potężna funkcjonalność, która pomaga przy danych asynchronicznych. Jest to kręgosłup biblioteki RxJs (ReactiveX JavaScript). Jest ona wymaga w Angularze . Planuje się jej dodanie na stałe do ECMAScript 7. Są to lepsze obietnice w JavaScript.
Observables mogą być konfigurowane do wysłania wartości, wiadomości,zdarzeń, synchronicznie i asynchronicznie. Te wartości możesz otrzymać poprzez subskrypcję do obiektu Observable.
Spójrzmy na ten kod jeszcze raz:
ngOnInit() {
this.http.get<Book>(this.baseUrl + 'api').subscribe(result => {
this.result = result;
}, error => console.error(error));
}
get<Book>() : pobierze książkę z API w ASP.NET CORE. Metoda subscribe() tworzy obiekt Observable, który może zrobić dwie akcję w zależności od tego, czy dane zapytanie zakończy się sukcesem, czy błędem. To wszystko dzieje się asynchronicznie i zostanie uruchomione w innym wątku. Reszta aplikacji będzie działać nadal.
Interfejsy
Teraz gdy wiemy jak HttpClient działa pytanie brzmi po co nam są interfejsy. Dlaczego nie -czysty JSON jako anonimowy obiekt JavaScript? Odpowiedź jest prosta, bo to zły pomysł:
Używamy TypeScript-a, ponieważ chcemy korzystać z definicji obiektów, a nie z anonimowych obiektów JavaScript. Wiemy z czego ma się składać ksiażka
Anonimowe obiekty ciężko sprawdzić. Jak masz sprawdzić jakiej właściwości brakuje w obiekcie, gdy nie masz jego szkieletu.
Anonimowe obiekty nie mogą być użyte ponownie
W prawdziwej aplikacji biznesowej nie moglibyśmy sobie pozwolić na utratę kontroli nad typem, jakim operujemy. Interfejsy więc górą
random-book.component.html
Teraz pora na obsługę widoku.
<h1>Losowa książka</h1>
<p>Oto rezultat:</p>
<p *ngIf="!result"><em>Loading...</em></p>
<div *ngIf="result">
<div>Tytuł : {{ result.name }}</div>
<div>Autor : {{ result.author}}</div>
</div>
ngIf jest strukturalną dyrektywą, która mówi czy stworzyć dany element HTML w zależności od warunku logicznego. Może być łączony z wyrażeniem else, jeśli if istnieje. Na początku nasz obiekt książki będzie pusty. Bycie undefined w JavaScript oznacza wartość false dla tego ngIf pocztkowo nie wyświetli naszych pól.
Dopiero po pobraniu pokażemy tytuł oraz nazwę autora.
{{result.name}} , {{result.author}} : to interpolacje, które wyświetlą zawartość naszych zmiennych.
Na razie nie dodaje nowych styli CSS więc zmiany tutaj się kończą. Pora na dodanie komponentu do Angulara. Trzeba dodać go w plikach :
- app.module.ts
- nav-menu.component.html
Do AppModule dodajemy import z naszym nowym komponentem. Później dodajemy go do deklaracji w @NgModule, a na koniec wybieramy dla niego adres nawigacyjny "/random".
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { NavMenuComponent } from './nav-menu/nav-menu.component';
import { HomeComponent } from './home/home.component';
import { RandomBookComponent } from './random-book/random-book.component';
@NgModule({
declarations: [
AppComponent,
NavMenuComponent,
HomeComponent,
RandomBookComponent
],
imports: [
BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
HttpClientModule,
FormsModule,
RouterModule.forRoot([
{ path: '', component: HomeComponent, pathMatch: 'full' },
{ path: 'random', component: RandomBookComponent },
])
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Pozostało także dodać nowy link do naszego komponentu w nav-menu.component.html
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse"
[ngClass]="{ show: isExpanded }">
<ul class="navbar-nav flex-grow">
<li class="nav-item"
[routerLinkActive]="['link-active']"
[routerLinkActiveOptions]="{ exact: true }">
<a class="nav-link text-dark" [routerLink]="['/']">Home</a>
</li>
<li class="nav-item" [routerLinkActive]="['link-active']">
<a class="nav-link text-dark" [routerLink]="['/random']">Losuj</a>
</li>
</ul>
</div>
Gdy przetestujesz aplikacje powinieneś zobaczyć następujący widok.
W tym wpisie spędziliśmy trochę czasu z konfiguracją i budową kontrolerów Web API w ASP.NET CORE. Pokazałem także jak stworzyć własny middleware, który może także sterować zapytaniami HTTP.
Mając gotowe API skorzystaliśmy z Angulara by wyświetlić losową książkę. Oto jak wygląda komunikacja pomiędzy Angularem, ASP.NET CORE-m.
Analogicznie możesz szybko dodać kolejny widok, który pokaże wszystkie książki. Tworzysz kolejny folder, a w nim trzy pliki
Tworzysz następujący kod TypeScript.
import { Component, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-show-books',
templateUrl: './show-books.component.html',
styleUrls: ['./show-books.component.css']
})
export class ShowBooksComponent {
public result: Book[];
constructor(
private http: HttpClient,
@Inject('BASE_URL') private baseUrl: string) {
}
ngOnInit() {
this.http.get<Book[]>(this.baseUrl + 'api/all').subscribe(result => {
this.result = result;
}, error => console.error(error));
}
}
interface Book {
name: string;
author: string;
}
Tym razem rezultatem zapytania do API będzie kolekcja książek. Jak widzisz kod za bardzo się nie zmienił. W widoku HTML stworzę tabelkę i skorzystam z dyrektywy ngFor, która będzie tworzyć elementy według pętli.
<h1>Wszystkie książki</h1>
<p>Oto rezultat:</p>
<p *ngIf="!result"><em>Loading...</em></p>
<table class='table table-striped' aria-labelledby="tableLabel"
*ngIf="result">
<thead>
<tr>
<th>Tytuł</th>
<th>Author</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let book of result">
<td>{{ book.title }}</td>
<td>{{ book.author }}</td>
</tr>
</tbody>
</table>
W AppModule dodaje kolejny komponent do obsługi.
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { NavMenuComponent } from './nav-menu/nav-menu.component';
import { HomeComponent } from './home/home.component';
import { RandomBookComponent } from './random-book/random-book.component';
import { ShowBooksComponent } from './show-books/show-books.component';
@NgModule({
declarations: [
AppComponent,
NavMenuComponent,
HomeComponent,
RandomBookComponent,
ShowBooksComponent
],
imports: [
BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
HttpClientModule,
FormsModule,
RouterModule.forRoot([
{ path: '', component: HomeComponent, pathMatch: 'full' },
{ path: 'random', component: RandomBookComponent },
{ path: 'all', component: ShowBooksComponent },
])
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Pozostało dodać mi jeszcze klawisz nawigacyjny do strony ze wszystkim książkami.
<ul class="navbar-nav flex-grow">
<li class="nav-item"
[routerLinkActive]="['link-active']"
[routerLinkActiveOptions]="{ exact: true }">
<a class="nav-link text-dark" [routerLink]="['/']">Home</a>
</li>
<li class="nav-item" [routerLinkActive]="['link-active']">
<a class="nav-link text-dark" [routerLink]="['/all']">Wszystkie</a>
</li>
<li class="nav-item" [routerLinkActive]="['link-active']">
<a class="nav-link text-dark" [routerLink]="['/random']">Losuj</a>
</li>
</ul>
i... Mamy przed sobą ładną tabelkę danych.
W następnym wpisie będziemy tworzyć formularze w Angularze. Zastanawiam się też czy w ASP.NET CORE nie zrobić logiki baz danych stosując Entity Framework.