21 sty 2025
11 min

Generatory w Angularze

Team Angulara nieustannie wprowadza nowe funkcje i narzędzia, aby ułatwić rozwój oprogramowania. Jednym z tych przydatnych narzędzi są „generatory”. To właśnie generatory automatycznie aktualizują Twój kod do najnowszych dobrych praktyk, oszczędzając Twój czas i wysiłek.

Ten przewodnik pokaże Ci, jak skutecznie korzystać z tych generatorów. Omówimy każdy generator, w tym:

  • Polecenie do uruchomienia: Prosta instrukcja, którą wpiszesz w swoim terminalu.
  • Wersje Angulara, z którymi jest kompatybilny: Zapewnienie zgodności z Twoim projektem.
  • Opis zapytań w poleceniu: Zrozumienie, co oznacza każde pytanie.
  • Przykłady kodu: Ilustrowanie, jak generator zmienia Twój kod.

Zacznijmy!

Control Flow

Jeśli Twój projekt w Angularze nadal używa starszych dyrektyw strukturalnych takich jak *ngIf, *ngFor i ngSwitch, możesz zmodernizować swój kod, korzystając z ich nowszych, bardziej wydajnych odpowiedników: @if, @for i @switch.

Polecenie:

ng g @angular/core:control-flow

Dostępne od: Angular 17

Po wykonaniu tego polecenia pojawią się następujące zapytania:

  • Which path in your project should be migrated? (Która ścieżka w Twoim projekcie powinna zostać zmigrowana?)

Domyślna wartość to ./, co oznacza, że zmigruje całą aplikację. Możemy również podać ścieżkę względną, jeśli chcemy zmigrować tylko część aplikacji.

  • Should the migration reformat your templates? (Czy migracja powinna sformatować Twoje szablony?)
    Jeśli wybierzesz ‘Y’ (tak), generator zmigruje Twój kod i automatycznie sformatuje wcięcia w szablonach HTML dla lepszej czytelności. Wybranie ‘n’ (nie) zmigruje kod bez żadnego formatowania.

Chociaż zazwyczaj zaleca się formatowanie kodu dla lepszej utrzymania, wybór należy do Ciebie.

Źródło kodu formatującego szablony: https://github.com/angular/angular/blob/main/packages/core/schematics/ng-generate/control-flow-migration/migration.ts#L69

https://github.com/angular/angular/blob/main/packages/core/schematics/ng-generate/control-flow-migration/util.ts#L730

Przykłady kodu

Instrukcja if: Przed 

<div *ngIf="isVisible; else elseBlock">
  Displayed when isVisible is true
</div>
<ng-template #elseBlock>
  <div>
    Displayed when isVisible is false
  </div>
</ng-template>

Instrukcja if: Po

@if (isVisible) {
  <div>Displayed when isVisible is true</div>
} @else {
  <div>Displayed when isVisible is false</div>
}

—————————

Instrukcja switch: Przed

<div [ngSwitch]="color">
  <div *ngSwitchCase="'red'">Red color selected</div>
  <div *ngSwitchCase="'blue'">Blue color selected</div>
  <div *ngSwitchCase="'green'">Green color selected</div>
  <div *ngSwitchDefault>Other color selected</div>
</div>

Instrukcja switch: Po

@switch (color) {
    @case ('red') {
      <div>Red color selected</div>
    }
    @case ('blue') {
      <div>Blue color selected</div>
    }
    @case ('green') {
      <div>Green color selected</div>
    }
    @default {
      <div>Other color selected</div>
    }
  }

—————————

Blok for: Przed

<ng-container *ngIf="items.length; else emptyList">
  <div *ngFor="let item of items">
    {{ item }}
  </div>
</ng-container>

<ng-template #emptyList>
  The list is empty
</ng-template>

Blok for: Po

@if (items.length) {
  @for (item of items; track item) {
    <div>
      {{ item }}
    </div>
  }
} @else {
  The list is empty
}

Możemy zawsze wprowadzić dodatkowe ulepszenia, aby uzyskać lepszy kod. Możemy zmienić zmigrowany kod, aby używał bloku @empty.

Blok for: Ulepszony

@for (item of items; track item) {
  <div>
    {{ item }}
  </div>
} @empty {
  The list is empty
}

