Veilige toegang tot de DOM met Angular SSR,Veilige toegang tot de DOM met Angular SSR

Gerard Monaco
Gerald Monaco

Het afgelopen jaar heeft Angular veel nieuwe functies gekregen, zoals hydratatie en uitstelbare weergaven, om ontwikkelaars te helpen hun Core Web Vitals te verbeteren en een geweldige ervaring voor hun eindgebruikers te garanderen. Er wordt ook onderzoek gedaan naar aanvullende server-side rendering-gerelateerde functies die voortbouwen op deze functionaliteit, zoals streaming en gedeeltelijke hydratatie.

Helaas is er één patroon dat ervoor kan zorgen dat uw applicatie of bibliotheek niet optimaal kan profiteren van al deze nieuwe en toekomstige functies: handmatige manipulatie van de onderliggende DOM-structuur. Angular vereist dat de structuur van de DOM consistent blijft vanaf het moment dat een component door de server wordt geserialiseerd, totdat deze in de browser wordt gehydrateerd. Het gebruik van ElementRef , Renderer2 of DOM API's om handmatig knooppunten uit de DOM toe te voegen, te verplaatsen of te verwijderen voordat hydratatie kan inconsistenties introduceren die voorkomen dat deze functies werken.

Niet alle handmatige DOM-manipulatie en -toegang is echter problematisch, en soms zelfs noodzakelijk. De sleutel tot veilig gebruik van de DOM is om uw behoefte eraan zo veel mogelijk te beperken en uw gebruik ervan vervolgens zo lang mogelijk uit te stellen. In de volgende richtlijnen wordt uitgelegd hoe u dit kunt bereiken en werkelijk universele en toekomstbestendige Angular-componenten kunt bouwen die ten volle kunnen profiteren van alle nieuwe en toekomstige functies van Angular.

Vermijd handmatige DOM-manipulatie

De beste manier om de problemen te voorkomen die handmatige DOM-manipulatie veroorzaakt, is, niet verrassend, door deze waar mogelijk helemaal te vermijden. Angular heeft ingebouwde API's en patronen die de meeste aspecten van de DOM kunnen manipuleren: u zou ze liever moeten gebruiken in plaats van rechtstreeks toegang te krijgen tot de DOM.

Muteer het eigen DOM-element van een component

Wanneer u een component of richtlijn schrijft, moet u mogelijk het hostelement (dat wil zeggen het DOM-element dat overeenkomt met de selector van de component of richtlijn) wijzigen om bijvoorbeeld een klasse, stijl of attribuut toe te voegen, in plaats van een wikkelelement. Het is verleidelijk om gewoon naar ElementRef te grijpen om het onderliggende DOM-element te muteren. In plaats daarvan moet u hostbindingen gebruiken om de waarden declaratief aan een expressie te binden:

@Component({
  selector: 'my-component',
  template: `...`,
  host: {
    '[class.foo]': 'true'
  },
})
export class MyComponent {
  /* ... */
}

Net als bij gegevensbinding in HTML kunt u bijvoorbeeld ook binden aan attributen en stijlen, en 'true' wijzigen in een andere expressie die Angular zal gebruiken om de waarde indien nodig automatisch toe te voegen of te verwijderen.

In sommige gevallen moet de sleutel dynamisch worden berekend. U kunt ook binden aan een signaal of functie die een set of kaart met waarden retourneert:

@Component({
  selector: 'my-component',
  template: `...`,
  host: {
    '[class.foo]': 'true',
    '[class]': 'classes()'
  },
})
export class MyComponent {
  size = signal('large');
  classes = computed(() => {
    return [`size-${this.size()}`];
  });
}

In complexere toepassingen kan het verleidelijk zijn om handmatige DOM-manipulatie uit te voeren om een ExpressionChangedAfterItHasBeenCheckedError te voorkomen. In plaats daarvan kunt u de waarde aan een signaal binden, zoals in het vorige voorbeeld. Dit kan naar behoefte worden gedaan en vereist geen adoptie van signalen in uw gehele codebase.

Muteer DOM-elementen buiten een sjabloon

Het is verleidelijk om te proberen de DOM te gebruiken om toegang te krijgen tot elementen die normaal gesproken niet toegankelijk zijn, zoals die behoren tot andere bovenliggende of onderliggende componenten. Dit is echter foutgevoelig, is in strijd met de inkapseling en maakt het moeilijk om deze componenten in de toekomst te wijzigen of te upgraden.

