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 al DOM sono problematici e a volte sono necessari. La chiave per utilizzare il DOM in sicurezza è ridurne al minimo l'utilizzo, posticipando il più possibile il suo utilizzo. 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 e future 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. È allettante utilizzare 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. Valuta bene quando e dove altri componenti (anche all'interno della stessa applicazione o libreria) potrebbero dover interagire con il comportamento o l'aspetto del tuo componente o personalizzarli, quindi esponi un modo sicuro e documentato per farlo. Utilizza funzionalità come l'iniezione di dipendenze gerarchica per rendere disponibile un'API per un sottoalbero quando le semplici proprietà @Input
e @Output
non sono sufficienti.
In passato, era comune implementare funzionalità come finestre di dialogo modali o descrizioni comando aggiungendo un elemento alla fine di <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 visualizzare 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 accedere il più possibile, potresti avere alcune manipolazione 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). Anziché verificare in modo condizionale se stai eseguendo l'app nel browser o simulare il comportamento sul server, puoi semplicemente utilizzare afterNextRender
:
@Component({
/* ... */
})
export class MyComponent {
@ViewChild('chart') chartRef: ElementRef;
myChart: MyChart|null = null;
constructor() {
afterNextRender(() => {
this.myChart = new MyChart(this.chartRef.nativeElement);
});
}
}
Esegui un layout personalizzato
A volte potrebbe essere necessario leggere o scrivere nel DOM per eseguire alcuni layout non ancora supportati 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 del DOM dopo la scrittura costringe il browser a ricalcolare il layout in modo sincrono, il che può influire notevolmente sulle prestazioni (vedi thrashing del layout). Pertanto, è importante suddividere attentamente la logica nelle fasi corrette.
Ad esempio, un componente della descrizione comando che vuole visualizzare una descrizione comando relativa a un altro elemento della pagina probabilmente utilizzerà due fasi. La fase EarlyRead
viene utilizzata inizialmente 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 sfruttare al meglio queste funzionalità nelle tue applicazioni e librerie.