Funkcja Inject

Funkcja inject w porównaniu do wstrzyknięć opartych na konstruktorze ma kilka zalet.

Niektóre z nich to:

  1. Lepsza bezpieczeństwo typów:
    Funkcja inject skuteczniej wnioskowuje typ wstrzykiwanej zależności.
  2. Reużywalność:
    Dzięki funkcji inject możemy tworzyć reużywalne funkcje pomocnicze, które zależą od wstrzykniętych zależności.
  3. Ulepszona dziedziczenie:
    Używając funkcji inject w dziedziczeniu klas, nie musimy przekazywać wstrzykniętych zależności przez konstruktor rodzica.
  4. Nowoczesność i przyszłościowość:
    Używanie funkcji inject zgodnie jest z najnowszymi najlepszymi praktykami.

Polecenie:

ng g @angular/core:inject

Dostępne od: Angular 18

Po wykonaniu tego polecenia pojawią się następujące zapytania:

    •  Which path in your project should be migrated? (Która ścieżka w Twoim projekcie powinna zostać zmigrowana?)
      Domyślna wartość to ./, co oznacza, że zmigruje całą aplikację. Możemy również podać ścieżkę względną, jeśli chcemy zmigrować tylko część aplikacji.
  • Do you want to migrate abstract classes? Abstract classes are not migrated by default, because their parameters aren’t guaranteed to be injectable (Czy chcesz zmigrować klasy abstrakcyjne? Klasy abstrakcyjne nie są migrowane domyślnie, ponieważ ich parametry nie są gwarantowane jako wstrzykiwalne.)
    Domyślnie klasy abstrakcyjne nie są migrowane, ponieważ Angular nie może określić, czy argumenty ich konstruktora mogą być poprawnie wstrzyknięte. Włączenie tej opcji dla klas abstrakcyjnych może w niektórych przypadkach wprowadzić błędy kompilacji.

Aby lepiej to zrozumieć, zobaczmy poniższy przykład:\

base-http.service.ts

export abstract class BaseHttpService {
  abstract endpoint: string;

  abstract baseUrl: string;
  constructor(private http: HttpClient) {}

  private getFullUrl(): string {
    return `${this.baseUrl}/${this.endpoint}`;
  }

  get<T>(params?: HttpParams, headers?: HttpHeaders): Observable<T> {
    return this.http.get<T>(this.getFullUrl(), { params, headers });
  }
}

products.service.ts

export class ProductsService extends BaseHttpService {
  endpoint: string = 'products';
  baseUrl: string = 'https://api.com';

  constructor(http: HttpClient) {
    super(http);
  }
}

Klasa BaseHttpService wstrzykuje HttpClient  za pomocą wstrzyknięcia opartego na konstruktorze. Ponieważ ProductsService rozszerza BaseHttpService, musi dostarczyć HttpClient do klasy nadrzędnej przez swój konstruktor.

Generatory w Angularze analizują pliki indywidualnie i nie są świadome relacji między klasami. Dlatego gdy generator przetwarza BaseHttpService:

  • Wykryje wstrzyknięcie HttpClient w konstruktorze.
  • Zmigruje wstrzyknięcie HttpClient do użycia funkcji inject(HttpClient).
  • Usunie parametry konstruktora, ponieważ teraz używa inject.

Ta operacja może prowadzić do błędów kompilacji w przypadkach takich jak ProductsService, gdzie podklasa polega na dostarczeniu HttpClient do klasy nadrzędnej.

Włączenie tej opcji dla klas abstrakcyjnych może wprowadzić błędy kompilacji. Jednak te błędy mogą pomóc zidentyfikować i rozwiązać potencjalne problemy.

Do you want to clean up all constructors or keep them backwards compatible? Enabling this option will include an additional signature of `constructor(…args: unknown[]);` that will avoid errors for sub-classes, but will increase the amount of generated code by the migration (Czy chcesz wyczyścić wszystkie konstruktory lub zachować ich kompatybilność wsteczną? Włączenie tej opcji doda dodatkowy sygnaturę constructor(…args: unknown[]);, która uniknie błędów dla podklas, ale zwiększy ilość generowanego kodu przez migrację.)
Domyślnie generator usuwa argumenty konstruktora, a jeśli konstruktor nie ma argumentów, usunie go całkowicie. Jeśli włączymy ten flag, Angular wprowadzi constructor(…args: unknown[]) dla zgodności wstecznej. Włączenie tej opcji rozwiązuje problemy, które możemy mieć z klasami abstrakcyjnymi.