In plaats daarvan moet uw component elk ander onderdeel als een zwarte doos beschouwen. Neem de tijd om te overwegen wanneer en waar andere componenten (zelfs binnen dezelfde applicatie of bibliotheek) mogelijk moeten communiceren met uw component of het gedrag of uiterlijk ervan moeten aanpassen, en stel vervolgens een veilige en gedocumenteerde manier voor om dit te doen. Gebruik functies zoals hiërarchische afhankelijkheidsinjectie om een ​​API beschikbaar te maken voor een substructuur wanneer eenvoudige @Input en @Output eigenschappen niet voldoende zijn.

Historisch gezien was het gebruikelijk om functies zoals modale dialogen of tooltips te implementeren door een element toe te voegen aan het einde van het <body> of een ander hostelement en vervolgens de inhoud daarheen te verplaatsen of te projecteren. Tegenwoordig kunt u echter waarschijnlijk een eenvoudig <dialog> -element in uw sjabloon weergeven:

@Component({
  selector: 'my-component',
  template: `<dialog #dialog>Hello World</dialog>`,
})
export class MyComponent {
  @ViewChild('dialog') dialogRef!: ElementRef;

  constructor() {
    afterNextRender(() => {
      this.dialogRef.nativeElement.showModal();
    });
  }
}

Stel handmatige DOM-manipulatie uit

Nadat u de voorgaande richtlijnen heeft gevolgd om uw directe DOM-manipulatie en -toegang zoveel mogelijk te minimaliseren, blijft er wellicht nog wat over dat onvermijdelijk is. In dergelijke gevallen is het belangrijk om het zo lang mogelijk uit te stellen. afterRender en afterNextRender callbacks zijn een geweldige manier om dit te doen, omdat ze alleen in de browser worden uitgevoerd nadat Angular op eventuele wijzigingen heeft gecontroleerd en deze aan de DOM heeft vastgelegd.

Voer JavaScript alleen in de browser uit

In sommige gevallen beschikt u over een bibliotheek of API die alleen in de browser werkt (bijvoorbeeld een kaartbibliotheek, enig IntersectionObserver gebruik, enz.). In plaats van voorwaardelijk te controleren of u in de browser draait of gedrag op de server uitschakelt, kunt u gewoon afterNextRender gebruiken:

@Component({
  /* ... */
})
export class MyComponent {
  @ViewChild('chart') chartRef: ElementRef;
  myChart: MyChart|null = null;
  
  constructor() {
    afterNextRender(() => {
      this.myChart = new MyChart(this.chartRef.nativeElement);
    });
  }
}

Voer een aangepaste lay-out uit

Soms moet u mogelijk naar de DOM lezen of schrijven om een ​​bepaalde lay-out uit te voeren die uw doelbrowsers nog niet ondersteunen, zoals het positioneren van knopinfo. afterRender is hiervoor een goede keuze, omdat u er zeker van kunt zijn dat de DOM zich in een consistente staat bevindt. afterRender en afterNextRender accepteren een phase van EarlyRead , Read of Write . Het lezen van de DOM-lay-out na het schrijven ervan dwingt de browser om de lay-out synchroon opnieuw te berekenen, wat de prestaties ernstig kan beïnvloeden (zie: lay-out thrashing ). Daarom is het belangrijk om uw logica zorgvuldig in de juiste fasen op te delen.

Een tooltipcomponent die tooltip wil weergeven ten opzichte van een ander element op de pagina zou bijvoorbeeld waarschijnlijk twee fasen gebruiken. De EarlyRead fase zou eerst worden gebruikt om de grootte en positie van de elementen te verkrijgen:

afterRender(() => {
    targetRect = targetEl.getBoundingClientRect();
    tooltipRect = tooltipEl.getBoundingClientRect();
  }, { phase: AfterRenderPhase.EarlyRead },
);

Vervolgens zou de Write de eerder gelezen waarde gebruiken om de tooltip daadwerkelijk te herpositioneren:

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 },
);

Door onze logica in de juiste fasen op te splitsen, kan Angular DOM-manipulatie effectief in batches over elk ander onderdeel van de applicatie verdelen, waardoor een minimale impact op de prestaties wordt gegarandeerd.

Conclusie

Er zijn veel nieuwe en opwindende verbeteringen aan de Angular-weergave aan de serverzijde in het verschiet, met als doel het voor u gemakkelijker te maken om uw gebruikers een geweldige ervaring te bieden. We hopen dat de voorgaande tips nuttig zullen zijn om u te helpen er optimaal gebruik van te maken in uw toepassingen en bibliotheken!