W ciągu ostatniego roku w ramach Angular dodano wiele nowych funkcji, takich jak hydratacja i opóźnione wyświetlanie widoków, aby pomóc deweloperom poprawić podstawowe wskaźniki internetowe i zapewnić użytkownikom wygodę korzystania z aplikacji. Trwają też badania nad dodatkowymi funkcjami związanymi z renderowaniem po stronie serwera, które opierają się na tej funkcjonalności, np. strumieniowanie i częściowe nawilżanie.
Niestety, jest jeden schemat, który może uniemożliwić Twojej aplikacji lub bibliotece pełne korzystanie z tych nowych i przyszłych funkcji: ręczna manipulacja podstawową strukturą DOM. Angular wymaga, aby struktura DOM była spójna od momentu serializacji komponentu przez serwer do momentu jego utworzenia w przeglądarce. Używanie interfejsów API ElementRef
, Renderer2
lub DOM do ręcznego dodawania, przenoszenia lub usuwania węzłów z DOM przed nasyceniem może spowodować niespójności, które uniemożliwiają działanie tych funkcji.
Nie wszystkie ręczne manipulacje DOM-em i dostęp do niego są jednak problematyczne, a czasami są wręcz niezbędne. Kluczem do bezpiecznego korzystania z DOM jest jak największe ograniczenie potrzeby korzystania z niego, a następnie odłożenie jego użycia na jak najdłuższy czas. Z podanych niżej wskazówek dowiesz się, jak to zrobić, oraz jak tworzyć naprawdę uniwersalne i przyszłościowe komponenty Angular, które mogą w pełni korzystać ze wszystkich nowych i przyszłych funkcji Angular.
Unikaj ręcznej manipulacji modelem DOM
Najlepszym sposobem na uniknięcie problemów spowodowanych ręczną manipulacją DOM jest, co nie powinno dziwić, całkowite unikanie tego w miarę możliwości. Angular ma wbudowane interfejsy API i wzorce, które umożliwiają manipulowanie większością aspektów DOM: zamiast bezpośredniego dostępu do DOM lepiej jest ich używać.
Mutowanie własnego elementu DOM komponentu
Podczas pisania komponentu lub dyrektywy może być konieczne zmodyfikowanie elementu hosta (czyli elementu DOM pasującego do selektora komponentu lub dyrektywy), aby na przykład dodać klasę, styl lub atrybut, zamiast kierować się na element opakowania lub go wprowadzać. Kuszące jest użycie funkcji ElementRef
, aby zmodyfikować element DOM. Zamiast tego użyj wiązań hosta, aby deklaratywnie związać wartości z wyrażeniem:
@Component({
selector: 'my-component',
template: `...`,
host: {
'[class.foo]': 'true'
},
})
export class MyComponent {
/* ... */
}
Podobnie jak w przypadku wiązania danych w HTML, możesz też na przykład wiązać atrybuty i style oraz zmieniać 'true'
na inny wyrażenie, którego Angular użyje do automatycznego dodawania lub usuwania wartości w razie potrzeby.
W niektórych przypadkach klucz musi być obliczany dynamicznie. Możesz również związać sygnał lub funkcję z zestawem lub mapą wartości:
@Component({
selector: 'my-component',
template: `...`,
host: {
'[class.foo]': 'true',
'[class]': 'classes()'
},
})
export class MyComponent {
size = signal('large');
classes = computed(() => {
return [`size-${this.size()}`];
});
}
W bardziej złożonych aplikacjach może pojawić się pokusa ręcznej manipulacji DOM-em, aby uniknąć korzystania z elementu ExpressionChangedAfterItHasBeenCheckedError
. Zamiast tego możesz związać wartość z sygnałem, tak jak w poprzednim przykładzie. Możesz to zrobić w dowolnym momencie, nie wymaga to stosowania sygnałów w całej bazie kodu.
Mutowanie elementów DOM poza szablonem
Kuszące jest użycie modelu DOM do uzyskania dostępu do elementów, które normalnie są niedostępne, np. do elementów należących do innych komponentów nadrzędnych lub podrzędnych. Takie rozwiązanie jest jednak podatne na błędy, narusza zasadę enkapsulacji i utrudnia późniejszą zmianę lub uaktualnienie tych komponentów.
Zamiast tego Twój komponent powinien traktować wszystkie inne komponenty jako czarne pudełko. Zastanów się, kiedy i gdzie inne komponenty (nawet w tej samej aplikacji lub bibliotece) mogą potrzebować interakcji z Twoim komponentem lub dostosowywania jego zachowania lub wyglądu, a potem udostępnij bezpieczny i udokumentowany sposób na wykonanie tych czynności. Użyj funkcji takich jak hierarchiczne wstrzykiwanie zależności, aby udostępnić interfejs API poddrzewiu, gdy proste właściwości @Input
i @Output
nie wystarczają.
W przeszłości często wdrażano funkcje takie jak modalne okna dialogowe czy tooltipy, dodając element do końca <body>
lub innego elementu hosta, a następnie przenosząc lub projektując tam zawartość. Obecnie w szablonie możesz jednak renderować prosty element <dialog>
:
@Component({
selector: 'my-component',
template: `<dialog #dialog>Hello World</dialog>`,
})
export class MyComponent {
@ViewChild('dialog') dialogRef!: ElementRef;
constructor() {
afterNextRender(() => {
this.dialogRef.nativeElement.showModal();
});
}
}
Opóźnianie ręcznej manipulacji DOM
Po zastosowaniu poprzednich wskazówek, aby zminimalizować bezpośrednią manipulację DOM i dostęp do niego w jak największym stopniu, możesz nadal mieć pewne nieuniknione pozostałości. W takich przypadkach ważne jest, aby odłożyć to na jak najdłużej. Do tego celu świetnie nadają się funkcje zwrotne afterRender
i afterNextRender
, ponieważ są one wykonywane tylko w przeglądarce, gdy Angular sprawdzi, czy zaszły jakieś zmiany, i zatwierdzi je w DOM.
Uruchamianie kodu JavaScript tylko w przeglądarce
W niektórych przypadkach biblioteka lub interfejs API działa tylko w przeglądarce (np. biblioteka wykresów, niektóre zastosowania IntersectionObserver
itp.). Zamiast sprawdzać warunkowo, czy działasz w przeglądarce, lub tworzyć na serwerze stuby zachowania, możesz użyć afterNextRender
:
@Component({
/* ... */
})
export class MyComponent {
@ViewChild('chart') chartRef: ElementRef;
myChart: MyChart|null = null;
constructor() {
afterNextRender(() => {
this.myChart = new MyChart(this.chartRef.nativeElement);
});
}
}
Utwórz układ niestandardowy
Czasami może być konieczne odczytanie lub zapisanie danych w modelu DOM, aby wykonać układ, którego docelowe przeglądarki jeszcze nie obsługują, np. pozycjonowanie okienka narzędzi. afterRender
jest w tym przypadku świetnym wyborem, ponieważ możesz mieć pewność, że stan DOM jest spójny. Parametry afterRender
i afterNextRender
mogą mieć wartość phase
EarlyRead
, Read
lub Write
. Odczytywanie układu DOM po zapisaniu powoduje, że przeglądarka musi ponownie obliczyć układ synchronicznie, co może poważnie wpłynąć na wydajność (patrz przeładowanie układu). Dlatego ważne jest, aby dokładnie podzielić logikę na odpowiednie fazy.
Na przykład komponent etykiety, który ma wyświetlać etykietę względem innego elementu na stronie, prawdopodobnie będzie używać 2 faz. Faza EarlyRead
służy do określenia rozmiaru i pozycji elementów:
afterRender(() => {
targetRect = targetEl.getBoundingClientRect();
tooltipRect = tooltipEl.getBoundingClientRect();
}, { phase: AfterRenderPhase.EarlyRead },
);
Następnie faza Write
używa wcześniej odczytanej wartości, aby zmienić pozycję etykietki narzędzia:
afterRender(() => {
tooltipEl.style.setProperty('left', `${targetRect.left + targetRect.width / 2 - tooltipRect.width / 2}px`);
tooltipEl.style.setProperty('top', `${targetRect.bottom - 4}px`);
}, { phase: AfterRenderPhase.Write },
);
Dzięki podzieleniu logiki na odpowiednie fazy Angular może skutecznie grupować manipulację DOM w przypadku każdego innego komponentu w aplikacji, co zapewnia minimalny wpływ na wydajność.
Podsumowanie
Wkrótce wprowadzimy wiele nowych i ciekawych ulepszeń renderowania po stronie serwera w Angular, aby ułatwić Ci zapewnianie użytkownikom wygodnej obsługi. Mamy nadzieję, że te wskazówki pomogą Ci w całkowitym wykorzystaniu tych funkcji w aplikacjach i bibliotekach.