Zobaczmy, jak wygląda BaseHttpService po włączeniu tej opcji:

export abstract class BaseHttpService {
  private http = inject(HttpClient);

  abstract endpoint: string;

  abstract baseUrl: string;

  /** Inserted by Angular inject() migration for backwards compatibility */
  constructor(...args: unknown[]);
  constructor() {}

  private getFullUrl(): string {
    return `${this.baseUrl}/${this.endpoint}`;
  }

  get<T>(params?: HttpParams, headers?: HttpHeaders): Observable<T> {
    return this.http.get<T>(this.getFullUrl(), { params, headers });
  }
}

Jak widać, generator dodał dwa konstruktory dla kompatybilności wstecznej.

To rozwiązuje błędy kompilacji, ale wprowadza dodatkowy kod.

Do you want optional inject calls to be non-nullable? Enable this option if you want the return type to be identical to `@Optional()`, at the expense of worse type safety (Czy chcesz, aby opcjonalne wywołania inject były nie-nullowalne? Włącz tę opcję, jeśli chcesz, aby typ zwracany był identyczny jak @Optional(), kosztem gorszego bezpieczeństwa typów.)
Angular zwraca null , jeśli wstrzyknięcie parametru @Optional się nie powiedzie. Jednakże, ponieważ dekoratory nie mogą wpływać na ich typ, w wielu przypadkach typy w naszej aplikacji są nieprawidłowe.

export class AppComponent {
  constructor(@Inject(MY_TOKEN) @Optional() private readonly token: MyToken) {}

  private tokenHandler() {
    console.log(this.token.claims);
  }
}

Metoda tokenHandler loguje claims tokena. Jednakże, ponieważ MY_TOKEN jest opcjonalny, właściwość this.token może być null. Ten kod spowoduje błąd w czasie wykonania, jeśli this.token będzie null podczas próby dostępu do jego claims.

Aby zapobiec temu błędowi w czasie wykonania, poprawnym podejściem jest zdefiniowanie właściwości token jako token: MyToken | null. Spowoduje to błąd kompilacji, jeśli spróbujesz uzyskać dostęp do claims null tokena, zmuszając Cię do jawnego sprawdzenia, czy token jest prawdziwy przed dostępem do jego właściwości.

private tokenHandler() {
    if (this.token) {
      console.log(this.token.claims);
    }
  }

Team Angulara jest świadomy tych błędów programistycznych i odpowiednio zakodował generatory.

Jeśli odpowiemy „tak” na ten prompt, kod stanie się:

private readonly token = inject<MyToken>(MY_TOKEN, { optional: true })!;

Zauważ znak wykrzyknika na końcu. To sprawia, że typ jest nie-nullowalny i nie wprowadzi żadnych błędów kompilacji.

Leniwe Ładowanie Routingu

Leniwe ładowanie routingu na żądanie umożliwia proces budowy aplikacji poprzez podział na mniejsze moduły, co przyspiesza początkowe ładowanie strony.

Polecenie:

ng g @angular/core:route-lazy-loading

Dostępne od: Angular 18

Po wykonaniu tego polecenia pojawią się następujące zapytania:

  •  Which path in your project should be migrated? (Która ścieżka w Twoim projekcie powinna zostać zmigrowana?)
    Domyślna wartość to ./, co oznacza, że zostanie zmigrowana cała aplikacja. Możemy również podać ścieżkę względną, jeśli chcemy zmigrować tylko jej część.

Przykłady kodu

provideRouter: Przed

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter([
      {
        path: 'products',
        component: ProductsComponent,
      },
    ]),
  ],
};

provideRouter: Po

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter([
      {
        path: 'products',
        loadComponent: () =>
          import('./pages/products/products.component').then(
            (m) => m.ProductsComponent,
          ),
      },
    ]),
  ],
};

—————————

Schemat obsługuje również komponenty eksportowane jako domyślne.

