Jednoduchá aplikace
Vytvořme jednoduchou aplikaci, která ukáže, jak zvládnout většinu hlavních úkolů, které byste potřebovali při používání Mithrilu.
Interaktivní příklad konečného výsledku si můžete prohlédnout zde
Nejprve vytvořme vstupní bod pro aplikaci. Vytvořte soubor index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Moje aplikace</title>
</head>
<body>
<script src="bin/app.js"></script>
</body>
</html>
Řádek <!doctype html>
označuje, že se jedná o dokument HTML 5. První meta tag charset
definuje kódování dokumentu a meta tag viewport
určuje, jak by měly mobilní prohlížeče škálovat stránku. Tag title
obsahuje text, který se zobrazí v záložce prohlížeče pro tuto aplikaci, a tag script
určuje cestu k JavaScript souboru, který ovládá aplikaci.
Celou aplikaci bychom mohli vytvořit v jediném souboru JavaScript, ale to by ztížilo navigaci v kódu. Místo toho rozdělme kód do modulů a sestavme tyto moduly do balíčku bin/app.js
.
Existuje mnoho způsobů, jak nastavit nástroj pro balení kódu, ale většina z nich je distribuována prostřednictvím npm. Ve skutečnosti je většina moderních knihoven a nástrojů JavaScript distribuována tímto způsobem, včetně Mithrilu. Chcete-li stáhnout npm, nainstalujte Node.js; npm se instaluje automaticky s ním. Jakmile máte nainstalovaný Node.js a npm, otevřete příkazový řádek a spusťte tento příkaz:
npm init -y
Pokud je npm správně nainstalován, vytvoří se soubor package.json
. Tento soubor bude obsahovat meta-popis projektu. Neváhejte upravit informace o projektu a autorovi v tomto souboru.
Pro instalaci Mithril.js postupujte podle pokynů na stránce instalace. Jakmile máte kostru projektu s nainstalovaným Mithril.js, jsme připraveni vytvořit aplikaci.
Začněme vytvořením modulu pro uložení našeho stavu. Vytvořme soubor s názvem src/models/User.js
// src/models/User.js
var User = {
list: [],
};
module.exports = User;
Nyní přidejme kód pro načtení dat ze serveru. Pro komunikaci se serverem můžeme použít nástroj XHR Mithril.js, m.request
. Nejprve zahrneme Mithril.js do modulu:
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
};
module.exports = User;
Následně vytvoříme funkci, která provede volání XHR. Pojďme ji nazvat loadList
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
// TODO: make XHR call
},
};
module.exports = User;
Poté můžeme přidat volání m.request
pro provedení požadavku XHR. Pro tento tutoriál budeme provádět volání XHR do mockovací REST API REM (MRTVÝ ODKAZ, FIXME: https //rem-rest-api.herokuapp.com/), navržené pro rychlé prototypování. Toto API vrací seznam uživatelů z koncového bodu GET https://mithril-rem.fly.dev/api/users
. Použijme m.request
k provedení požadavku XHR a naplnění našich dat odpovědí z tohoto koncového bodu.
Poznámka: Pro správnou funkci koncového bodu REM může být nutné povolit cookies třetích stran.
// 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;
Možnost method
je metoda HTTP. Chcete-li načíst data ze serveru bez způsobení vedlejších účinků na serveru, musíme použít metodu GET
. url
je adresa koncového bodu API. Řádek withCredentials: true
označuje, že používáme cookies (což je požadavek pro REM API).
Volání m.request
vrací Promise, který se vyřeší na data z koncového bodu. Ve výchozím nastavení Mithril.js předpokládá, že tělo odpovědi HTTP je ve formátu JSON a automaticky jej parsuje do objektu nebo pole JavaScript. Zpětné volání .then
se spustí po dokončení požadavku XHR. V tomto případě zpětné volání přiřadí pole result.data
do User.list
.
Všimněte si, že v loadList
také používáme příkaz return
. To je obecně dobrá praxe při práci s Promises, což nám umožňuje registrovat více zpětných volání, která se mají spustit po dokončení požadavku XHR.
Tento jednoduchý model odhaluje dva členy: User.list
(pole objektů uživatelů) a User.loadList
(metoda, která naplní User.list
daty ze serveru).
Nyní vytvořme zobrazení, abychom mohli zobrazit data z našeho modelu User.
Vytvořte soubor s názvem src/views/UserList.js
. Nejprve zahrňme Mithril.js a náš model, protože budeme muset použít obojí:
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
Nyní vytvořme komponentu Mithril.js. Komponenta je jednoduše objekt, který má metodu view
:
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
view: function () {
// TODO add code here
},
};
Ve výchozím nastavení jsou zobrazení Mithril.js definována pomocí hyperscript. Hyperscript nabízí stručnou syntaxi, kterou lze odsazovat přirozeněji než HTML pro složité tagy, a protože jeho syntaxe je pouze JavaScript, je možné využít spoustu nástrojů ekosystému JavaScript. Například:
- Můžete použít Babel k transpilaci ES6+ do ES5 pro IE a k transpilaci JSX (rozšíření syntaxe podobné HTML vložené přímo do kódu) na příslušná volání hyperscript.
- Můžete použít ESLint pro snadné lintování bez speciálních pluginů.
- Můžete použít Terser nebo UglifyJS (pouze ES5) pro snadnou minimalizaci kódu.
- Můžete použít Istanbul pro pokrytí kódu.
- Můžete použít TypeScript pro snadnou analýzu kódu. (K dispozici jsou definice typů podporované komunitou, takže si nemusíte vytvářet vlastní.)
Začněme s hyperscript a vytvořme seznam položek. Hyperscript je idiomatický způsob použití Mithril.js, ale JSX funguje docela podobně.
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
view: function () {
return m('.user-list');
},
};
Řetězec ".user-list"
je selektor CSS a jak byste očekávali, .user-list
představuje třídu. Pokud není zadán tag, div
je výchozí. Takže toto zobrazení je ekvivalentní <div class="user-list"></div>
.
Nyní se odkažme na seznam uživatelů z modelu, který jsme vytvořili dříve (User.list
), abychom dynamicky procházeli data:
// 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);
})
);
},
};
Protože User.list
je pole JavaScript a zobrazení hyperscript jsou také JavaScript, můžeme procházet pole pomocí metody .map
. Tím se vytvoří pole vnode, které představuje seznam div
, z nichž každý obsahuje jméno uživatele.
Problém samozřejmě je, že jsme nikdy nezavolali funkci User.loadList
. Proto je User.list
stále prázdné pole, a proto by toto zobrazení vykreslilo prázdnou stránku. Protože chceme, aby se funkce User.loadList
volala při vykreslení této komponenty, můžeme využít komponentní metody životního cyklu:
// 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);
})
);
},
};
Všimněte si, že jsme do komponenty přidali metodu oninit
, která odkazuje na User.loadList
. To znamená, že při inicializaci komponenty bude volána funkce User.loadList, která spustí požadavek XHR. Když server vrátí odpověď, User.list
se naplní.
Také si všimněte, že jsme nedělali oninit: User.loadList()
(s závorkami na konci). Rozdíl je v tom, že oninit: User.loadList()
funkci zavolá jednou a okamžitě, zatímco oninit: User.loadList
ji zavolá pouze při vykreslení komponenty. To je důležitý rozdíl a častá past pro vývojáře, kteří s JavaScriptem začínají: okamžité volání funkce znamená, že požadavek XHR se spustí, jakmile je vyhodnocen zdrojový kód, i když se komponenta nikdy nevykreslí. Také, pokud je komponenta někdy znovu vytvořena, funkce nebude volána znovu, jak se očekávalo.
Vykresleme zobrazení ze vstupního bodu souboru src/index.js
, který jsme vytvořili dříve:
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
m.mount(document.body, UserList);
Volání m.mount
vykreslí zadanou komponentu (UserList
) do elementu DOM (document.body
) a vymaže veškerý DOM, který tam byl dříve. Otevření souboru HTML v prohlížeči by nyní mělo zobrazit seznam jmen osob.
V tuto chvíli vypadá seznam poněkud obyčejně, protože jsme nedefinovali žádné styly. Přidejme tedy několik z nich. Nejprve vytvořme soubor s názvem styles.css
a zahrňme jej do souboru index.html
:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Moje aplikace</title>
<link href="styles.css" rel="stylesheet" />
</head>
<body>
<script src="bin/app.js"></script>
</body>
</html>
Nyní můžeme přidat styly pro komponentu 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;
}
Opětovné načtení okna prohlížeče by nyní mělo zobrazit některé stylované prvky.
Přidejme do naší aplikace routing.
Routing znamená svázání obrazovky s jedinečnou adresou URL, aby bylo možné přecházet z jedné "stránky" na druhou. Mithril.js je navržen pro Single Page Applications, takže tyto "stránky" nemusí být nutně různé soubory HTML v tradičním smyslu slova. Místo toho routing v Single Page Applications zachovává stejný soubor HTML po celou dobu jeho životnosti, ale mění stav aplikace prostřednictvím JavaScriptu. Routing na straně klienta má tu výhodu, že se vyhýbá zábleskům prázdné obrazovky mezi přechody stránek a může snížit množství dat odesílaných ze serveru, pokud se používá ve spojení s architekturou orientovanou na webové služby (tj. aplikace, která stahuje data jako JSON namísto stahování předem vykreslených bloků podrobného HTML).
Routing můžeme přidat změnou volání m.mount
na volání m.route
:
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
m.route(document.body, '/list', {
'/list': UserList,
});
Volání m.route
určuje, že aplikace bude vykreslena do document.body
. Argument "/list"
je výchozí adresa. To znamená, že uživatel bude přesměrován na tuto adresu, pokud zadá neplatnou adresu. Objekt {"/list": UserList}
deklaruje mapu existujících adres a na jaké komponenty se každá adresa překládá.
Obnovení stránky v prohlížeči by nyní mělo připojit #!/list
k adrese URL, což znamená, že routing funguje. Protože tato adresa vykresluje UserList, měli bychom stále vidět seznam lidí na obrazovce jako dříve.
Úryvek #!
je známý jako hashbang a je to běžně používaný řetězec pro implementaci routingu na straně klienta. Je možné tento řetězec konfigurovat prostřednictvím m.route.prefix
. Některé konfigurace vyžadují podporu změn na straně serveru, takže budeme hashbang používat i nadále po zbytek tohoto tutoriálu.
Přidejme do naší aplikace další adresu pro úpravu uživatelů. Nejprve vytvořme modul s názvem views/UserForm.js
// src/views/UserForm.js
module.exports = {
view: function () {
// TODO implement view
},
};
Poté můžeme require
tento nový modul z 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,
});
A nakonec můžeme vytvořit adresu, která na ni odkazuje:
// 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,
});
Všimněte si, že nová adresa má v sobě :id
. Toto je parametr cesty; můžete si to představit jako zástupný znak; adresa /edit/1
by se přeložila na UserForm
s id
"1"
. /edit/2
by se také přeložila na UserForm
, ale s id
"2"
. A tak dále.
Implementujme komponentu UserForm
tak, aby mohla reagovat na tyto parametry cesty:
// 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'),
]);
},
};
A přidejme do styles.css
další styly:
/* 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;
}
Právě teď tato komponenta nereaguje na události uživatele. Přidejme do našeho modelu User
v src/models/User.js
nějaký kód. Takto vypadá kód právě teď:
// 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;
Přidejme kód, který nám umožní načíst jednoho uživatele
// 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;
Všimněte si, že jsme přidali vlastnost User.current
a metodu User.load(id)
, která tuto vlastnost naplní. Nyní můžeme naplnit zobrazení UserForm
pomocí této nové metody:
// 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'),
]);
},
};
Podobně jako komponenta UserList
, oninit
volá User.load()
. Pamatujete si, že jsme měli parametr cesty s názvem :id
na adrese "/edit/:id": UserForm
? Parametr cesty se stane atributem vnode komponenty UserForm
, takže routing na /edit/1
by způsobilo, že vnode.attrs.id
bude mít hodnotu "1"
.
Nyní upravme zobrazení UserList
, abychom mohli odtud přejít na 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
);
})
);
},
};
Zde jsme nahradili vnode .user-list-item
za m.route.Link
s touto třídou a stejnými potomky. Přidali jsme href
, který odkazuje na adresu, kterou chceme. To znamená, že kliknutí na odkaz by změnilo část adresy URL, která následuje za hashbang #!
(čímž se změní adresa bez uvolnění aktuální stránky HTML). V pozadí se pro implementaci odkazu používá značka <a>
a vše funguje automaticky.
Pokud obnovíte stránku v prohlížeči, nyní byste měli mít možnost kliknout na osobu a přejít do formuláře. Měli byste mít také možnost stisknout tlačítko pro návrat v prohlížeči a vrátit se z formuláře do seznamu lidí.
Formulář se po stisknutí tlačítka "Uložit" stále neukládá. Opravme tento formulář:
// 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', 'Jméno'),
m('input.input[type=text][placeholder=Jméno]', {
oninput: function (e) {
User.current.firstName = e.target.value;
},
value: User.current.firstName,
}),
m('label.label', 'Příjmení'),
m('input.input[placeholder=Příjmení]', {
oninput: function (e) {
User.current.lastName = e.target.value;
},
value: User.current.lastName,
}),
m('button.button[type=submit]', 'Uložit'),
]
);
},
};
Přidali jsme události oninput
k oběma vstupním polím. Tyto události nastavují vlastnosti User.current.firstName
a User.current.lastName
při každé změně hodnoty pole uživatelem.
Dále jsme definovali, že metoda User.save
by měla být volána po odeslání formuláře stisknutím tlačítka "Uložit". Implementujme tuto metodu:
// 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;
V metodě save
používáme HTTP metodu PUT
, abychom indikovali, že provádíme aktualizaci dat na serveru (upsert).
Nyní zkuste upravit jméno uživatele v aplikaci. Po uložení změny byste měli vidět aktualizované jméno v seznamu uživatelů.
V současné době se můžeme vrátit do seznamu uživatelů pouze pomocí tlačítka zpět v prohlížeči. Ideálně bychom chtěli mít menu, nebo obecněji rozvržení (layout), kam můžeme umístit globální prvky uživatelského rozhraní.
Vytvořme soubor 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' }, 'Uživatelé')]),
m('section', vnode.children),
]);
},
};
Tato komponenta je jednoduchá a obsahuje <nav>
s odkazem na seznam uživatelů. Stejně jako u odkazů /edit
, i tento odkaz používá m.route.Link
k vytvoření směrovatelného odkazu.
Všimněte si elementu <section>
, jehož obsah je definován jako vnode.children
. vnode
zde odkazuje na instanci komponenty Layout
, takže vnode.children
představují všechny prvky uvnitř <section>
.
A nyní aktualizujme styly:
/* 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;
}
Upravme router v src/index.js
, abychom začlenili naše rozvržení:
// 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));
},
},
});
Nahradili jsme každou komponentu RouteResolverem, což je objekt, který definuje, jak se má daná cesta vykreslit.
Metody render
se píší stejně jako běžné pohledy komponent, pomocí vnořených volání m()
.
Je zajímavé, že ve volání m()
lze místo řetězce selektoru použít přímo komponenty. Zde, ve směrování /list
, máme m(Layout, m(UserList))
. To znamená, že existuje kořenový vnode, který reprezentuje instanci Layout
, a ta má vnode UserList
jako svého jediného potomka.
Ve směrování /edit/:id
existuje také argument vnode
, který předává parametry směrování do komponenty UserForm
. Takže pokud je URL /edit/1
, pak vnode.attrs
bude v tomto případě {id: 1}
a m(UserForm, vnode.attrs)
je ekvivalentní m(UserForm, {id: 1})
. Ekvivalentní JSX kód by byl <UserForm id={vnode.attrs.id} />
.
Obnovte stránku v prohlížeči a nyní uvidíte globální navigaci na všech stránkách aplikace.
Tímto náš tutoriál končí.
V tomto tutoriálu jsme si prošli procesem vytváření velmi jednoduché aplikace, ve které můžeme zobrazit seznam uživatelů ze serveru a upravovat je jednotlivě. Jako další cvičení se pokuste implementovat vytváření a mazání uživatelů sami.
Pokud chcete vidět více příkladů kódu Mithril.js, navštivte stránku příklady. Pokud máte dotazy, neváhejte se zastavit v chatovací místnosti Mithril.js.