Standalone Components to jedna z funckjonalności na którą społeczność Angulara czekała od dawna. Angular 15 to jednak nie tylko Standalone Components, a cała paczka z dawna wyczekiwanych funkcjonalności.
1. Angular v15
Kolejne wersje Angulara publikowane są co 6 miesięcy z dokładnością szwajcarskiego zegarka. Dzisiaj światło dzienne ujrzał Angular 15. Czy wywraca on do góry nogami status quo jak Svelte albo chociaż prezentuje nowe innowacyjne koncepcje jak React Server Components? Niestety nie. Jest to jednak wydanie pełne małych inkrementalnych zmian, adresujących realne bolączki deweloperów. Jeśli mam być szczery to dla mnie jest to najbardziej interesujące wydanie Angulara od co najmniej dwóch lat.
Angular Standalone Components/
Zanim przejdziemy do clue całej sprawy mała dygresja dla wszystkich, którzy na co dzień nie mają do czynienia z frameworkiem od Google. Angularowe moduły mają niewiele wspólnego z modułami znanym z JavaScriptu. Natywne moduły pozwalają podzielić aplikację na wiele plików i zarządzać API, jakie udostępniamy. Angularowe moduły mają na celu zapewniać konfigurację zależności dla Dependency Injection. W Angularze każdy komponent czy dyrektywa musi być częścią jakiegoś modułu, co czyni je najbardziej atomicznym elementem frameworku. Co ciekawe, moduły trafiły do Angulara dopiero w wersji 2.0.0-rc.5 i były odpowiedzią na problemy z publikowanie Angularowych bibliotek w npm. Z racji, że framework był już na etapie Release Candidate, to całe rozwiązanie powstało w przyśpieszonym tempie, i jak to bywa z takimi rozwiązaniami zostało z nami na dłużej.
Na przestrzeni lat dobre praktyki wokół Angulara ewoluowały i obecnie najczęściej uskuteczniamy jest schemat SCAM ( Single Component Angular Module). Pewnie nie byłoby w tym nic złego, gdyby nie ilość boilerplateu jaka generowana jest przez taką architekturę.
@Component({
selector: 'vived-my-component',
template:`
<div>
<h2>Today is {{today | date}} </h2>'
<CustomComponent />
</div>
`
})
export class MyComponent {
readonly today = new Date();
}
@NgModule({
imports: [ CommonModule, CustomComponentModule ],
declarations: [ MyComponent ],
exports: [ MyComponent ],
})
export class MyComponentModule { }
Angular Standalone Components, czyli komponenty pozbawione modułów, zostały po raz pierwszy zaprezentowane w postaci RFC pod koniec 2021 roku. Społeczność właściwie od razu pokochała je bezwarunkowo. W połowie 2022 roku wraz z Angularem 14 otrzymaliśmy ich pierwszą niestabilną implementację. W tym tygodniu wraz z Angularem 15 doczekaliśmy się ich pierwszej stabilnej wersji.
W dużym uproszczeniu Standalone Components, to architektura SCAM zrobiona dobrze. Jeśli do komponentu dołączymy odpowiednie metadane, to Angular automatycznie zacznie traktować dany komponent jako moduł.
@Component({
selector: 'vived-my-component',
standalone: true.,
imports: [ CommonModule, CustomComponent ]
template: `
<div>
<h2>Today is {{today | date}} </h2>'
<CustomComponent />
</div>
`
})
export class MyComponent {
readonly today = new Date();
}
Zespół Angulara nie poprzestał na API do tworzenia Standalone Components, ale za cel postawił sobie przygotowanie funkcyjnego API, które pozwoli całkowicie pozbyć się z modułów z większości aplikacji. Angular 15 jest zwieńczeniem tych starań, bo do funkcyjnego API do bootstapowania aplikacji dołącza funkcyjny router oraz funkcyjny moduł http.
// This is how you bootstrap application with modules
@NgModule({
declarations: [AppComponent],
imports: [
RouterModule.forRoot(
[
{ path: '/home', component: HomeComponent },
{ path: '**', redirectTo: '/home' },
],
{
preloadingStrategy: PreloadAllModules,
}
),
],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
],
bootstrap: [AppComponent],
})
export class AppModule {}
platformBrowserDynamic().bootstrapModule(AppModule)
// This is how you bootstrap application without modules
bootstrapApplication(AppComponent, {
providers: [
provideRouter([
{ path: '/home', component: HomeComponent },
{ path: '**', redirectTo: '/home' },
], withPreloading(PreloadAllModules)),
provideHttpClient(withInterceptors([AuthInterceptor]))
]
});
Oprócz oczywistych zalet w postaci czytelniejszego kodu oraz pozbycia się skomplikowanych NgModules
, zastosowanie funkcyjnego podejścia sprawiło, że wszystkie API są teraz w pełni tree-shakable. Jak chwali się zespół Angulara, w przypadku API routera odchudza to paczkę o średnio 11%!
Na zakończenie warto podkreślić, że Standalone Components nie są próbą całkowitego usunięcia Angularowych modułów. Ich koncepcja jest głęboko zakorzeniona w architekturze Angulara i w wielu przypadkach ich wykorzystanie wciąż będzie niezbędne do osiągnięcia pożądanego rezultatu. Moduły pozostają również kluczowym aspektem enkapsulacji wielu zewnętrznych bibliotek.
Functional Router Guards
W sporym uproszczeniu Routing w Angularze oparty jest o tablicę obiektów definiujących ścieżkę, komponent do wyrenderowania oraz listę Guardów. Te ostatnie pozwalają zawierają logikę warunkującą dostęp do danej ścieżki oraz umożliwiają wykonanie odpowiedniego przekierowania.
@NgModule({
imports: [
RouterModule.forRoot(
[
{ path: '/login', component: LoginComponent },
{ path: '/home', component: HomeComponet, canActivate: [LoggedInGuard] },
{ path: '**', redirectTo: '/home' },
]
),
],
exports: [RouterModule],
})
export class AppRoutingModule {}
@Injectable({ providedIn: 'root' })
export class LoggedInGuard implements CanActivate {
constructor(
private readonly authService: AuthService,
private readonly router: Router
) {}
async canActivate(route: ActivatedRouteSnapshot): Promise<boolean> {
if (!await this.authService.isLoggedIn()) {
return true;
}
return this.router.navigate(‘/home’);
}
}
Gdybym dla Angulara 15 miał wymyślić chwytliwe hasło reklamowe, to byłoby to zdecydowanie “Mniej boilerplateu, więcej frajdy”. Kolejną sporą nowością w Angularze 15 jest możliwość zastąpienia skomplikowanych klasowych Guradów prostą funkcją zwracającą wartość boolean. Zaryzykuję stwierdzenie, że wykorzystując dodaną w Angularze 14 funkcję inject()
możliwe jest całkowite pozbycie się z kodu klasowych Guardów.
provideRouter([
{ path: '/login', component: LoginComponent },
{ path: '/home', component: HomeComponet, canActivate: [isLoggedIn()] },
{ path: '**', redirectTo: '/home' },
])
function isLoggedIn() {
if (!await inject(AuthService).isLoggedIn()) {
return true;
}
return inject(Router).navigate('/home');
}
Niewątpliwą zaletą nowej architektury jest też prostota parametryzacji. Zamiast przekazywać i czytać dane z ActivatedRouteSnapshot, możemy po prostu przekazać je jako argumenty funkcji.
// This is how you parametarize Guards with class approach
@NgModule({
imports: [
RouterModule.forRoot(
[
{ path: '/login', component: LoginComponent },
{
path: '/home',
component: HomeComponet,
canActivate: [AuthStateGuard],
data: { states: ['LOGGED_IN'] }
},
{ path: '**', redirectTo: '/home' },
]
),
],
exports: [RouterModule],
})
export class AppRoutingModule {}
@Injectable({ providedIn: 'root' })
export class AuthStateGuard implements CanActivate {
constructor(
private readonly authService: AuthService,
private readonly router: Router
) {}
async canActivate(route: ActivatedRouteSnapshot): Promise<boolean> {
if (route.data.states.includes('LOGGED_IN') && await this.authService.isLoggedIn()) {
return true;
}
return this.router.navigate('/login');
}
}
// This is how you parametarize Guards with functional approach
provideRouter([
{ path: '/login', component: LoginComponent },
{
path: '/home',
component: HomeComponet,
canActivate: [authStatesGuard(['LOGGED_IN'])]
} ,
{ path: '**', redirectTo: '/home' },
])
function authStatesGuard(states: string[]) {
if (states.includes('LOGGED_IN') && await inject(AuthService).isLoggedIn()) {
return true;
}
return inject(Router).navigate('/login');
}
Na koniec warto wspomnieć jeszcze, że analogiczne funkcyjne API dostały również interceptor HTTP
provideHttpClient(
withInterceptors([
(req, next) => {
// We can use the inject() function inside this function
// For example: inject(AuthService)
return next(req);
},
])
)
Directive Composition API
W odróżnieniu od Reacta czy Vue, które oparte są o funkcje, Angular zbudowany jest głównie wokół klas. Z tego względu w frameworku od Google nie można zastosować prostej kompozycji funkcji. Jej miejsce zajmują dyrektywy, czyli klasy pozwalające rozszerzyć funkcjonalność komponentu
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
constructor(private el: ElementRef) {
this.el.nativeElement.style.backgroundColor = 'yellow';
}
}
@Component({
selector: 'vived-my-component',
standalone: true.,
imports: [ HighlightDirective ]
// 👇 This is how you enrich component behaviour with Directive
template: '<CustomComponent appHighlight />';
})
export class MyComponent {}
Jedną z największych bolączek dyrektyw do tej pory był brak możliwości przypisania ich bezpośrednio przy definiowaniu komponentu oraz problemy z ich rozszerzaniem. Żeby lepiej zobrazować problem, wyobraźcie sobie, że implementujecie przycisk, który po najechaniu zapala się na żółto. W waszym kodzie istnieje już dyrektywa, która umożliwia zapalenie dowolnego komponentu na żółto. Angular nie umożliwia jednak przypisania dyrektywy do komponentu podczas jego definiowania. W efekcie skazani jesteście albo na duplikację kodu, albo manualne dopinanie dyrektywy za każdym razem, kiedy korzystacie z stworzonego komponentu.
Angular 15 rozwiązuje ten problem, poprzez dodanie do dekoratora @Component
właściwości hostDirective
. Przekazujemy do niej listę dyrektyw, które mają zostać zaaplikowane do naszego komponentu. Komponent bazowy automatycznie dziedziczy wszystkie właściwości przypisanych do niego dyrektyw.
@Component({
selector: 'vived-my-component',
standalone: true,
imports: [ HighlightDirective ],
// 👇 This is how you assign directive to the component in Angular 15
hostDirectives: [AppHighlight],
template: '<button>Hello World</button>';
})
export class CustomComponent {
readonly today = new Date();
}
Co interesujące, nowo dodana właściwość umożliwia również kompozycję samych dyrektyw.
@Directive({
selector: '[appHighlightAndUnderline]',
hostDirectives: [AppHighlight, AppUnerline]
})
export class HighlightAndUnderline { }
Optimised Image Directive
Project Aurora to inicjatywa powołana do życia przez zespół rozwijający Google Chrome. Jego głównym zadaniem jest współpraca z twórcami popularnych bibliotek i frameworków, w celu tworzenia jak najlepszych doświadczeń dla użytkowników internetu. Niespełna kilka tygodni temu informowaliśmy Was o next/image
, czyli komponencie do prezentowania obrazów tak, aby optymalizować metryki Core Web Vitals. Komponent ten był efektem współpracy zespołu Project Aurora i Next.js. Do Angulara 15 trafiła analogiczna dyrektywa, która również jest efektem współpracy z Project Aurora.
@Component({
standalone: true,
// 👇 Notice how src is replaced with ngSrc
template: '<img [ngSrc]=”src” [alt]=”alt”/>'
imports: [NgOptimizedImage],
})
class MyStandaloneComponent {
@Input() src: string;
@Input() alt: string;
}
Usprawniony Stack Trace
Wyniki corocznej ankiety przeprowadzanej przez zespół Angulara wyraźnie pokazały, że największym problemem w debugowaniu są skomplikowane komunikaty błędów. Aby zaadresować ten problem, we współpracy z zespołem Chrome DevTools, stworzony został mechanizm anotacji, który pozwala usunąć z stack trace linie wskazujące na node_modules
. Dzięki temu łatwiejsze powinno być zrozumienie gdzie w aplikacji tak naprawdę wystąpił błąd.
// Error from Angular 14
ERROR Error: Uncaught (in promise): Error
Error
at app.component.ts:18:11
at Generator.next (<anonymous>)
at asyncGeneratorStep (asyncToGenerator.js:3:1)
at _next (asyncToGenerator.js:25:1)
at _ZoneDelegate.invoke (zone.js:372:26)
at Object.onInvoke (core.mjs:26378:33)
at _ZoneDelegate.invoke (zone.js:371:52)
at Zone.run (zone.js:134:43)
at zone.js:1275:36
at _ZoneDelegate.invokeTask (zone.js:406:31)
at resolvePromise (zone.js:1211:31)
at zone.js:1118:17
at zone.js:1134:33
// The same Error from Angular 15
ERROR Error: Uncaught (in promise): Error
Error
at app.component.ts:18:11
at fetch (async)
at (anonymous) (app.component.ts:4)
at request (app.component.ts:4)
at (anonymous) (app.component.ts:17)
at submit (app.component.ts:15)
at AppComponent_click_3_listener (app.component.html:4)
Źródła:
https://blog.angular.io/angular-v15-is-now-available-df7be7f2f4c8
Zainstaluj teraz i czytaj tylko dobre teksty!
2. TypeScript 4.9
Ostatnie kilka wersji TypeScript to głównie nudne optymalizacje i obsługa kolejnych przypadków brzegowych. Wydany w tym tygodniu TypeScript 4.9 jest zupełnie inny, bo wprowadza aż dwa nowe słowa kluczowe. Bez zbędnego przedłużania przyjrzyjmy się co nowego trafiło do języka od Microsoftu i dlaczego jest to jego najciekawsza wersja od dawna.
Satisfies operator
Tłumacząc działanie nowego operatora posłużę się przykładem z notatki od Microsoftu. Załóżmy, że potrzebujemy otypować następujący kod:
// Each property can be a string or an RGB tuple.
const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255]
};
// We want to be able to use array methods on 'red'...
const redComponent = palette.red.at(0);
// or string methods on 'green'...
const greenNormalized = palette.green.toUpperCase();
W pierwszej chwili do głowy przyjść może zdefiniowanie typu Color oraz wykorzystanie typu Record. Niestety, w takim przypadku zmuszeni jesteśmy wykonywać niebezpieczną operację rzutowania:
type Color = "red" | "green" | "blue";
type RGB = [red: number, green: number, blue: number];
type Palette = Record<Color, string | RGB>
const palette: Palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255]
};
// We want to be able to use array methods on 'red'...
const redComponent = (palette.red as RGB).at(0);
// or string methods on 'green'...
const greenNormalized = (palette.green as string).toUpperCase();
Aby usunąć rzutowanie, możemy w definicji typu zawrzeć sposób w jaki będzie on definiowany. W naszym przypadku nie jest to może najgorszy pomysł, ale zobaczcie sami, ile dodatkowego kodu musimy wygenerować, nie mówiąc już o dużo mniejszej elastyczności.
type StringColor = "green";
type RGBColor = "red" | "blue";
type RGB = [red: number, green: number, blue: number];
type StringColorPalette = Record<StringColor, string>;
type RGBColorPalette = Record<RGBColor, RGB>;
type Palette = StringColorPalette & RGBColorPalette;
const palette: Palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255]
};
const redComponent = palette.red.at(0);
const greenNormalized = palette.green.toUpperCase();
Obejściem tego problemu ma być nowy operator satisfies, który będzie walidował typ w momencie przypisania, ale nie będzie miał wpływu na typ ewaluowany przez TypeScript. Brzmi skomplikowanie, ale na prostym przykładzie dobrze widać, o co chodzi:
type Color = "red" | "green" | "blue";
type RGB = [red: number, green: number, blue: number];
type Palette = Record<Color, string | RGB>
const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255]
} satisfies Palette;
// Both of these methods are still accessible!
const redComponent = palette.red.at(0);
const greenNormalized = palette.green.toUpperCase();
// —-----------------------------------
// Example errors caught by satisfies
// —-----------------------------------
const spelloPalette = {
red: [255, 0, 0],
green: "#00ff00",
bleu: [0, 0, 255] // Such typos are now caught
} satisfies Palette;
// Missing properties are now caught
const missingColorPalette = {
red: [255, 0, 0],
bleu: [0, 0, 255]
} satisfies Palette;
const wrongColorTypePalette = {
red: [255, 0], // Such typos are now also caught
green: "#00ff00",
bleu: [0, 0, 255]
} satisfies Palette;
Auto-Accessors
TypeScript 4.9 wprowadza jeszcze jedno nowe słowo kluczowe `accessor`. Za jego pomocą możliwe będzie zdefiniowanie pola w klasie, które pod spodem będzie prywatną zmienną zapakowaną w getter i setter. Po co to wszystko? Jak się okazuje, jest to przygotowanie na dekoratory, które obecnie znajdują się w 3 fazie procesu TC39.
class Person {
accessor name: string;
constructor(name: string) {
this.name = name;
}
}
class Person {
#__name: string;
get name() {
return this.#__name;
}
set name(value: string) {
this.#__name = name;
}
constructor(name: string) {
this.name = name;
}
}
Usprawnione porównania z NaN
Co do tego, że JavaScript to szalony język, chyba wszyscy się zgodzimy. W końcu w jakim innym języku mogłoby wydarzyć się coś takiego:
Dzisiaj nie będziemy jednak rozmawiać o tym, a o porównaniach z NaN
(Not a Number). W większości języków obsługujących zmienne zmiennoprzecinkowe, zakłada się, że nic nie może być równe NaN
– nawet inna instancja NaN
NaN == 0 // false
NaN == Nan // false
NaN === Nan // false
Dosyć częstym błędem, który łatwo przeoczyć jest więc bezpośrednie porównywanie z `NaN`. TypeScript 4.9 będzie nas skrupulatnie przed tym chronił zwracając odpowiedni błąd.
function validate(someValue: number) {
return someValue !== NaN;
// ~~~~~~~~~~~~~~~~~
// error: This condition will always return 'true'.
// Did you mean '!Number.isNaN(someValue)'?
}
Co przyniesie TypeScript 5.0?
Kolejna wersja TypeScript nad którą rozpoczęto już prace będzie oznaczona numerkiem 5.0. Należy jednak pamiętać, że TypeScript nie stosuje konwencji Semantic Versioning i nie koniecznie oznacza to, że wersja ta zawierać będzie breaking changes. Przeglądając repozytorium projektu możemy już zobaczyć co szykują dla nas ludzie z Microsoft. W skrócie, kompilator przepisany został z archaicznych namespaces na moduły. Dzięki temu kompilator uruchamia się między 10-25% szybciej, a paczka została odchudzona o prawie 50%.
Źródła:
https://devblogs.microsoft.com/typescript/announcing-typescript-4-9/
3. Deno 1.28 z wsparciem dla npm
Piekło oficjalnie zamarzło! Deno, czyli projekt, który miał naprawić wszystkie błędy Node.js, dostał wsparcie dla npm. Po latach prób odcięcia się od najpopularniejszego repozytorium JavaScript twórcy zaakceptowali, że pomimo jego wielu wad, bez npm ciężko żyć.
Nie dajcie się jednak zmylić – npm w połączeniu z Deno będzie działał zupełnie inaczej niż w połączeniu z Node.js. Po pierwsze, Deno nie będzie wymagać uruchamiania `npm install`, ani żadnej innej komendy npm. Po drugie, Deno nie będzie tworzyć katalogu node_modules,a pobrane paczki przechowywane będą globalnie. Po trzecie, importy z npm będą oznaczone specjalnym prefixem `npm:`
import { chalk } from "npm:chalk@5";
Dlaczego decyzja o dodaniu wsparcia dla npm zapadła właśnie teraz? Moim zdaniem, przede wszystkim dlatego, że jest to słuszna decyzja. Jeśli jednak lubicie filmy z żółtymi napisami, to istnieje jeszcze jedna teoria.
Wokół Deno zbudowany została całkiem pokaźna firma. Do tej pory zgromadziła ona od szeregu inwestorów ponad 25M$. Na samym rozwoju silnika uruchomieniowego JavaScript ciężko zarabiać, dlatego firma skupiła się na rozbudowie infrastruktury chmurowej. Efektem ponad roku prac jest chmura Deno Deploy. Znalazła ona już pierwszych komercyjnych klientów w postaci Supabase (otwartoźródłowa alternatywa dla Firebase) czy Netlify, którzy swoje Edge Functions postanowili oprzeć właśnie o Deno Deploy. Wraz z zewnętrznymi inwestorami i klientami przychodzą oczekiwania wzrostu. Deno wokół idei lepszego Node.js zbudował naprawdę dużo. Wydaje się jednak, że bez sensownej integracji z npm dalszy rozwój nie był możliwy.
Jeśli naprawdę lubicie filmy z żółtym napisami, to istnieje jeszcze jedna teoria. Pod koniec wakacji na rynku pojawił się nowy gracz w postaci Bun.js. Jest to alternatywa typu drop-in dla Node.js, napisana w szybkim języku Zig i oparta o silnik JavaScriptCore. O ile Deno swój marketing skupia na rozwiązywaniu problemów Node.js, to Bun stawia nacisk na wydajność.
Początkowo Bun wydawał się chwilową ciekawostką. W ciągu kilkutygodni przerodził się jednak w pełnoprawny startup, który zgromadził 7M$. Oven (bo tak zdecydowała się nazwać firmę) przyjął strategię bliźniaczo podobną do Deno Company. Firma zamierza przygotować architekturę chmurową opartą o Bun, którą sprzedawać będą klientom.
Informacja o pracach nad dodaniem wsparcie dla npm w Deno zupełnie przypadkiem zbiegła się w czasie z powstaniem firmy Oven, a fakt, że Bun.js wspiera npm na pewno nie miał wpływu na podjęte decyzje
Źródła
Zainstaluj teraz i czytaj tylko dobre teksty!
Bonus: Opera pierwszą przeglądarką z wbudowanym TikTok’iem
Nie przesłyszeliście się – Opera jest pierwszą przeglądarką, która zintegrowała się z TikTok’iem. Stał się on częścią paska integracji, wśród których znajdziemy na przykład Twittera cze Messengera. Jak twierdzą twórcy przeglądarki – ludzie, którzy zaczęli korzystać z natywnych integracji wbudowanych w przeglądarkę, nie wracają już do ich webowych odpowiedników. To jak, instalujemy teraz Operę, żeby bez skrupułów oglądać TikTok’a w pracy?