provideRouter: Przed

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter([
      {
        path: 'products',
        component: ProductsComponent,
      },
    ]),
  ],
};

provideRouter: Po

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter([
      {
        path: 'products',
        loadComponent: () => import('./pages/products/products.component'),
      },
    ]),
  ],
};

Schemat migracji przekształci także konfigurację RouterModule . Zmigrowany kod będzie oparty na RouterModule, ale zmigruje ładowanie routingu z góry na leniwe ładowanie. Zaleca się migrację do aplikacji standalone i ponowne wykonanie tej migracji.

Sygnałowe Inputy

Ten schemat migracji przekształca dekoratory Input na ich sygnałowe odpowiedniki.

Polecenie:

ng g @angular/core:signal-input-migration

Dostępne od: Angular 19

Po wykonaniu tego polecenia pojawią się następujące zapytania:

  •  Which path in your project should be migrated? (Która ścieżka w Twoim projekcie powinna zostać zmigrowana?)
    Domyślna wartość to ./, co oznacza, że zostanie zmigrowana cała aplikacja. Możemy również podać ścieżkę względną, jeśli chcemy zmigrować tylko jej część.
  • Do you want to migrate as much as possible, even if it may break your build? ( Czy chcesz zmigrować jak najwięcej, nawet jeśli może to spowodować problemy z kompilacją?)
    Inputy komponentu są kluczową częścią API komponentu i powinny być traktowane jako pojedyncze źródło prawdy.

    • Tradycyjne Inputy: Wcześniej wejścia zdefiniowane za pomocą dekoratora @Input() mogły być modyfikowane wewnątrz komponentu, co potencjalnie prowadziło do nieoczekiwanych zachowań.
    • Sygnałowe Inputy: Nowe API Sygnalnych Wejść wymusza najlepszą praktykę: wartości wejść powinny być traktowane jako niemutowalne wewnątrz komponentu.

Jeśli wybierzesz „tak” na to zapytanie, wszystkie wejścia w Twoim projekcie zostaną zmigrowane do API Sygnałowych Inputów, niezależnie od tego, jak są obecnie używane. Może to wymagać dostosowań w logice komponentu, jeśli wcześniej modyfikowałeś wartości inputów wewnątrz komponentu.

Przykłady kodu

Użyjmy tego komponentu jako przykładu i zobaczmy, jak kod zostanie zmigrowany, jeśli odpowiemy „n” (Nie) na drugie zapytanie.

Przed

