Applicazione semplice
Sviluppiamo un'applicazione semplice per illustrare le principali funzionalità di Mithril.
Un esempio interattivo del risultato finale è disponibile qui
Innanzitutto, creiamo un file di ingresso per l'applicazione. Crea un file index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>La mia applicazione</title>
</head>
<body>
<script src="bin/app.js"></script>
</body>
</html>
La riga <!doctype html>
indica che si tratta di un documento HTML5. Il primo meta tag charset
specifica la codifica del documento, mentre il meta tag viewport
definisce come i browser mobili devono scalare la pagina. Il tag title
contiene il testo da visualizzare nella scheda del browser per questa applicazione e il tag script
indica il percorso del file JavaScript che controlla l'applicazione.
Potremmo creare l'intera applicazione in un singolo file JavaScript, ma ciò renderebbe difficile la navigazione nel codice in seguito. Invece, divideremo il codice in moduli e assembleremo questi moduli per creare un bundle bin/app.js
.
Esistono molti modi per configurare uno strumento per creare un bundle, ma la maggior parte sono distribuiti tramite npm. Infatti, la maggior parte delle librerie e degli strumenti JavaScript moderni sono distribuiti in questo modo, incluso Mithril. Per scaricare npm, installa Node.js; npm viene installato automaticamente con esso. Una volta installati Node.js e npm, apri la riga di comando ed esegui questo comando:
npm init -y
Se npm è installato correttamente, verrà creato un file package.json
. Questo file conterrà una descrizione schematica del progetto. Puoi modificare le informazioni sul progetto e sull'autore in questo file.
Per installare Mithril.js, segui le istruzioni nella pagina installazione. Una volta che hai uno scheletro di progetto con Mithril.js installato, siamo pronti per creare l'applicazione.
Iniziamo creando un modulo per memorizzare il nostro stato. Crea un file chiamato src/models/User.js
:
// src/models/User.js
var User = {
list: [],
};
module.exports = User;
Ora aggiungiamo del codice per caricare alcuni dati da un server. Per comunicare con un server, possiamo usare l'utility XHR di Mithril.js, chiamata m.request
. Per prima cosa, includiamo Mithril.js nel modulo:
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
};
module.exports = User;
Successivamente, creiamo una funzione che eseguirà una chiamata XHR. Chiamiamola loadList
:
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
// TODO: make XHR call
},
};
module.exports = User;
Quindi possiamo aggiungere una chiamata a m.request
per effettuare una richiesta XHR. Per questo tutorial, effettueremo chiamate XHR all'API REST simulata REM (DEAD LINK, FIXME: https //rem-rest-api.herokuapp.com/), progettata per la prototipazione rapida. Questa API restituisce un elenco di utenti dall'endpoint GET https://mithril-rem.fly.dev/api/users
. Usiamo m.request
per effettuare una richiesta XHR e popolare i nostri dati con la risposta di quell'endpoint.
Nota: i cookie di terze parti potrebbero dover essere abilitati affinché l'endpoint REM funzioni.
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users',
withCredentials: true,
})
.then(function (result) {
User.list = result.data;
});
},
};
module.exports = User;
L'opzione method
è un metodo HTTP. Per recuperare i dati dal server senza causare effetti collaterali sul server, dobbiamo usare il metodo GET
. L'url
è l'indirizzo dell'endpoint API. La riga withCredentials: true
indica che stiamo usando i cookie (che è un requisito per l'API REM).
La chiamata m.request
restituisce una Promise che si risolve nei dati dall'endpoint. Per impostazione predefinita, Mithril.js presuppone che un corpo di risposta HTTP sia in formato JSON e lo analizzi automaticamente in un oggetto o array JavaScript. La callback .then
viene eseguita quando la richiesta XHR è completa. In questo caso, la callback assegna l'array result.data
a User.list
.
Si noti che abbiamo anche un'istruzione return
in loadList
. Questa è una buona pratica generale quando si lavora con le Promise, in quanto ci consente di registrare più callback da eseguire dopo il completamento della richiesta XHR.
Questo semplice modello espone due proprietà: User.list
(un array di oggetti utente) e User.loadList
(un metodo che popola User.list
con i dati del server).
Ora, creiamo un modulo di visualizzazione in modo da poter visualizzare i dati dal nostro modulo modello Utente.
Crea un file chiamato src/views/UserList.js
. Per iniziare, includiamo Mithril.js e il nostro modello, poiché dovremo usare entrambi:
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
Successivamente, creiamo un componente Mithril.js. Un componente è semplicemente un oggetto che ha un metodo view
:
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
view: function () {
// TODO add code here
},
};
Per impostazione predefinita, le viste Mithril.js sono descritte usando hyperscript. Hyperscript offre una sintassi concisa che può essere indentata più naturalmente dell'HTML per tag complessi e, poiché la sua sintassi è solo JavaScript, è possibile sfruttare gran parte dell'ecosistema di strumenti JavaScript. Per esempio:
- Puoi usare Babel per transpilare ES6+ in ES5 per IE e per transpilare JSX (un'estensione di sintassi inline simile a HTML) in chiamate hyperscript appropriate.
- Puoi usare ESLint per un facile linting senza plugin speciali.
- Puoi usare Terser o UglifyJS (solo ES5) per minimizzare facilmente il tuo codice.
- Puoi usare Istanbul per la code coverage.
- Puoi usare TypeScript per una facile analisi del codice. (Ci sono definizioni di tipo supportate dalla comunità disponibili, quindi non è necessario crearne di proprie.)
Iniziamo con hyperscript e creiamo un elenco di elementi. Hyperscript è il modo idiomatico di usare Mithril.js, ma JSX funziona in modo abbastanza simile.
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
view: function () {
return m('.user-list');
},
};
La stringa ".user-list"
è un selettore CSS e, come ci si aspetterebbe, .user-list
rappresenta una classe. Quando un tag non è specificato, div
è il valore predefinito. Quindi questa vista è equivalente a <div class="user-list"></div>
.
Ora, facciamo riferimento all'elenco di utenti dal modello che abbiamo creato in precedenza (User.list
) per scorrere dinamicamente i dati:
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
view: function () {
return m(
'.user-list',
User.list.map(function (user) {
return m('.user-list-item', user.firstName + ' ' + user.lastName);
})
);
},
};
Poiché User.list
è un array JavaScript e poiché le viste hyperscript sono solo JavaScript, possiamo scorrere l'array usando il metodo .map
. Questo crea un array di vnode che rappresenta un elenco di div
, ognuno contenente il nome di un utente.
Il problema, ovviamente, è che non abbiamo mai chiamato la funzione User.loadList
. Pertanto, User.list
è ancora un array vuoto, e quindi questa vista renderebbe una pagina vuota. Poiché vogliamo che User.loadList
venga chiamato quando rendiamo questo componente, possiamo sfruttare i metodi del ciclo di vita del componente:
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
oninit: User.loadList,
view: function () {
return m(
'.user-list',
User.list.map(function (user) {
return m('.user-list-item', user.firstName + ' ' + user.lastName);
})
);
},
};
Nota che abbiamo aggiunto un metodo oninit
al componente, che fa riferimento a User.loadList
. Questo significa che quando il componente si inizializza, User.loadList
verrà chiamato, attivando una richiesta XHR. Quando il server restituisce una risposta, User.list
viene popolato.
Inoltre, nota che non abbiamo fatto oninit: User.loadList()
(con le parentesi alla fine). La differenza è che oninit: User.loadList()
chiamerebbe la funzione immediatamente, mentre oninit: User.loadList
chiama quella funzione solo quando il componente viene renderizzato. Questa è una differenza importante e un errore comune per gli sviluppatori nuovi a JavaScript: chiamare la funzione immediatamente significa che la richiesta XHR si attiverà non appena il codice sorgente viene valutato, anche se il componente non viene mai renderizzato. Inoltre, se il componente viene mai ricreato (navigando avanti e indietro attraverso l'applicazione), la funzione non verrà chiamata di nuovo come previsto.
Renderizziamo la vista dal file di punto di ingresso src/index.js
che abbiamo creato in precedenza:
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
m.mount(document.body, UserList);
La chiamata m.mount
rende il componente specificato (UserList
) in un elemento DOM (document.body
), cancellando qualsiasi DOM che fosse presente in precedenza. L'apertura del file HTML in un browser dovrebbe ora visualizzare un elenco di nomi di persone.
In questo momento, l'elenco sembra piuttosto semplice perché non abbiamo definito nessuno stile. Quindi aggiungiamone alcuni. Creiamo prima un file chiamato styles.css
e includiamolo nel file index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>La mia applicazione</title>
<link href="styles.css" rel="stylesheet" />
</head>
<body>
<script src="bin/app.js"></script>
</body>
</html>
Ora possiamo stilizzare il componente UserList
:
.user-list {
list-style: none;
margin: 0 0 10px;
padding: 0;
}
.user-list-item {
background: #fafafa;
border: 1px solid #ddd;
color: #333;
display: block;
margin: 0 0 1px;
padding: 8px 15px;
text-decoration: none;
}
.user-list-item:hover {
text-decoration: underline;
}
Ricaricare la finestra del browser ora dovrebbe visualizzare alcuni elementi stilizzati.
Aggiungiamo il routing alla nostra applicazione.
Routing significa associare una schermata a un URL univoco, per creare la possibilità di passare da una "pagina" all'altra. Mithril.js è progettato per le Single Page Applications (SPA), quindi queste "pagine" non sono necessariamente diversi file HTML nel senso tradizionale della parola. Invece, il routing nelle Single Page Applications mantiene lo stesso file HTML per tutta la sua durata, ma cambia lo stato dell'applicazione tramite JavaScript. Il routing lato client offre il vantaggio di evitare schermate vuote lampeggianti durante le transizioni di pagina e può ridurre la quantità di dati inviati dal server quando viene utilizzato in combinazione con un'architettura orientata ai servizi web (ovvero, un'applicazione che scarica i dati come JSON invece di scaricare blocchi pre-renderizzati di HTML verboso).
Possiamo aggiungere il routing cambiando la chiamata m.mount
in una chiamata m.route
:
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
m.route(document.body, '/list', {
'/list': UserList,
});
La chiamata m.route
specifica che l'applicazione verrà renderizzata in document.body
. L'argomento "/list"
è la route predefinita. Ciò significa che l'utente verrà reindirizzato a quella route se raggiunge una route inesistente. L'oggetto {"/list": UserList}
dichiara un elenco di route esistenti e a quali componenti ogni route si risolve.
L'aggiornamento della pagina nel browser dovrebbe ora aggiungere #!/list
all'URL per indicare che il routing sta funzionando. Poiché quella route esegue il rendering di UserList, dovremmo ancora vedere l'elenco di persone sullo schermo come prima.
Lo snippet #!
è noto come hashbang, ed è una stringa comunemente usata per implementare il routing lato client. È possibile configurare questa stringa tramite m.route.prefix
. Alcune configurazioni richiedono modifiche lato server di supporto, quindi continueremo a usare l'hashbang per il resto di questo tutorial.
Aggiungiamo un'altra route alla nostra applicazione per la modifica degli utenti. Per prima cosa creiamo un modulo chiamato views/UserForm.js
:
// src/views/UserForm.js
module.exports = {
view: function () {
// TODO implement view
},
};
Quindi possiamo require
questo nuovo modulo da src/index.js
:
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
var UserForm = require('./views/UserForm');
m.route(document.body, '/list', {
'/list': UserList,
});
E infine, possiamo creare una route che vi faccia riferimento:
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
var UserForm = require('./views/UserForm');
m.route(document.body, '/list', {
'/list': UserList,
'/edit/:id': UserForm,
});
Nota che la nuova route ha un :id
al suo interno. Questo è un parametro di route; puoi pensarlo come un carattere jolly; la route /edit/1
si risolverebbe in UserForm
con un id
di "1"
. /edit/2
si risolverebbe anche in UserForm
, ma con un id
di "2"
. E così via.
Implementiamo il componente UserForm
in modo che possa gestire quei parametri di route:
// src/views/UserForm.js
var m = require('mithril');
module.exports = {
view: function () {
return m('form', [
m('label.label', 'First name'),
m('input.input[type=text][placeholder=First name]'),
m('label.label', 'Last name'),
m('input.input[placeholder=Last name]'),
m('button.button[type=submit]', 'Save'),
]);
},
};
E aggiungiamo altri stili a styles.css
:
/* styles.css */
body,
.input,
.button {
font: normal 16px Verdana;
margin: 0;
}
.user-list {
list-style: none;
margin: 0 0 10px;
padding: 0;
}
.user-list-item {
background: #fafafa;
border: 1px solid #ddd;
color: #333;
display: block;
margin: 0 0 1px;
padding: 8px 15px;
text-decoration: none;
}
.user-list-item:hover {
text-decoration: underline;
}
.label {
display: block;
margin: 0 0 5px;
}
.input {
border: 1px solid #ddd;
border-radius: 3px;
box-sizing: border-box;
display: block;
margin: 0 0 10px;
padding: 10px 15px;
width: 100%;
}
.button {
background: #eee;
border: 1px solid #ddd;
border-radius: 3px;
color: #333;
display: inline-block;
margin: 0 0 10px;
padding: 10px 15px;
text-decoration: none;
}
.button:hover {
background: #e8e8e8;
}
In questo momento, questo componente non fa nulla per rispondere agli eventi dell'utente. Aggiungiamo del codice al nostro modello User
in src/models/User.js
. Ecco come appare il codice in questo momento:
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users',
withCredentials: true,
})
.then(function (result) {
User.list = result.data;
});
},
};
module.exports = User;
Aggiungiamo del codice per permetterci di caricare un singolo utente:
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users',
withCredentials: true,
})
.then(function (result) {
User.list = result.data;
});
},
current: {},
load: function (id) {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users/' + id,
withCredentials: true,
})
.then(function (result) {
User.current = result;
});
},
};
module.exports = User;
Nota che abbiamo aggiunto una proprietà User.current
e un metodo User.load(id)
che popola quella proprietà. Ora possiamo popolare la vista UserForm
usando questo nuovo metodo:
// src/views/UserForm.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
oninit: function (vnode) {
User.load(vnode.attrs.id);
},
view: function () {
return m('form', [
m('label.label', 'First name'),
m('input.input[type=text][placeholder=First name]', {
value: User.current.firstName,
}),
m('label.label', 'Last name'),
m('input.input[placeholder=Last name]', { value: User.current.lastName }),
m('button.button[type=submit]', 'Save'),
]);
},
};
Simile al componente UserList
, oninit
chiama User.load()
. Ricorda che avevamo un parametro di route chiamato :id
sulla route "/edit/:id": UserForm
? Il parametro di route diventa un attributo del vnode del componente UserForm
, quindi il routing a /edit/1
farebbe sì che vnode.attrs.id
abbia un valore di "1"
.
Ora, modifichiamo la vista UserList
in modo da poter navigare da lì a un UserForm
:
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
oninit: User.loadList,
view: function () {
return m(
'.user-list',
User.list.map(function (user) {
return m(
m.route.Link,
{
class: 'user-list-item',
href: '/edit/' + user.id,
},
user.firstName + ' ' + user.lastName
);
})
);
},
};
Qui abbiamo sostituito il vnode .user-list-item
con un m.route.Link
con quella classe e gli stessi figli. Abbiamo aggiunto un href
che fa riferimento alla route che vogliamo. Ciò significa che fare clic sul link cambierebbe la parte dell'URL che viene dopo l'hashbang #!
(cambiando così la route senza scaricare la pagina HTML corrente). Dietro le quinte, utilizza un tag <a>
per implementare il link, e tutto funziona in modo semplice.
Se aggiorni la pagina nel browser, dovresti ora essere in grado di fare clic su una persona ed essere portato a un form. Dovresti anche essere in grado di premere il pulsante indietro nel browser per tornare dal form all'elenco di persone.
Il modulo non si salva ancora quando si preme "Salva". Implementiamo questa funzionalità:
// src/views/UserForm.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
oninit: function (vnode) {
User.load(vnode.attrs.id);
},
view: function () {
return m(
'form',
{
onsubmit: function (e) {
e.preventDefault();
User.save();
},
},
[
m('label.label', 'Nome'),
m('input.input[type=text][placeholder=Nome]', {
oninput: function (e) {
User.current.firstName = e.target.value;
},
value: User.current.firstName,
}),
m('label.label', 'Cognome'),
m('input.input[placeholder=Cognome]', {
oninput: function (e) {
User.current.lastName = e.target.value;
},
value: User.current.lastName,
}),
m('button.button[type=submit]', 'Salva'),
]
);
},
};
Abbiamo aggiunto degli eventi oninput
a entrambi gli input per aggiornare le proprietà User.current.firstName
e User.current.lastName
mentre l'utente digita.
Inoltre, abbiamo specificato che il metodo User.save
deve essere invocato quando si preme il pulsante "Salva". Implementiamo questo metodo:
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users',
withCredentials: true,
})
.then(function (result) {
User.list = result.data;
});
},
current: {},
load: function (id) {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users/' + id,
withCredentials: true,
})
.then(function (result) {
User.current = result;
});
},
save: function () {
return m.request({
method: 'PUT',
url: 'https://mithril-rem.fly.dev/api/users/' + User.current.id,
body: User.current,
withCredentials: true,
});
},
};
module.exports = User;
Nel metodo save
, utilizziamo il metodo HTTP PUT
per indicare che stiamo eseguendo un upsert dei dati sul server.
Ora, prova a modificare il nome di un utente nell'applicazione. Dopo aver salvato le modifiche, dovresti vederle riflesse nell'elenco degli utenti.
Attualmente, possiamo tornare all'elenco degli utenti solo tramite il pulsante indietro del browser. Sarebbe ideale avere un menu, o più in generale, un layout in cui inserire elementi UI globali.
Creiamo un file src/views/Layout.js
:
// src/views/Layout.js
var m = require('mithril');
module.exports = {
view: function (vnode) {
return m('main.layout', [
m('nav.menu', [m(m.route.Link, { href: '/list' }, 'Utenti')]),
m('section', vnode.children),
]);
},
};
Questo componente è piuttosto semplice: contiene un <nav>
con un link all'elenco degli utenti. Come per i link /edit
, questo link utilizza m.route.Link
per creare un collegamento che gestisce le rotte.
Si noti la presenza di un elemento <section>
con vnode.children
come elementi figli. vnode
è un riferimento al vnode che rappresenta un'istanza del componente Layout (ovvero, il vnode restituito da una chiamata m(Layout)
). Pertanto, vnode.children
si riferisce a tutti gli elementi figli di quel vnode.
Aggiorniamo anche gli stili:
/* styles.css */
body,
.input,
.button {
font: normal 16px Verdana;
margin: 0;
}
.layout {
margin: 10px auto;
max-width: 1000px;
}
.menu {
margin: 0 0 30px;
}
.user-list {
list-style: none;
margin: 0 0 10px;
padding: 0;
}
.user-list-item {
background: #fafafa;
border: 1px solid #ddd;
color: #333;
display: block;
margin: 0 0 1px;
padding: 8px 15px;
text-decoration: none;
}
.user-list-item:hover {
text-decoration: underline;
}
.label {
display: block;
margin: 0 0 5px;
}
.input {
border: 1px solid #ddd;
border-radius: 3px;
box-sizing: border-box;
display: block;
margin: 0 0 10px;
padding: 10px 15px;
width: 100%;
}
.button {
background: #eee;
border: 1px solid #ddd;
border-radius: 3px;
color: #333;
display: inline-block;
margin: 0 0 10px;
padding: 10px 15px;
text-decoration: none;
}
.button:hover {
background: #e8e8e8;
}
Modifichiamo il router in src/index.js
per integrare il nostro layout:
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
var UserForm = require('./views/UserForm');
var Layout = require('./views/Layout');
m.route(document.body, '/list', {
'/list': {
render: function () {
return m(Layout, m(UserList));
},
},
'/edit/:id': {
render: function (vnode) {
return m(Layout, m(UserForm, vnode.attrs));
},
},
});
Abbiamo sostituito ogni componente con un RouteResolver (in pratica, un oggetto con un metodo render
). I metodi render
possono essere scritti nello stesso modo in cui si scriverebbero le normali view dei componenti, annidando le chiamate m()
.
Un aspetto interessante è che i componenti possono essere usati al posto di una stringa selettore in una chiamata m()
. Qui, nella route /list
, abbiamo m(Layout, m(UserList))
. Questo significa che esiste un vnode radice che rappresenta un'istanza di Layout
, il quale ha un vnode UserList
come unico elemento figlio.
Quindi, se l'URL è /edit/1
, allora vnode.attrs
in questo caso corrisponde a {id: 1}
, e quindi m(UserForm, vnode.attrs)
è equivalente a m(UserForm, {id: 1})
. Il codice JSX equivalente sarebbe <UserForm id={vnode.attrs.id} />
.
Aggiorna la pagina nel browser e ora vedrai la navigazione globale su ogni pagina dell'app.
Questo conclude il tutorial.
In questo tutorial, abbiamo visto come creare un'applicazione molto semplice in cui possiamo elencare gli utenti prelevandoli da un server e modificarli singolarmente. Come esercizio aggiuntivo, prova a implementare la creazione e l'eliminazione degli utenti autonomamente.
Se vuoi vedere altri esempi di codice Mithril.js, consulta la pagina examples. Se hai domande, non esitare a visitare la chat room di Mithril.js.