Im letzten Jahr wurden viele neue Funktionen in Angular hinzugefügt, z. B. Hydration und deferrable views, mit denen Entwickler ihre Core Web Vitals verbessern und die Nutzerfreundlichkeit für ihre Endnutzer erhöhen können. Wir arbeiten auch an zusätzlichen serverseitigen Rendering-Funktionen, die auf dieser Funktion aufbauen, z. B. Streaming und teilweise Hydratisierung.
Leider gibt es ein Muster, das verhindern kann, dass Ihre Anwendung oder Bibliothek alle diese neuen und kommenden Funktionen voll ausschöpft: die manuelle Manipulation der zugrunde liegenden DOM-Struktur. Angular erfordert, dass die DOM-Struktur von dem Zeitpunkt an, an dem eine Komponente vom Server serialisiert wird, konsistent bleibt, bis sie im Browser hydriert ist. Wenn Sie ElementRef
, Renderer2
oder DOM-APIs verwenden, um Knoten vor der Hydratisierung manuell zum DOM hinzuzufügen, zu verschieben oder daraus zu entfernen, kann dies zu Inkonsistenzen führen, die die Funktion dieser Funktionen verhindern.
Nicht jede manuelle DOM-Manipulation und -Zugriff sind jedoch problematisch und manchmal notwendig. Der Schlüssel zur sicheren Verwendung des DOM besteht darin, die Notwendigkeit so weit wie möglich zu minimieren und dann die Verwendung so lange wie möglich hinauszuzögern. In den folgenden Richtlinien wird erläutert, wie Sie dies erreichen und wirklich universelle und zukunftssichere Angular-Komponenten erstellen können, die alle neuen und kommenden Funktionen von Angular voll ausschöpfen können.
Manuelle DOM-Manipulation vermeiden
Der beste Weg, die durch manuelle DOM-Manipulation verursachten Probleme zu vermeiden, besteht natürlich darin, diese nach Möglichkeit vollständig zu vermeiden. Angular bietet integrierte APIs und Muster, mit denen die meisten Aspekte des DOM manipuliert werden können. Sie sollten diese verwenden, anstatt direkt auf das DOM zuzugreifen.
DOM-Element einer Komponente mutieren
Wenn Sie eine Komponente oder Direktive schreiben, müssen Sie möglicherweise das Hostelement (d. h. das DOM-Element, das mit dem Sellektor der Komponente oder Direktive übereinstimmt) ändern, um beispielsweise eine Klasse, einen Stil oder ein Attribut hinzuzufügen, anstatt ein Wrapper-Element zu verwenden oder einzuführen. Es ist verlockend, einfach nach ElementRef
zu greifen, um das zugrunde liegende DOM-Element zu ändern. Stattdessen sollten Sie Hostbindungen verwenden, um die Werte deklarativ an einen Ausdruck zu binden:
@Component({
selector: 'my-component',
template: `...`,
host: {
'[class.foo]': 'true'
},
})
export class MyComponent {
/* ... */
}
Ähnlich wie bei der Datenbindung in HTML können Sie beispielsweise auch Attribute und Stile binden und 'true'
in einen anderen Ausdruck ändern, den Angular verwendet, um den Wert bei Bedarf automatisch hinzuzufügen oder zu entfernen.
In einigen Fällen muss der Schlüssel dynamisch berechnet werden. Sie können auch ein Signal oder eine Funktion binden, die einen Satz oder eine Zuordnung von Werten zurückgibt:
@Component({
selector: 'my-component',
template: `...`,
host: {
'[class.foo]': 'true',
'[class]': 'classes()'
},
})
export class MyComponent {
size = signal('large');
classes = computed(() => {
return [`size-${this.size()}`];
});
}
Bei komplexeren Anwendungen kann es verlockend sein, eine manuelle DOM-Manipulation zu verwenden, um eine ExpressionChangedAfterItHasBeenCheckedError
zu vermeiden. Stattdessen können Sie den Wert wie im vorherigen Beispiel an ein Signal binden. Dies kann nach Bedarf erfolgen. Es sind keine Signale in der gesamten Codebasis erforderlich.
DOM-Elemente außerhalb einer Vorlage mutieren
Es ist verlockend, über das DOM auf Elemente zuzugreifen, auf die normalerweise nicht zugegriffen werden kann, z. B. auf Elemente, die zu anderen über- oder untergeordneten Komponenten gehören. Dies ist jedoch fehleranfällig, verstößt gegen die Kapselung und erschwert es, diese Komponenten in Zukunft zu ändern oder zu aktualisieren.
Stattdessen sollten alle anderen Komponenten als Blackbox gewertet werden. Überlegen Sie, wann und wo andere Komponenten (auch innerhalb derselben Anwendung oder Bibliothek) mit der Komponente interagieren oder ihr Verhalten oder Aussehen anpassen müssen, und stellen Sie dann eine sichere und dokumentierte Möglichkeit dafür bereit. Verwenden Sie Funktionen wie die hierarchische Abhängigkeitsinjektion, um eine API für einen untergeordneten Knoten verfügbar zu machen, wenn einfache @Input
- und @Output
-Eigenschaften nicht ausreichen.
Bisher war es üblich, Funktionen wie modale Dialogfelder oder Kurzinfos zu implementieren, indem am Ende des <body>
oder eines anderen Hostelements ein Element hinzugefügt und dann Inhalte dorthin verschoben oder projiziert wurden. Heutzutage können Sie jedoch wahrscheinlich stattdessen ein einfaches <dialog>
-Element in Ihrer Vorlage rendern:
@Component({
selector: 'my-component',
template: `<dialog #dialog>Hello World</dialog>`,
})
export class MyComponent {
@ViewChild('dialog') dialogRef!: ElementRef;
constructor() {
afterNextRender(() => {
this.dialogRef.nativeElement.showModal();
});
}
}
Manuelle DOM-Bearbeitung aufschieben
Nachdem Sie die vorherigen Richtlinien verwendet haben, um die direkte DOM-Manipulation und den Zugriff so weit wie möglich zu minimieren, haben Sie möglicherweise einige übrig, die unvermeidlich sind. In solchen Fällen ist es wichtig, die Aktualisierung so lange wie möglich hinauszuschieben. afterRender
- und afterNextRender
-Callbacks eignen sich hervorragend dazu, da sie nur im Browser ausgeführt werden, nachdem Angular nach Änderungen gesucht und sie im DOM verbindlich gemacht hat.
Nur Browser-JavaScript ausführen
In einigen Fällen gibt es Bibliotheken oder APIs, die nur im Browser funktionieren, z. B. eine Diagrammbibliothek oder die Verwendung von IntersectionObserver
. Anstatt bedingt zu prüfen, ob du im Browser ausgeführt wirst, oder das Verhalten auf dem Server zu stützen, kannst du einfach afterNextRender
verwenden:
@Component({
/* ... */
})
export class MyComponent {
@ViewChild('chart') chartRef: ElementRef;
myChart: MyChart|null = null;
constructor() {
afterNextRender(() => {
this.myChart = new MyChart(this.chartRef.nativeElement);
});
}
}
Benutzerdefiniertes Layout ausführen
Manchmal müssen Sie das DOM lesen oder darauf schreiben, um ein Layout auszuführen, das von Ihren Zielbrowsern noch nicht unterstützt wird, z. B. das Positionieren eines Kurzinfofelds. afterRender
eignet sich hervorragend dafür, da Sie sicher sein können, dass sich das DOM in einem konsistenten Zustand befindet. afterRender
und afterNextRender
akzeptieren einen phase
-Wert von EarlyRead
, Read
oder Write
. Wenn das DOM-Layout nach dem Schreiben gelesen wird, muss der Browser das Layout synchron neu berechnen. Das kann sich ernsthaft auf die Leistung auswirken (siehe Layout-Trashing). Daher ist es wichtig, die Logik sorgfältig in die richtigen Phasen aufzuteilen.
Beispielsweise verwendet eine Kurzinfo-Komponente, die eine Kurzinfo relativ zu einem anderen Element auf der Seite anzeigen soll, wahrscheinlich zwei Phasen. In der EarlyRead
-Phase werden zuerst die Größe und Position der Elemente erfasst:
afterRender(() => {
targetRect = targetEl.getBoundingClientRect();
tooltipRect = tooltipEl.getBoundingClientRect();
}, { phase: AfterRenderPhase.EarlyRead },
);
Dann verwendet die Phase Write
den zuvor gelesenen Wert, um die Kurzinfo tatsächlich neu zu positionieren:
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 },
);
Durch die Aufteilung unserer Logik in die richtigen Phasen ist Angular in der Lage, DOM-Manipulationen über alle anderen Komponenten der Anwendung im Batch zu stapeln und so eine minimale Leistungseinbußen zu gewährleisten.
Fazit
Es gibt viele neue und spannende Verbesserungen am serverseitigen Rendering von Angular, mit dem Ziel, Ihnen die Bereitstellung einer hervorragenden Nutzererfahrung zu erleichtern. Wir hoffen, dass Ihnen die vorherigen Tipps dabei helfen, die Vorteile von Firebase in Ihren Anwendungen und Bibliotheken voll auszuschöpfen.