@Component({
  selector: 'app-user-card',
  imports: [JsonPipe],
  template: `
    <p>{{ user | json }}</p>
    <p>{{ isEnabled }}</p>
    <p>{{ isEdit }}</p>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent {
  @Input() user: User | undefined = undefined;
  @Input({
    required: true,
    transform: booleanAttribute,
  })
  isEnabled: boolean = false;
  @Input() isEdit: boolean = false;

  methodThatEditsTheInput() {
    this.isEdit = false;
  }
}

Oczekujemy, że zmigrowany kod będzie:

  • Miał poprawną wartość domyślną dla inputu user
  • Miał isEnabled jako wymagane oraz posiadało transformację boolean
  • Nie migrował isEdit , ponieważ jego wartość jest aktualizowana przez metodę
  • Szablon HTML będzie miał odpowiednie użycie

Po

@Component({
  selector: 'app-user-card',
  imports: [JsonPipe],
  template: `
    <p>{{ user() | json }}</p>
    <p>{{ isEnabled() }}</p>
    <p>{{ isEdit }}</p>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent {
  readonly user = input<User>();
  readonly isEnabled = input.required<boolean, unknown>({ transform: booleanAttribute });
  @Input() isEdit: boolean = false;

  methodThatEditsTheInput() {
    this.isEdit = false;
  }
}

Sygnał zawsze ma wartość, a sygnalne inputy mają domyślnie wartość undefined. Dlatego nie musimy jawnie definiować wartości undefined dla inputu user .

Przeanalizujmy także dwa typy generyczne inputu isEnabled. Pierwszy (boolean) to rzeczywisty typ inputu. Zatem typ isEnabled to boolean.

Drugi typ generyczny to typ dostarczonej wartości. Ponieważ jest to typ boolean, wartość może być ciągiem znaków „true” lub „false”. Bezpiecznie byłoby również zastąpić unknown na string. Jednak Angular podczas migracji bezpiecznie używa unknown, ponieważ typ nie jest znany i ostatecznie zostanie zawężony poprzez asercje typów lub strażników typów.

Zobaczmy teraz, jak kod będzie wyglądał, jeśli odpowiemy „Y” (Tak) na zapytanie.

@Component({
  selector: 'app-user-card',
  imports: [JsonPipe],
  template: `
    <p>{{ user() | json }}</p>
    <p>{{ isEnabled() }}</p>
    <p>{{ isEdit() }}</p>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent {
  readonly user = input<User>();
  readonly isEnabled = input.required<boolean, unknown>({ transform: booleanAttribute });
  readonly isEdit = input<boolean>(false);

  methodThatEditsTheInput() {
    this.isEdit = false; 
  }
}

Input isEdit został zmigrowany, co prowadzi do błędu kompilacji w metodzie methodThatEditsTheInput .

Najlepszym rozwiązaniem jest odpowiedzenie „n” (Nie) i użycie opcji –insert-todos.

–insert-todos

Opcja –insert-todos doda komentarze TODO w naszym kodzie dla przypadków, które nie zostały zmigrowane.

ng g @angular/core:signal-input-migration --insert-todos
@Component({
  selector: 'app-user-card',
  imports: [JsonPipe],
  template: `
    <p>{{ user() | json }}</p>
    <p>isEnabled: {{ isEnabled() }}</p>
    <p>{{ isEdit }}</p>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent {
  readonly user = input<User>();

  readonly isEnabled = input.required<boolean, unknown>({
    transform: booleanAttribute,
  });

  // TODO: Skipped for migration because:
  //  Your application code writes to the input. This prevents migration.
  @Input() isEdit: boolean = false;

  methodThatEditsTheInput() {
    this.isEdit = false;
  }
}

Outputy

Ta migracja zastępuje dekoratory Output funkcją output

Polecenie:

ng g @angular/core:output-migration

Dostępne od: Angular 19

Po wykonaniu tego polecenia pojawią się następujące zapytania:

  •  Która ścieżka w Twoim projekcie powinna zostać zmigrowana?
    Domyślna wartość to ./, co oznacza, że zostanie zmigrowana cała aplikacja. Możemy również podać ścieżkę względną, jeśli chcemy zmigrować tylko jej część.

Przykłady kodu

Przed migracją:

export class UserCardComponent {
  @Output('userChanged') userChange = new EventEmitter();

  methodThatDoesSomething() {
    this.userChange.emit();
  }
}

Po migracji:

export class UserCardComponent {
  readonly userChange = output({ alias: 'userChanged' });

  methodThatDoesSomething() {
    this.userChange.emit();
  }
}

Sygnałowe Zapytania (Queries)

Ta migracja przekształca dekoratory @ViewChild @ViewChildren @ContentChild oraz @ContentChildren na sygnalne zapytania viewChild, viewChildren, contentChild i contentChildren odpowiednio.

Polecenie:

ng g @angular/core:signal-queries-migration

Dostępne od: Angular 19

Po wykonaniu tego polecenia pojawią się następujące zapytania:

  •  Which path in your project should be migrated? (Która ścieżka w Twoim projekcie powinna zostać zmigrowana?)
    Domyślna wartość to ./, co oznacza, że zostanie zmigrowana cała aplikacja. Możemy również podać ścieżkę względną, jeśli chcemy zmigrować tylko jej część.
  • Do you want to migrate as much as possible, even if it may break your build? (Czy chcesz zmigrować jak najwięcej, nawet jeśli może to spowodować problemy z kompilacją?)
    Ponieważ ten schemat stara się nie wprowadzać błędów kompilacji podczas migracji kodu, jeśli odpowiemy „n” (Nie) na to zapytanie, nie zostaną zmigrowane wystąpienia, które mogą się nie powieść lub spowodować błędy kompilacji. Przykłady to:

    • Jeśli używamy wyrażenia w control flow (*ngIf, @if), Angular nie będzie w stanie zawęzić typu
      Przykład
@Component({
  selector: 'app-user-card',
  imports: [NgIf, NgTemplateOutlet],
  template: `
    <ng-container *ngIf="cardContentTemplate">
      <ng-templateOutlet [ngTemplateOutlet]="cardContentTemplate" />
    </ng-container>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent {
  @ContentChild('cardContent', { read: TemplateRef }) cardContentTemplate:
    | TemplateRef<any>
    | undefined;
}
  • Jeśli używamy zapytania w połączeniu z @HostBinding, migracja się nie powiedzie.
    Przykład

     @HostBinding('class.my-custom-class')
      @ContentChild('cardContent', { read: TemplateRef })
      cardContentTemplate: TemplateRef<any> | undefined;
  • Jeśli używamy setterów do obsługi wartości zapytania
    Przykład
@ContentChild('cardContent', { read: TemplateRef })
set cardContentTemplateAsSet(value: TemplateRef<any> | undefined) {
  console.log('cardContentTemplateAsSet', value);
}

Jeśli wybierzesz „tak” na to zapytanie, migrator spróbuje dokonać migracji, ale należy ręcznie sprawdzić, czy aplikacja działa zgodnie z oczekiwaniami, ponieważ mogą wystąpić błędy.

Jeśli chcesz mieć pewność, możesz odpowiedzieć „n” (Nie) na zapytanie i użyć opcji –insert-todos.

ng g @angular/core:signal-queries-migration --insert-todos

Korzystając z tej opcji, migrator doda komentarze TODO do miejsc, które nie udało się zmigrować.

Przyjrzyjmy się przykładom kodu przed i po migracji.

Przykłady kodu
viewChild & viewChildren Przed

  @ViewChild(UserCardComponent)
  userCard!: UserCardComponent;

  @ViewChildren(UserCardComponent)
  userCards!: QueryList<UserCardComponent>;

viewChild & viewChildren Po

readonly userCard = viewChild.required(UserCardComponent);
readonly userCards = viewChildren(UserCardComponent);

—————————

contentChild Przed

 @HostBinding('class.my-custom-class')
 @ContentChild('cardContent', { read: TemplateRef })
 cardContentTemplateWithHostBinding: TemplateRef<any> | undefined;

 @ContentChild('cardContent', { read: TemplateRef })
 set cardContentTemplateAsSet(value: TemplateRef<any> | undefined) {
   console.log('cardContentTemplateAsSet', value);
 }

 @ContentChild('cardContent', { read: TemplateRef })
 cardContentTemplateUsedWithIfCondition: TemplateRef<any> | undefined;

 @ContentChild('cardContent', { read: TemplateRef })
 cardContentTemplate!: TemplateRef<any>;

contentChild Po

 // TODO: Skipped for migration because:
 //  This query is used in combination with `@HostBinding` and migrating would break.
 @HostBinding('class.my-custom-class')
 @ContentChild('cardContent', { read: TemplateRef })
 cardContentTemplateWithHostBinding: TemplateRef<any> | undefined;

 // TODO: Skipped for migration because:
 //  Accessor queries cannot be migrated as they are too complex.
 @ContentChild('cardContent', { read: TemplateRef })
 set cardContentTemplateAsSet(value: TemplateRef<any> | undefined) {
   console.log('cardContentTemplateAsSet', value);
 }

 // TODO: Skipped for migration because:
 //  This query is used in a control flow expression (e.g. `@if` or `*ngIf`)
 //  and migrating would break narrowing currently.
 @ContentChild('cardContent', { read: TemplateRef })
 cardContentTemplateUsedWithIfCondition: TemplateRef<any> | undefined;

 readonly cardContentTemplate = contentChild.required('cardContent', { read: TemplateRef });

Zespół Angular wykonuje fantastyczną robotę! Nieustannie dodają nowe funkcje i ułatwiają utrzymanie naszych projektów na bieżąco. Aby dowiedzieć się więcej o dostępnych narzędziach migracyjnych, odwiedź tę stronę: https://next.angular.dev/reference/migrations. Strona ta pokaże Ci wszystkie istniejące migracje oraz nowe, które zostaną dodane w przyszłości.

Dzięki za przeczytanie!!

Podziel się artykułem

Zapisz się na nasz newsletter

Dołącz do community Angular.love i bądź na bieżąco z trendami.