Componenti
Struttura
I componenti sono un meccanismo per incapsulare parti di una vista, rendendo il codice più facile da organizzare e/o riutilizzare.
Qualsiasi oggetto JavaScript che abbia un metodo view
è un componente Mithril.js. I componenti possono essere utilizzati tramite l'utility m()
:
// definisci il tuo componente
var Example = {
view: function (vnode) {
return m('div', 'Hello');
},
};
// utilizza il tuo componente
m(Example);
// HTML equivalente
// <div>Hello</div>
Metodi del ciclo di vita
I componenti possono avere gli stessi metodi del ciclo di vita dei nodi del DOM virtuale. Si noti che vnode
viene passato come argomento a ciascun metodo del ciclo di vita, inclusa view
(a onbeforeupdate
viene passato anche il precedente vnode):
var ComponentWithHooks = {
oninit: function (vnode) {
console.log('initialized');
},
oncreate: function (vnode) {
console.log('DOM created');
},
onbeforeupdate: function (newVnode, oldVnode) {
return true;
},
onupdate: function (vnode) {
console.log('DOM updated');
},
onbeforeremove: function (vnode) {
console.log('exit animation can start');
return new Promise(function (resolve) {
// chiama dopo che l'animazione è completa
resolve();
});
},
onremove: function (vnode) {
console.log('removing DOM element');
},
view: function (vnode) {
return 'hello';
},
};
Come altri tipi di nodi del DOM virtuale, i componenti possono avere metodi del ciclo di vita aggiuntivi definiti quando vengono utilizzati come tipi di vnode.
function initialize(vnode) {
console.log('initialized as vnode');
}
m(ComponentWithHooks, { oninit: initialize });
I metodi del ciclo di vita nei vnode non sovrascrivono i metodi dei componenti, né viceversa. I metodi del ciclo di vita dei componenti vengono sempre eseguiti dopo il metodo corrispondente del vnode.
Prestare attenzione a non riutilizzare i nomi dei metodi del ciclo di vita per le proprie funzioni di callback nei vnode.
Per saperne di più sui metodi del ciclo di vita, vedi la pagina dei metodi del ciclo di vita.
Passaggio di dati ai componenti
I dati possono essere passati alle istanze dei componenti passando un oggetto attrs
come secondo parametro nella funzione hyperscript:
m(Example, { name: 'Floyd' });
Questi dati sono accessibili nella vista del componente o nei metodi del ciclo di vita tramite vnode.attrs
:
var Example = {
view: function (vnode) {
return m('div', 'Hello, ' + vnode.attrs.name);
},
};
NOTA: I metodi del ciclo di vita possono anche essere definiti nell'oggetto attrs
, quindi è necessario evitare di utilizzare i loro nomi per le proprie callback poiché verrebbero invocati anche da Mithril.js stesso. Usarli in attrs
solo quando si desidera specificamente utilizzarli come metodi del ciclo di vita.
Stato
Come tutti i nodi del DOM virtuale, i vnode dei componenti possono avere uno stato. Lo stato del componente è utile per supportare architetture orientate agli oggetti, per l'incapsulamento e per separare le responsabilità.
Si noti che, a differenza di molti altri framework, la modifica dello stato del componente non attiva redraws o aggiornamenti del DOM. Invece, i redraws vengono eseguiti quando vengono attivati i gestori di eventi, quando le richieste HTTP effettuate da m.request vengono completate o quando il browser naviga verso route diverse. I meccanismi di stato dei componenti di Mithril.js esistono semplicemente come una comodità per le applicazioni.
Se si verifica una modifica dello stato che non è il risultato di nessuna delle condizioni di cui sopra (ad esempio, dopo un setTimeout
), è possibile utilizzare m.redraw()
per attivare manualmente un redraw.
Stato del componente Closure
Negli esempi precedenti, ogni componente è definito come un POJO (Plain Old JavaScript Object), che viene utilizzato internamente da Mithril.js come prototipo per le istanze di quel componente. È possibile utilizzare lo stato del componente con un POJO (come discuteremo di seguito), ma non è l'approccio più pulito o più semplice. Per questo useremo un componente closure, che è semplicemente una funzione di avvolgimento che restituisce un'istanza di componente POJO, che a sua volta ha un proprio scope privato.
Con un componente closure, lo stato può essere semplicemente mantenuto da variabili dichiarate all'interno della funzione esterna:
function ComponentWithState(initialVnode) {
// Variabile di stato del componente, univoca per ogni istanza
var count = 0;
// Istanza di componente POJO: qualsiasi oggetto con una
// funzione view che restituisce un vnode
return {
oninit: function (vnode) {
console.log('init a closure component');
},
view: function (vnode) {
return m(
'div',
m('p', 'Count: ' + count),
m(
'button',
{
onclick: function () {
count += 1;
},
},
'Increment count'
)
);
},
};
}
Qualsiasi funzione dichiarata all'interno della closure ha anche accesso alle sue variabili di stato.
function ComponentWithState(initialVnode) {
var count = 0;
function increment() {
count += 1;
}
function decrement() {
count -= 1;
}
return {
view: function (vnode) {
return m(
'div',
m('p', 'Count: ' + count),
m(
'button',
{
onclick: increment,
},
'Increment'
),
m(
'button',
{
onclick: decrement,
},
'Decrement'
)
);
},
};
}
I componenti closure vengono utilizzati nello stesso modo dei POJO, ad esempio m(ComponentWithState, { passedData: ... })
.
Un grande vantaggio dei componenti closure è che non dobbiamo preoccuparci di associare this
quando si collegano le callback del gestore di eventi. In effetti this
non viene mai usato e non dobbiamo mai pensare alle ambiguità del contesto this
.
Stato del componente POJO
Si raccomanda generalmente di utilizzare le closure per la gestione dello stato del componente. Se, tuttavia, si ha motivo di gestire lo stato in un POJO, lo stato di un componente può essere accessibile in tre modi diversi: come modello all'inizializzazione, tramite vnode.state
e tramite la parola chiave this
nei metodi del componente.
All'inizializzazione
Per i componenti POJO, l'oggetto componente è il prototipo di ogni istanza del componente, quindi qualsiasi proprietà definita sull'oggetto componente sarà accessibile come proprietà di vnode.state
. Ciò permette una semplice inizializzazione dello stato come 'modello'.
Nell'esempio seguente, data
diventa una proprietà dell'oggetto vnode.state
del componente ComponentWithInitialState
.
var ComponentWithInitialState = {
data: 'Initial content',
view: function (vnode) {
return m('div', vnode.state.data);
},
};
m(ComponentWithInitialState);
// HTML equivalente
// <div>Initial content</div>
Tramite vnode.state
Come puoi vedere, lo stato può anche essere accessibile tramite la proprietà vnode.state
, che è disponibile per tutti i metodi del ciclo di vita così come per il metodo view
di un componente.
var ComponentWithDynamicState = {
oninit: function (vnode) {
vnode.state.data = vnode.attrs.text;
},
view: function (vnode) {
return m('div', vnode.state.data);
},
};
m(ComponentWithDynamicState, { text: 'Hello' });
// HTML equivalente
// <div>Hello</div>
Tramite la parola chiave this
Lo stato può anche essere accessibile tramite la parola chiave this
, che è disponibile per tutti i metodi del ciclo di vita così come per il metodo view
di un componente.
var ComponentUsingThis = {
oninit: function (vnode) {
this.data = vnode.attrs.text;
},
view: function (vnode) {
return m('div', this.data);
},
};
m(ComponentUsingThis, { text: 'Hello' });
// HTML equivalente
// <div>Hello</div>
Bisogna tenere presente che quando si utilizzano funzioni ES5, il valore di this
nelle funzioni anonime nidificate non è l'istanza del componente. Ci sono due modi raccomandati per aggirare questa limitazione di JavaScript: usare le arrow functions oppure, se queste non sono supportate, usare vnode.state
.
Classi
Se si adatta alle tue esigenze (come nei progetti orientati agli oggetti), i componenti possono anche essere scritti usando le classi:
class ClassComponent {
constructor(vnode) {
this.kind = 'class component';
}
view() {
return m('div', `Hello from a ${this.kind}`);
}
oncreate() {
console.log(`A ${this.kind} was created`);
}
}
I componenti di classe devono definire un metodo view()
, rilevato tramite .prototype.view
, per ottenere l'albero da renderizzare.
Possono essere usati nello stesso modo dei componenti normali.
// ESEMPIO: tramite m.render
m.render(document.body, m(ClassComponent));
// ESEMPIO: tramite m.mount
m.mount(document.body, ClassComponent);
// ESEMPIO: tramite m.route
m.route(document.body, '/', {
'/': ClassComponent,
});
// ESEMPIO: composizione di componenti
class AnotherClassComponent {
view() {
return m('main', [m(ClassComponent)]);
}
}
Stato del componente di classe
Con le classi, lo stato può essere gestito dalle proprietà e dai metodi dell'istanza della classe e accessibile tramite this
:
class ComponentWithState {
constructor(vnode) {
this.count = 0;
}
increment() {
this.count += 1;
}
decrement() {
this.count -= 1;
}
view() {
return m(
'div',
m('p', 'Count: ', this.count),
m(
'button',
{
onclick: () => {
this.increment();
},
},
'Increment'
),
m(
'button',
{
onclick: () => {
this.decrement();
},
},
'Decrement'
)
);
}
}
Si noti che dobbiamo usare le arrow functions per le callback del gestore di eventi in modo che il contesto this
possa essere referenziato correttamente.
Combinazione di tipi di componenti
I componenti possono essere combinati. Un componente di classe può avere componenti closure o POJO come figli, ecc.
Attributi speciali
Mithril.js attribuisce una semantica speciale a diverse chiavi di proprietà, quindi normalmente dovresti evitare di usarle negli attributi normali dei componenti.
- Metodi del ciclo di vita:
oninit
,oncreate
,onbeforeupdate
,onupdate
,onbeforeremove
eonremove
key
, che viene utilizzato per tracciare l'identità nei frammenti con chiavetag
, che viene utilizzato per distinguere i vnode dagli oggetti attributi normali e da altre entità che sono oggetti non-vnode.
Evitare anti-pattern
Sebbene Mithril.js sia flessibile, alcuni pattern di codice sono sconsigliati:
Evitare componenti pesanti
In generale, un componente "pesante" (fat) è un componente che ha metodi di istanza personalizzati. In altre parole, dovresti evitare di collegare funzioni a vnode.state
o this
. È estremamente raro avere una logica che si adatti logicamente a un metodo di istanza del componente e che non possa essere riutilizzata da altri componenti. È relativamente comune che tale logica possa essere necessaria da un componente diverso in futuro.
È più facile rifattorizzare il codice se quella logica è posizionata nel livello dati piuttosto che se è legata a uno stato del componente.
Considera questo componente pesante:
// views/Login.js
// EVITARE
var Login = {
username: '',
password: '',
setUsername: function (value) {
this.username = value;
},
setPassword: function (value) {
this.password = value;
},
canSubmit: function () {
return this.username !== '' && this.password !== '';
},
login: function () {
/*...*/
},
view: function () {
return m('.login', [
m('input[type=text]', {
oninput: function (e) {
this.setUsername(e.target.value);
},
value: this.username,
}),
m('input[type=password]', {
oninput: function (e) {
this.setPassword(e.target.value);
},
value: this.password,
}),
m(
'button',
{ disabled: !this.canSubmit(), onclick: this.login },
'Login'
),
]);
},
};
Normalmente, nel contesto di un'applicazione più grande, un componente di login come quello sopra esiste insieme a componenti per la registrazione dell'utente e il recupero della password. Immagina di voler essere in grado di precompilare il campo email quando si naviga dalla schermata di login alle schermate di registrazione o di recupero della password (o viceversa), in modo che l'utente non debba digitare nuovamente la propria email se si è imbattuto nella pagina sbagliata (o forse vuoi indirizzare l'utente al modulo di registrazione se non viene trovato un username).
Vediamo subito che condividere i valori dei campi username
e password
da questo componente a un altro è difficile. Questo perché il componente 'pesante' incapsula il proprio stato, il che per definizione rende questo stato difficile da accedere dall'esterno.
È più opportuno rifattorizzare questo componente ed estrarre il codice di stato dal componente e nel livello dati dell'applicazione. Questo può essere semplice come creare un nuovo modulo:
// models/Auth.js
// PREFERIRE
var Auth = {
username: '',
password: '',
setUsername: function (value) {
Auth.username = value;
},
setPassword: function (value) {
Auth.password = value;
},
canSubmit: function () {
return Auth.username !== '' && Auth.password !== '';
},
login: function () {
/*...*/
},
};
module.exports = Auth;
Quindi, possiamo pulire il componente:
// views/Login.js
// PREFERIRE
var Auth = require('../models/Auth');
var Login = {
view: function () {
return m('.login', [
m('input[type=text]', {
oninput: function (e) {
Auth.setUsername(e.target.value);
},
value: Auth.username,
}),
m('input[type=password]', {
oninput: function (e) {
Auth.setPassword(e.target.value);
},
value: Auth.password,
}),
m(
'button',
{
disabled: !Auth.canSubmit(),
onclick: Auth.login,
},
'Login'
),
]);
},
};
In questo modo, il modulo Auth
è ora la fonte di verità per lo stato relativo all'autenticazione e un componente Register
può facilmente accedere a questi dati e persino riutilizzare metodi come canSubmit
, se necessario. Inoltre, se è richiesto un codice di validazione (ad esempio, per il campo email), è sufficiente modificare setEmail
e tale modifica eseguirà la validazione dell'email per qualsiasi componente che modifica un campo email.
Come bonus, si noti che non abbiamo più bisogno di usare .bind
per mantenere un riferimento allo stato per i gestori di eventi del componente.
Non inoltrare vnode.attrs
stesso ad altri vnodes
A volte, per mantenere un'interfaccia flessibile e semplificare l'implementazione, si potrebbe voler inoltrare gli attributi a un particolare componente o elemento figlio, in questo caso il modal di Bootstrap. Potrebbe essere allettante inoltrare gli attributi di un vnode in questo modo:
// EVITARE
var Modal = {
// ...
view: function (vnode) {
return m('.modal[tabindex=-1][role=dialog]', vnode.attrs, [
// inoltrando `vnode.attrs` qui ^
// ...
]);
},
};
Se lo fai come sopra, potresti riscontrare problemi quando lo usi:
var MyModal = {
view: function () {
return m(
Modal,
{
// Questo lo attiva due volte, quindi non viene visualizzato
onupdate: function (vnode) {
if (toggle) $(vnode.dom).modal('toggle');
},
},
[
// ...
]
);
},
};
Invece, dovresti inoltrare singoli attributi nei vnode:
// PREFERIRE
var Modal = {
// ...
view: function (vnode) {
return m('.modal[tabindex=-1][role=dialog]', vnode.attrs.attrs, [
// inoltrando `attrs:` qui ^
// ...
]);
},
};
// Esempio
var MyModal = {
view: function () {
return m(Modal, {
attrs: {
// Questo lo attiva una volta
onupdate: function (vnode) {
if (toggle) $(vnode.dom).modal('toggle');
},
},
// ...
});
},
};
Non manipolare children
Se un componente gestisce in modo specifico gli attributi o i children, dovresti passare all'uso di attributi personalizzati.
Spesso è desiderabile definire più set di children, ad esempio, se un componente ha un titolo e un corpo configurabili.
Evita di destrutturare la proprietà children
per questo scopo.
// EVITARE
var Header = {
view: function (vnode) {
return m('.section', [
m('.header', vnode.children[0]),
m('.tagline', vnode.children[1]),
]);
},
};
m(Header, [m('h1', 'My title'), m('h2', 'Lorem ipsum')]);
// caso d'uso di consumo scomodo
m(Header, [
[m('h1', 'My title'), m('small', 'A small note')],
m('h2', 'Lorem ipsum'),
]);
Il componente sopra rompe l'assunzione che i children verranno emessi nello stesso formato contiguo in cui vengono ricevuti. È difficile capire il componente senza leggere la sua implementazione. Invece, usa gli attributi come parametri denominati e riserva children
per contenuti figlio uniformi:
// PREFERIRE
var BetterHeader = {
view: function (vnode) {
return m('.section', [
m('.header', vnode.attrs.title),
m('.tagline', vnode.attrs.tagline),
]);
},
};
m(BetterHeader, {
title: m('h1', 'My title'),
tagline: m('h2', 'Lorem ipsum'),
});
// caso d'uso di consumo più chiaro
m(BetterHeader, {
title: [m('h1', 'My title'), m('small', 'A small note')],
tagline: m('h2', 'Lorem ipsum'),
});
Definisci i componenti staticamente, chiamali dinamicamente
Evita di creare definizioni di componenti all'interno delle view
Se crei un componente dall'interno di un metodo view
(direttamente inline o chiamando una funzione che lo fa), ogni redraw avrà un clone diverso del componente. Quando si confrontano i vnode dei componenti, se il componente a cui fa riferimento il nuovo vnode non è strettamente uguale a quello a cui fa riferimento il vecchio componente, si presume che i due siano componenti diversi anche se alla fine eseguono codice equivalente. Ciò significa che i componenti creati dinamicamente tramite una factory verranno sempre ricreati da zero.
Per questo motivo è consigliabile evitare di ricreare i componenti. Invece, utilizzali nel modo standard.
// EVITARE
var ComponentFactory = function (greeting) {
// crea un nuovo componente ad ogni chiamata
return {
view: function () {
return m('div', greeting);
},
};
};
m.render(document.body, m(ComponentFactory('hello')));
// chiamare una seconda volta ricrea div da zero invece di non fare nulla
m.render(document.body, m(ComponentFactory('hello')));
// PREFERIRE
var Component = {
view: function (vnode) {
return m('div', vnode.attrs.greeting);
},
};
m.render(document.body, m(Component, { greeting: 'hello' }));
// chiamare una seconda volta non modifica il DOM
m.render(document.body, m(Component, { greeting: 'hello' }));
Evita di creare istanze di componenti al di fuori delle view
Al contrario, per motivi analoghi, se un'istanza di componente viene creata al di fuori di una view, i successivi redraw eseguiranno una verifica di uguaglianza sul nodo e lo ignoreranno. Pertanto, le istanze dei componenti dovrebbero sempre essere create all'interno delle view:
// EVITARE
var Counter = {
count: 0,
view: function (vnode) {
return m(
'div',
m('p', 'Count: ' + vnode.state.count),
m(
'button',
{
onclick: function () {
vnode.state.count++;
},
},
'Increase count'
)
);
},
};
var counter = m(Counter);
m.mount(document.body, {
view: function (vnode) {
return [m('h1', 'My app'), counter];
},
});
Nell'esempio sopra, fare clic sul pulsante del componente contatore aumenterà il suo conteggio di stato, ma la sua view non verrà attivata perché il vnode che rappresenta il componente condivide lo stesso riferimento e quindi il processo di rendering non li confronta. Dovresti sempre chiamare i componenti nella view per assicurarti che venga creato un nuovo vnode:
// PREFERIRE
var Counter = {
count: 0,
view: function (vnode) {
return m(
'div',
m('p', 'Count: ' + vnode.state.count),
m(
'button',
{
onclick: function () {
vnode.state.count++;
},
},
'Increase count'
)
);
},
};
m.mount(document.body, {
view: function (vnode) {
return [m('h1', 'My app'), m(Counter)];
},
});