Nell'ultimo anno, Angular ha acquisito molte nuove funzionalità, come l'idratazione e le visualizzazioni differibili, per aiutare gli sviluppatori a migliorare i Core Web Vitals e garantire un'esperienza eccezionale agli utenti finali. Sono in corso anche ricerche su altre funzionalità correlate al rendering lato server che si basano su questa funzionalità, come lo streaming e l'idratazione parziale.
Purtroppo, esiste un pattern che potrebbe impedire alla tua applicazione o libreria di sfruttare appieno tutte queste funzionalità nuove e future: la manipolazione manuale della struttura DOM sottostante. Angular richiede che la struttura del DOM rimanga coerente dal momento in cui un componente viene serializzato dal server fino a quando non viene attivato nel browser. L'utilizzo di ElementRef
, Renderer2
o delle API DOM per aggiungere, spostare o rimuovere manualmente i nodi dal DOM prima dell'idratazione può introdurre incoerenze che impediscono il funzionamento di queste funzionalità.
Tuttavia, non tutte le manipolazioni e gli accessi manuali del DOM sono problematici e talvolta sono necessari. La chiave per utilizzare il DOM in modo sicuro è ridurre al minimo la necessità di utilizzarlo il più possibile e quindi rinviarlo il più a lungo possibile. Le seguenti linee guida spiegano come raggiungere questo obiettivo e creare componenti Angular veramente universali e adatti al futuro che possono sfruttare appieno tutte le funzionalità nuove ed emergenti di Angular.
Evita la manipolazione manuale del DOM
Il modo migliore per evitare i problemi causati dalla manipolazione manuale del DOM è, non a caso, evitarla del tutto, ove possibile. Angular dispone di API e pattern integrati che possono manipolare la maggior parte degli aspetti del DOM: ti consigliamo di utilizzarli anziché accedere direttamente al DOM.
Mutare l'elemento DOM di un componente
Quando scrivi un componente o una direttiva, potresti dover modificare l'elemento host (ovvero l'elemento DOM corrispondente al selettore del componente o della direttiva) per, ad esempio, aggiungere una classe, uno stile o un attributo, anziché scegliere come target o introdurre un elemento wrapper. Potresti avere la tentazione di raggiungere ElementRef
per modificare l'elemento DOM sottostante. Dovresti invece utilizzare le associazioni all'host per associare in modo dichiarativo i valori a un'espressione:
@Component({
selector: 'my-component',
template: `...`,
host: {
'[class.foo]': 'true'
},
})
export class MyComponent {
/* ... */
}
Come per il collegamento dati in HTML, puoi anche, ad esempio, eseguire il collegamento ad attributi e stili e modificare 'true'
in un'espressione diversa che Angular utilizzerà per aggiungere o rimuovere automaticamente il valore in base alle esigenze.
In alcuni casi, la chiave dovrà essere calcolata dinamicamente. Puoi anche eseguire il binding a un indicatore o a una funzione che restituisce un insieme o una mappa di valori:
@Component({
selector: 'my-component',
template: `...`,
host: {
'[class.foo]': 'true',
'[class]': 'classes()'
},
})
export class MyComponent {
size = signal('large');
classes = computed(() => {
return [`size-${this.size()}`];
});
}
In applicazioni più complesse, potrebbe essere allettante ricorrere alla manipolazione manuale del DOM per evitare un ExpressionChangedAfterItHasBeenCheckedError
. In alternativa, puoi associare il valore a un indicatore come nell'esempio precedente. Questa operazione può essere eseguita in base alle esigenze e non richiede l'adozione degli indicatori nell'intera base di codice.
Modificare gli elementi DOM esterni a un modello
È allettante provare a utilizzare il DOM per accedere a elementi normalmente non accessibili, ad esempio quelli che appartengono ad altri componenti principali o secondari. Tuttavia, questo approccio è soggetto a errori, viola l'incapsulamento e rende difficile modificare o eseguire l'upgrade di questi componenti in futuro.
Il componente deve invece considerare tutti gli altri componenti come scatole nere. Prenditi il tempo necessario per valutare quando e dove altri componenti (anche all'interno della stessa applicazione o libreria) potrebbero aver bisogno di interagire o personalizzare il comportamento o l'aspetto del tuo componente, quindi esponi un modo sicuro e documentato per farlo. Utilizza funzionalità come l'inserimento delle dipendenze gerarchiche per rendere disponibile un'API in un sottoalbero quando non sono sufficienti proprietà @Input
e @Output
semplici.
In passato, era comune implementare funzionalità come finestre di dialogo modali o descrizioni comando aggiungendo un elemento alla fine del <body>
o di un altro elemento host e poi spostando o proiettando i contenuti al suo interno. Tuttavia, al giorno d'oggi è probabile che tu possa eseguire il rendering di un semplice elemento <dialog>
nel tuo modello:
@Component({
selector: 'my-component',
template: `<dialog #dialog>Hello World</dialog>`,
})
export class MyComponent {
@ViewChild('dialog') dialogRef!: ElementRef;
constructor() {
afterNextRender(() => {
this.dialogRef.nativeElement.showModal();
});
}
}
Rimandare la manipolazione manuale del DOM
Dopo aver utilizzato le linee guida precedenti per ridurre al minimo la manipolazione diretta del DOM e l'accesso il più possibile, potresti avere alcune restrizioni inevitabili. In questi casi, è importante posticipare il più a lungo possibile. I callback afterRender
e afterNextRender
sono un ottimo modo per farlo, in quanto vengono eseguiti solo nel browser, dopo che Angular ha controllato eventuali modifiche e le ha applicate al DOM.
Eseguire JavaScript solo nel browser
In alcuni casi, una libreria o un'API funziona solo nel browser (ad esempio una libreria di grafici, alcuni utilizzi di IntersectionObserver
e così via). Invece di verificare in modo condizionale se è in esecuzione sul browser o di bloccare il comportamento del server, puoi utilizzare afterNextRender
:
@Component({
/* ... */
})
export class MyComponent {
@ViewChild('chart') chartRef: ElementRef;
myChart: MyChart|null = null;
constructor() {
afterNextRender(() => {
this.myChart = new MyChart(this.chartRef.nativeElement);
});
}
}
Layout personalizzato
A volte potrebbe essere necessario leggere o scrivere nel DOM per eseguire un layout non ancora supportato dai browser di destinazione, ad esempio il posizionamento di una descrizione comando. afterRender
è un'ottima scelta per questo, in quanto puoi avere la certezza che il DOM sia in uno stato coerente. afterRender
e afterNextRender
accettano un valore phase
pari a EarlyRead
, Read
o Write
. La lettura del layout DOM dopo la scrittura costringe il browser a ricalcolare il layout in modo sincrono, il che può influire seriamente sulle prestazioni (vedi thrashing del layout). Pertanto, è importante suddividere attentamente la logica nelle fasi corrette.
Ad esempio, un componente della descrizione comando che vuole mostrare una descrizione comando relativa a un altro elemento della pagina probabilmente utilizzerà due fasi. La fase EarlyRead
viene utilizzata innanzitutto per acquisire le dimensioni e la posizione degli elementi:
afterRender(() => {
targetRect = targetEl.getBoundingClientRect();
tooltipRect = tooltipEl.getBoundingClientRect();
}, { phase: AfterRenderPhase.EarlyRead },
);
La fase Write
utilizzerà quindi il valore letto in precedenza per riposizionare effettivamente la descrizione comando:
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 },
);
Suddividendo la logica nelle fasi corrette, Angular è in grado di eseguire in batch la manipolazione del DOM in tutti gli altri componenti dell'applicazione, garantendo un impatto minimo sulle prestazioni.
Conclusione
Sono in arrivo molti nuovi e interessanti miglioramenti al rendering lato server di Angular, con l'obiettivo di semplificare l'offerta di un'esperienza eccezionale agli utenti. Ci auguriamo che i suggerimenti precedenti ti siano utili per sfruttarli appieno nelle tue applicazioni e librerie.