Komponenty
Struktura
Komponenty slouží k zapouzdření částí uživatelského rozhraní (UI), což usnadňuje organizaci kódu a jeho opakované použití.
Libovolný JavaScriptový objekt, který obsahuje metodu view
, je považován za komponentu Mithril.js. Komponenty se používají pomocí utility m()
:
// definice komponenty
var Example = {
view: function (vnode) {
return m('div', 'Hello');
},
};
// použití komponenty
m(Example);
// ekvivalentní HTML
// <div>Hello</div>
Metody životního cyklu
Komponenty mohou mít stejné metody životního cyklu jako uzly virtuálního DOM. Je důležité si uvědomit, že vnode
je předáván jako argument do každé metody životního cyklu a také do metody view
. Navíc, předchozí vnode je předán do onbeforeupdate
:
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) {
// zavolejte po dokončení animace
resolve();
});
},
onremove: function (vnode) {
console.log('removing DOM element');
},
view: function (vnode) {
return 'hello';
},
};
Stejně jako u jiných typů uzlů virtuálního DOM, i u komponent lze definovat další metody životního cyklu, pokud jsou použity jako typy vnode.
function initialize(vnode) {
console.log('initialized as vnode');
}
m(ComponentWithHooks, { oninit: initialize });
Metody životního cyklu ve vnodes nepřepisují metody komponent, ani naopak. Metody životního cyklu komponent jsou vždy spuštěny po odpovídající metodě vnode.
Dbejte na to, abyste nepoužívali názvy metod životního cyklu jako názvy vlastních callback funkcí ve vnodech.
Chcete-li se dozvědět více o metodách životního cyklu, podívejte se na stránku o metodách životního cyklu.
Předávání dat komponentám
Data lze předávat instancím komponent předáním objektu attrs
jako druhého parametru ve funkci hyperscript:
m(Example, { name: 'Floyd' });
K těmto datům lze přistupovat v pohledu komponenty nebo v metodách životního cyklu prostřednictvím vnode.attrs
:
var Example = {
view: function (vnode) {
return m('div', 'Hello, ' + vnode.attrs.name);
},
};
Poznámka: Metody životního cyklu lze také definovat v objektu attrs
, takže byste se měli vyhnout používání jejich názvů pro vlastní callbacky, protože by je také vyvolal samotný Mithril.js. Používejte je v attrs
, pouze pokud si je výslovně přejete používat jako metody životního cyklu.
Stav
Stejně jako všechny uzly virtuálního DOM, i komponentní vnodes mohou mít stav. Stav komponenty je užitečný pro podporu objektově orientovaných architektur, pro zapouzdření a pro oddělení zájmů.
Všimněte si, že na rozdíl od mnoha jiných frameworků, změna stavu komponenty nezpůsobuje překreslení nebo aktualizaci DOM. Místo toho se překreslení provádí, když se spustí obslužné rutiny událostí, když se dokončí požadavky HTTP provedené pomocí m.request nebo když prohlížeč přejde na různé trasy. Mechanismus stavu komponenty Mithril.js existuje jednoduše jako usnadnění pro aplikace.
Pokud dojde ke změně stavu, která není způsobena žádnou z výše uvedených podmínek (např. po setTimeout
), můžete použít m.redraw()
k ručnímu spuštění překreslení.
Stav komponenty s uzávěrem (Closure component state)
V předchozích příkladech je každá komponenta definována jako POJO (Plain Old JavaScript Object), který Mithril.js interně používá jako prototyp pro instance dané komponenty. Je možné používat stav komponenty s POJO (jak si probereme níže), ale není to nejčistší ani nejjednodušší přístup. K tomu použijeme komponentu s uzávěrem, což je jednoduše obalová funkce, která vrací instanci komponenty POJO, která má svůj vlastní uzavřený scope.
V případě komponent s uzávěrem lze stav snadno udržovat pomocí proměnných deklarovaných uvnitř obalující funkce:
function ComponentWithState(initialVnode) {
// Proměnná stavu komponenty, jedinečná pro každou instanci
var count = 0;
// Instance komponenty POJO: jakýkoli objekt s
// funkcí view, která vrací 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'
)
);
},
};
}
Jakékoli funkce deklarované uvnitř uzávěru mají rovněž přístup k proměnným stavu.
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'
)
);
},
};
}
Uzavřené komponenty se používají stejným způsobem jako POJO, např. m(ComponentWithState, { passedData: ... })
.
Velkou výhodou komponent s uzávěrem je, že se nemusíme starat o vazbu this
při připojování callbacků obslužné rutiny událostí. Ve skutečnosti se this
nikdy nepoužívá a nikdy se nemusíme zamýšlet nad nejednoznačnostmi kontextu this
.
Stav komponenty POJO
Obecně se doporučuje používat uzávěry pro správu stavu komponenty. Pokud však máte důvod spravovat stav v POJO, můžete ke stavu komponenty přistupovat třemi způsoby: jako k plánu při inicializaci, prostřednictvím vnode.state
a pomocí klíčového slova this
v metodách komponenty.
Při inicializaci
U komponent POJO je objekt komponenty prototypem každé instance komponenty, takže jakákoli vlastnost definovaná na objektu komponenty bude přístupná jako vlastnost vnode.state
. To umožňuje jednoduchou inicializaci stavu.
V následujícím příkladu se data
stane vlastností objektu vnode.state
komponenty ComponentWithInitialState
.
var ComponentWithInitialState = {
data: 'Initial content',
view: function (vnode) {
return m('div', vnode.state.data);
},
};
m(ComponentWithInitialState);
// Ekvivalentní HTML
// <div>Initial content</div>
Prostřednictvím vnode.state
Jak vidíte, ke stavu lze přistupovat také prostřednictvím vlastnosti vnode.state
, která je k dispozici všem metodám životního cyklu i metodě view
komponenty.
var ComponentWithDynamicState = {
oninit: function (vnode) {
vnode.state.data = vnode.attrs.text;
},
view: function (vnode) {
return m('div', vnode.state.data);
},
};
m(ComponentWithDynamicState, { text: 'Hello' });
// Ekvivalentní HTML
// <div>Hello</div>
Prostřednictvím klíčového slova this
Ke stavu lze přistupovat také prostřednictvím klíčového slova this
, které je k dispozici všem metodám životního cyklu i metodě view
komponenty.
var ComponentUsingThis = {
oninit: function (vnode) {
this.data = vnode.attrs.text;
},
view: function (vnode) {
return m('div', this.data);
},
};
m(ComponentUsingThis, { text: 'Hello' });
// Ekvivalentní HTML
// <div>Hello</div>
Uvědomte si, že při použití funkcí ES5 není hodnota this
v vnořených anonymních funkcích instancí komponenty. Existují dva doporučené způsoby, jak toto omezení JavaScriptu obejít: použijte šipkové funkce, nebo pokud nejsou podporovány, použijte vnode.state
.
Třídy
Pokud vám to vyhovuje (jako v objektově orientovaných projektech), lze komponenty psát také pomocí tříd:
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`);
}
}
Aby se strom vykreslil, komponenty jako třídy musí definovat metodu view()
, která je detekována pomocí .prototype.view
.
Lze je používat stejným způsobem jako běžné komponenty.
// PŘÍKLAD: prostřednictvím m.render
m.render(document.body, m(ClassComponent));
// PŘÍKLAD: prostřednictvím m.mount
m.mount(document.body, ClassComponent);
// PŘÍKLAD: prostřednictvím m.route
m.route(document.body, '/', {
'/': ClassComponent,
});
// PŘÍKLAD: kompozice komponent
class AnotherClassComponent {
view() {
return m('main', [m(ClassComponent)]);
}
}
Stav třídní komponenty
U tříd lze stav spravovat pomocí vlastností a metod instance třídy a přistupovat k nim pomocí 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'
)
);
}
}
Všimněte si, že pro callbacky obslužných rutin událostí musíme použít arrow funkce, abychom správně odkazovali na kontext this
.
Kombinování druhů komponent
Komponenty lze libovolně kombinovat. Třídní komponenta může mít jako potomky komponenty s uzávěrem nebo POJO atd.
Speciální atributy
Mithril.js klade zvláštní sémantiku na několik klíčů vlastností, takže byste se jim měli normálně vyhnout v běžných atributech komponent.
- Metody životního cyklu:
oninit
,oncreate
,onbeforeupdate
,onupdate
,onbeforeremove
aonremove
key
, který se používá ke sledování identity v klíčovaných fragmentechtag
, který se používá k odlišení vnodes od běžných objektů atributů a dalších věcí, které nejsou objekty vnode.
Vyhněte se anti-vzorům
Přestože je Mithril.js flexibilní, existují určité vzory kódu, které se nedoporučují:
Vyhněte se "tlustým" komponentám
Obecně řečeno, "tlustá" komponenta je komponenta, která má vlastní metody instance. Jinými slovy, měli byste se vyhnout připojování funkcí k vnode.state
nebo this
. Je velmi neobvyklé, aby logika, která logicky patří do metody instance komponenty, nemohla být znovu použita jinými komponentami. Je poměrně běžné, že tato logika může být v budoucnu potřebná jinou komponentou.
Refaktorování kódu je snazší, pokud je tato logika umístěna v datové vrstvě, než když je pevně svázána se stavem komponenty.
Zvažte tuto "tlustou" komponentu:
// views/Login.js
// NEDOPORUČUJE SE
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'
),
]);
},
};
Normálně, v kontextu větší aplikace, existuje přihlašovací komponenta, jako je ta výše, vedle komponent pro registraci uživatele a obnovu hesla. Představte si, že chceme být schopni předvyplnit pole e-mailu při navigaci z přihlašovací obrazovky na obrazovky registrace nebo obnovy hesla (nebo naopak), aby uživatel nemusel znovu zadávat svůj e-mail, pokud se náhodou dostal na špatnou stránku (nebo možná chcete uživatele přesunout do registračního formuláře, pokud není nalezeno uživatelské jméno).
Okamžitě je zřejmé, že sdílení polí username
a password
z této komponenty do jiné je komplikované. Je to proto, že "tlustá" komponenta zapouzdřuje svůj stav, což ze své definice ztěžuje přístup k tomuto stavu zvenčí.
Dává větší smysl refaktorovat tuto komponentu a přesunout kód stavu z komponenty do datové vrstvy aplikace. To může být tak jednoduché jako vytvoření nového modulu:
// models/Auth.js
// DOPORUČUJE SE
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;
Poté můžeme komponentu vyčistit:
// views/Login.js
// DOPORUČUJE SE
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'
),
]);
},
};
Modul Auth
se tak stává zdrojem dat pro stav související s ověřováním. Komponenta Register
pak může snadno přistupovat k těmto datům a v případě potřeby i opakovaně používat metody jako canSubmit
. Kromě toho, pokud je vyžadován validační kód (například pro pole e-mailu), stačí upravit setEmail
a tato změna provede validaci e-mailu pro jakoukoli komponentu, která upravuje pole e-mailu.
Jako bonus si všimněte, že již nemusíme používat .bind
k udržení odkazu na stav pro obslužné rutiny událostí komponenty.
Nepředávejte vnode.attrs
samotné jiným vnodes
Někdy je žádoucí zachovat flexibilní rozhraní a zjednodušit implementaci tím, že se atributy předají konkrétní podřízené komponentě nebo prvku, v tomto případě Bootstrap's modal. Může být lákavé předat atributy vnode takto:
// NEDOPORUČUJE SE
var Modal = {
// ...
view: function (vnode) {
return m('.modal[tabindex=-1][role=dialog]', vnode.attrs, [
// předávání `vnode.attrs` zde ^
// ...
]);
},
};
Pokud to uděláte tak, jak je uvedeno výše, můžete narazit na problémy při jeho používání:
var MyModal = {
view: function () {
return m(
Modal,
{
// Toto přepne dvakrát, takže se nezobrazí
onupdate: function (vnode) {
if (toggle) $(vnode.dom).modal('toggle');
},
},
[
// ...
]
);
},
};
Místo toho byste měli předávat jednotlivé atributy do vnodes:
// DOPORUČUJE SE
var Modal = {
// ...
view: function (vnode) {
return m('.modal[tabindex=-1][role=dialog]', vnode.attrs.attrs, [
// předávání `attrs:` zde ^
// ...
]);
},
};
// Příklad
var MyModal = {
view: function () {
return m(Modal, {
attrs: {
// Toto přepne jednou
onupdate: function (vnode) {
if (toggle) $(vnode.dom).modal('toggle');
},
},
// ...
});
},
};
Nemanipulujte s children
Pokud má komponenta specifický způsob, jakým aplikuje atributy nebo zpracovává potomky, doporučuje se použít vlastní atributy.
Často je žádoucí definovat více sad potomků, například pokud má komponenta konfigurovatelný název a tělo.
Vyhněte se destrukturování vlastnosti children
pro tento účel.
// NEDOPORUČUJE SE
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')]);
// neohrabaný případ použití
m(Header, [
[m('h1', 'My title'), m('small', 'A small note')],
m('h2', 'Lorem ipsum'),
]);
Výše uvedená komponenta porušuje předpoklad, že potomci budou vyvedeni ve stejném souvislém formátu, v jakém jsou přijímáni. Je obtížné porozumět komponentě bez přečtení její implementace. Místo toho použijte atributy jako pojmenované parametry a rezervujte children
pro jednotný obsah potomků:
// DOPORUČUJE SE
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'),
});
// jasnější případ použití
m(BetterHeader, {
title: [m('h1', 'My title'), m('small', 'A small note')],
tagline: m('h2', 'Lorem ipsum'),
});
Definujte komponenty staticky, volejte je dynamicky
Vyhněte se vytváření definic komponent uvnitř pohledů
Pokud vytvoříte komponentu uvnitř metody view
(buď přímo vložením kódu, nebo voláním funkce, která komponentu vytváří), každé překreslení vytvoří novou kopii komponenty.
Při porovnávání vnode komponent se, pokud se komponenta, na kterou odkazuje nový vnode, striktně nerovná komponentě, na kterou odkazuje starý vnode, předpokládá, že se jedná o dvě různé komponenty, i když ve skutečnosti vykonávají stejný kód. To znamená, že komponenty vytvořené dynamicky prostřednictvím továrny budou vždy znovu vytvořeny od začátku.
Proto je vhodné se vyhnout opakovanému vytváření komponent. Místo toho používejte komponenty standardním způsobem.
// NEDOPORUČUJE SE
var ComponentFactory = function (greeting) {
// vytvoří novou komponentu při každém volání
return {
view: function () {
return m('div', greeting);
},
};
};
m.render(document.body, m(ComponentFactory('hello')));
// druhé volání znovu vytvoří div od začátku, místo aby nic nedělalo
m.render(document.body, m(ComponentFactory('hello')));
// DOPORUČUJE SE
var Component = {
view: function (vnode) {
return m('div', vnode.attrs.greeting);
},
};
m.render(document.body, m(Component, { greeting: 'hello' }));
// druhé volání neupraví DOM
m.render(document.body, m(Component, { greeting: 'hello' }));
Vyhněte se vytváření instancí komponent mimo pohledy
Naopak, z podobných důvodů, pokud je instance komponenty vytvořena mimo pohled, budoucí překreslení provedou kontrolu rovnosti na uzlu a přeskočí jej. Proto by instance komponent měly být vždy vytvářeny uvnitř pohledů:
// NEDOPORUČUJE SE
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];
},
});
Ve výše uvedeném příkladu kliknutí na tlačítko komponenty čítače zvýší počet stavu, ale jeho pohled nebude spuštěn, protože vnode reprezentující komponentu sdílí stejný odkaz, a proto proces vykreslování neporovnává. Komponenty byste měli vždy volat v pohledu, abyste zajistili vytvoření nového vnode:
// DOPORUČUJE SE
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)];
},
});