Komponensek
Szerkezet
A komponensek a nézet részeinek elkapszulázására szolgáló mechanizmusok, amelyek segítségével a kód könnyebben szervezhető és/vagy újra felhasználható.
Minden olyan JavaScript objektum, amely rendelkezik view
metódussal, Mithril.js komponensnek minősül. A komponensek a m()
segédeszközzel használhatók:
// definiáljuk a komponenst
var Example = {
view: function (vnode) {
return m('div', 'Hello');
},
};
// használjuk a komponenst
m(Example);
// ezzel egyenértékű HTML
// <div>Hello</div>
Életciklus metódusok
A komponensek ugyanazokkal az életciklus metódusokkal rendelkezhetnek, mint a virtuális DOM csomópontok. Fontos, hogy a vnode
argumentumként kerül átadásra minden életciklus metódusnak, valamint a view
-nak is (az előző vnode az onbeforeupdate
metódusnak is átadásra kerül):
var ComponentWithHooks = {
oninit: function (vnode) {
console.log('inicializálva');
},
oncreate: function (vnode) {
console.log('DOM létrehozva');
},
onbeforeupdate: function (newVnode, oldVnode) {
return true;
},
onupdate: function (vnode) {
console.log('DOM frissítve');
},
onbeforeremove: function (vnode) {
console.log('kilépési animáció elindítható');
return new Promise(function (resolve) {
// hívjuk meg az animáció befejezése után
resolve();
});
},
onremove: function (vnode) {
console.log('DOM elem eltávolítása');
},
view: function (vnode) {
return 'hello';
},
};
A virtuális DOM más csomóponttípusaihoz hasonlóan a komponensek további életciklus metódusokkal is rendelkezhetnek, amelyek vnode típusként való használatkor vannak definiálva.
function initialize(vnode) {
console.log('vnode-ként inicializálva');
}
m(ComponentWithHooks, { oninit: initialize });
A vnode-ok életciklus metódusai nem írják felül a komponens metódusait, és fordítva sem. A komponens életciklus metódusai mindig a vnode megfelelő metódusa után futnak le.
Ügyeljünk arra, hogy ne használjuk az életciklus metódusok neveit saját callback függvényeink neveiként a vnode-okban.
Az életciklus metódusokról bővebben az életciklus metódusok oldalon olvashatunk.
Adatok átadása komponenseknek
Adatokat úgy adhatunk át a komponens példányoknak, hogy egy attrs
objektumot adunk át a hyperscript függvény második paramétereként:
m(Example, { name: 'Floyd' });
Ezek az adatok elérhetők a komponens nézetében vagy életciklus metódusaiban a vnode.attrs
segítségével:
var Example = {
view: function (vnode) {
return m('div', 'Hello, ' + vnode.attrs.name);
},
};
MEGJEGYZÉS: Az életciklus metódusok definiálhatók az attrs
objektumban is, ezért kerüljük a nevük használatát saját callback-jeinkhez, mivel azokat a Mithril.js is meghívná. Csak akkor használjuk őket az attrs
-ban, ha kifejezetten életciklus metódusként szeretnénk használni őket.
Állapot
Az összes virtuális DOM csomóponthoz hasonlóan a komponens vnode-oknak is lehet állapota. A komponens állapota hasznos az objektumorientált architektúrák támogatásához, az elkapszulázáshoz és a felelősségek szétválasztásához.
Vegye figyelembe, hogy sok más keretrendszertől eltérően a komponens állapotának megváltoztatása nem vált ki újrarajzolásokat vagy DOM frissítéseket. Ehelyett az újrarajzolások az eseménykezelők aktiválódásakor, a m.request által végzett HTTP kérések befejeződésekor, vagy amikor a böngésző különböző útvonalakra navigál történnek. A Mithril.js komponens állapot mechanizmusai egyszerűen kényelmi funkcióként léteznek az alkalmazások számára.
Ha olyan állapotváltozás következik be, amely nem a fenti feltételek eredménye (pl. setTimeout
után), akkor a m.redraw()
segítségével manuálisan is elindíthatunk egy újrarajzolást.
Closure komponens állapot
A fenti példákban minden komponens POJO-ként (Plain Old JavaScript Object - egyszerű régi JavaScript objektum) van definiálva, amelyet a Mithril.js belsőleg használ az adott komponens példányainak prototípusaként. Lehetséges komponens állapotot használni egy POJO-val (ahogy azt alább tárgyaljuk), de ez nem a legtisztább vagy legegyszerűbb megközelítés. Ehhez egy closure komponenst fogunk használni, ami nem más, mint egy burkoló függvény, ami visszaad egy POJO komponens példányt, aminek saját, zárt hatóköre van.
Egy closure komponenssel az állapot egyszerűen a külső függvényen belül deklarált változók segítségével tartható fenn:
function ComponentWithState(initialVnode) {
// Komponens állapotváltozó, minden példányra egyedi
var count = 0;
// POJO komponens példány: bármely objektum, amely rendelkezik
// view függvénnyel, amely egy vnode-ot ad vissza
return {
oninit: function (vnode) {
console.log('closure komponens inicializálása');
},
view: function (vnode) {
return m(
'div',
m('p', 'Count: ' + count),
m(
'button',
{
onclick: function () {
count += 1;
},
},
'Increment count'
)
);
},
};
}
A closure-en belül deklarált bármely függvény hozzáfér az állapotváltozóihoz.
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'
)
);
},
};
}
A closure komponensek ugyanúgy használhatók, mint a POJO-k, pl. m(ComponentWithState, { passedData: ... })
.
A closure komponensek nagy előnye, hogy nem kell aggódnunk a this
kötése miatt az eseménykezelő callback-ekhez való csatoláskor. Valójában a this
soha nem kerül használatra, és soha nem kell gondolkodnunk a this
kontextus kétértelműségein.
POJO komponens állapot
Általánosságban ajánlott closure-öket használni a komponens állapotának kezelésére. Ha azonban valamilyen okból POJO-ban szeretnénk kezelni az állapotot, a komponens állapota háromféleképpen érhető el: inicializáláskor tervrajzként, a vnode.state
segítségével, illetve a this
kulcsszóval a komponens metódusaiban.
Inicializáláskor
A POJO komponensek esetében a komponens objektum az egyes komponens példányok prototípusa, így a komponens objektumon definiált bármely tulajdonság elérhető a vnode.state
tulajdonságaként. Ez lehetővé teszi az egyszerű "tervrajz" állapot inicializálást.
Az alábbi példában a data
a ComponentWithInitialState
komponens vnode.state
objektumának tulajdonságává válik.
var ComponentWithInitialState = {
data: 'Initial content',
view: function (vnode) {
return m('div', vnode.state.data);
},
};
m(ComponentWithInitialState);
// Ezzel egyenértékű HTML
// <div>Initial content</div>
vnode.state-en keresztül
Mint látható, az állapot a vnode.state
tulajdonságon keresztül is elérhető, amely minden életciklus metódus számára elérhető, valamint a komponens view
metódusa számára is.
var ComponentWithDynamicState = {
oninit: function (vnode) {
vnode.state.data = vnode.attrs.text;
},
view: function (vnode) {
return m('div', vnode.state.data);
},
};
m(ComponentWithDynamicState, { text: 'Hello' });
// Ezzel egyenértékű HTML
// <div>Hello</div>
A this kulcsszóval
Az állapot a this
kulcsszóval is elérhető, amely minden életciklus metódus számára elérhető, valamint a komponens view
metódusa számára is.
var ComponentUsingThis = {
oninit: function (vnode) {
this.data = vnode.attrs.text;
},
view: function (vnode) {
return m('div', this.data);
},
};
m(ComponentUsingThis, { text: 'Hello' });
// Ezzel egyenértékű HTML
// <div>Hello</div>
Ne feledjük, hogy az ES5 függvények használatakor a this
értéke a beágyazott anonim függvényekben nem a komponens példánya. Ennek a JavaScript korlátozásnak a megkerülésére két ajánlott módszer létezik: nyíl függvények használata, vagy ha azok nem támogatottak, a vnode.state
használata.
Osztályok
Ha megfelel az igényeinknek (például objektumorientált projektekben), a komponensek osztályok használatával is megírhatók:
class ClassComponent {
constructor(vnode) {
this.kind = 'class component'; // osztálykomponens
}
view() {
return m('div', `Hello from a ${this.kind}`); // Helló egy ${this.kind} típusú komponensből
}
oncreate() {
console.log(`A ${this.kind} was created`); // Létrehoztunk egy ${this.kind} típusú komponenst
}
}
Az osztálykomponenseknek definiálniuk kell egy view()
metódust, amelyet a .prototype.view
segítségével lehet észlelni, hogy a fa renderelhető legyen.
Ugyanúgy használhatók, mint a normál komponensek.
// PÉLDA: m.render-en keresztül
m.render(document.body, m(ClassComponent));
// PÉLDA: m.mount-on keresztül
m.mount(document.body, ClassComponent);
// PÉLDA: m.route-on keresztül
m.route(document.body, '/', {
'/': ClassComponent,
});
// PÉLDA: komponens kompozíció
class AnotherClassComponent {
view() {
return m('main', [m(ClassComponent)]);
}
}
Osztály komponens állapot
Osztályokkal az állapot az osztálypéldány tulajdonságaival és metódusaival kezelhető, és a this
segítségével érhető el:
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'
)
);
}
}
Vegye figyelembe, hogy nyíl függvényeket kell használnunk az eseménykezelő callback-ekhez, hogy a this
kontextus helyesen hivatkozhasson.
Komponens típusok keverése
A komponensek szabadon keverhetők. Egy osztálykomponensnek lehetnek closure vagy POJO komponensei gyermekként, stb.
Speciális attribútumok
A Mithril.js speciális szemantikát rendel számos tulajdonság kulcshoz, ezért általában kerülni kell azok használatát a normál komponens attribútumokban.
- Életciklus metódusok:
oninit
,oncreate
,onbeforeupdate
,onupdate
,onbeforeremove
ésonremove
key
(kulcs), amelyet a kulcsolt fragmentumokban lévő identitás nyomon követésére használnaktag
(címke), amelyet arra használnak, hogy megkülönböztessék a vnode-okat a normál attribútum objektumoktól és más olyan dolgoktól, amelyek nem vnode objektumok.
Kerüljük az anti-mintákat
Bár a Mithril.js rugalmas, bizonyos kódminták használata nem ajánlott:
Kerüljük a kövér komponenseket
Általánosságban elmondható, hogy a "kövér" komponens egy olyan komponens, amely egyéni példánymetódusokkal rendelkezik. Más szavakkal, kerülni kell a függvények csatolását a vnode.state
-hez vagy a this
-hez. Rendkívül ritka, hogy olyan logika legyen, amely logikailag belefér egy komponens példánymetódusába, és amelyet más komponensek nem tudnak újrahasználni. Viszonylag gyakori, hogy az említett logikára egy másik komponensnek is szüksége lehet a jövőben.
Könnyebb a kódot refaktorálni, ha ez a logika az adatszintben van elhelyezve, mintha egy komponens állapotához lenne kötve.
Tekintsük ezt a kövér komponenst:
// views/Login.js
// KERÜLENDŐ
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'
),
]);
},
};
Általában egy nagyobb alkalmazás kontextusában egy olyan bejelentkező komponens, mint a fenti, a felhasználói regisztrációhoz és a jelszó helyreállításhoz szükséges komponensek mellett létezik. Képzeljük el, hogy előre szeretnénk kitölteni az e-mail mezőt, amikor a bejelentkező képernyőről a regisztrációs vagy jelszó-helyreállítási képernyőre navigálunk (vagy fordítva), hogy a felhasználónak ne kelljen újra beírnia az e-mail címét, ha véletlenül rossz oldalt töltött ki (vagy talán a regisztrációs űrlapra szeretné irányítani a felhasználót, ha nem található felhasználónév).
Azonnal látjuk, hogy a username
és password
mezők megosztása ebből a komponensből egy másikba nehéz. Ez azért van, mert a kövér komponens elkapszulázza az állapotát, ami definíció szerint megnehezíti az állapot külső elérését.
Értelmesebb refaktorálni ezt a komponenst, és kihúzni az állapotkódot a komponensből az alkalmazás adatszintjébe. Ez lehet olyan egyszerű, mint egy új modul létrehozása:
// models/Auth.js
// ELŐNYBEN RÉSZESÍTENDŐ
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;
Ezután megtisztíthatjuk a komponenst:
// views/Login.js
// ELŐNYBEN RÉSZESÍTENDŐ
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'
),
]);
},
};
Ily módon az Auth
modul most az igazság forrása a hitelesítéssel kapcsolatos állapotokhoz, és egy Register
komponens könnyen hozzáférhet ezekhez az adatokhoz, és akár újra is felhasználhatja az olyan metódusokat, mint a canSubmit
, ha szükséges. Ezenkívül, ha érvényesítési kódra van szükség (például az e-mail mezőhöz), akkor csak az setEmail
-t kell módosítania, és ez a változás e-mail érvényesítést végez minden olyan komponens számára, amely módosítja az e-mail mezőt.
Bónuszként vegye figyelembe, hogy többé nem kell .bind
-ot használnunk, hogy hivatkozást tartsunk a komponens eseménykezelőinek állapotára.
Ne továbbítsuk a vnode.attrs
-t magát más vnode-oknak
Néha érdemes rugalmasan tartani az interfészt és egyszerűsíteni a megvalósítást azáltal, hogy attribútumokat továbbítunk egy adott gyermekkomponensnek vagy elemnek, például a Bootstrap modal-nak. Kísértést jelenthet egy vnode attribútumainak továbbítása az alábbiak szerint:
// KERÜLENDŐ
var Modal = {
// ...
view: function (vnode) {
return m('.modal[tabindex=-1][role=dialog]', vnode.attrs, [
// a `vnode.attrs` továbbítása itt ^
// ...
]);
},
};
Ha a fentiek szerint csináljuk, problémákba ütközhetünk a használatakor:
var MyModal = {
view: function () {
return m(
Modal,
{
// Ez kétszer váltja át, így nem jelenik meg
onupdate: function (vnode) {
if (toggle) $(vnode.dom).modal('toggle');
},
},
[
// ...
]
);
},
};
Ehelyett egyes attribútumokat kell továbbítanunk a vnode-okba:
// ELŐNYBEN RÉSZESÍTENDŐ
var Modal = {
// ...
view: function (vnode) {
return m('.modal[tabindex=-1][role=dialog]', vnode.attrs.attrs, [
// az `attrs:` továbbítása itt ^
// ...
]);
},
};
// Példa
var MyModal = {
view: function () {
return m(Modal, {
attrs: {
// Ez egyszer váltja át
onupdate: function (vnode) {
if (toggle) $(vnode.dom).modal('toggle');
},
},
// ...
});
},
};
Ne manipuláljuk a children
-t
Ha egy komponens véleményes abban, hogy hogyan alkalmazza az attribútumokat vagy a gyermekeket, akkor át kell váltania az egyéni attribútumok használatára.
Gyakran kívánatos több gyermekhalmazt definiálni, például ha egy komponens konfigurálható címmel és törzzsel rendelkezik.
Kerüljük a children
tulajdonság destrukturálását erre a célra.
// KERÜLENDŐ
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')]);
// kínos használati eset
m(Header, [
[m('h1', 'My title'), m('small', 'A small note')],
m('h2', 'Lorem ipsum'),
]);
A fenti komponens megsérti azt a feltételezést, hogy a gyermekek ugyanabban a folytonos formátumban kerülnek kiadásra, mint ahogyan érkeznek. Nehéz megérteni a komponenst anélkül, hogy elolvasná a megvalósítását. Ehelyett használjunk attribútumokat névvel ellátott paraméterekként, és tartsuk fenn a children
-t az egységes gyermektartalom számára:
// ELŐNYBEN RÉSZESÍTENDŐ
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'),
});
// tisztább használati eset
m(BetterHeader, {
title: [m('h1', 'My title'), m('small', 'A small note')],
tagline: m('h2', 'Lorem ipsum'),
});
Statikusan definiáljuk a komponenseket, dinamikusan hívjuk meg őket
Kerüljük a komponens definíciók létrehozását a nézeteken belül
Ha egy komponenst egy view
metóduson belül hozunk létre (akár közvetlenül beágyazva, akár egy olyan függvény meghívásával, amely ezt teszi), minden újrarajzolásnak a komponens egy másik klónja lesz. A komponens vnode-ok diffelésekor, ha az új vnode által hivatkozott komponens nem szigorúan egyenlő a régi komponens által hivatkozott komponenssel, akkor a kettőt különböző komponenseknek tekintik, még akkor is, ha végső soron egyenértékű kódot futtatnak. Ez azt jelenti, hogy a dinamikusan, gyár által létrehozott komponensek mindig a semmiből lesznek újra létrehozva.
Ezért kerüljük a komponensek újbóli létrehozását. Ehelyett használjuk a komponenseket a bevett módon.
// KERÜLENDŐ
var ComponentFactory = function (greeting) {
// minden híváskor új komponenst hoz létre
return {
view: function () {
return m('div', greeting);
},
};
};
m.render(document.body, m(ComponentFactory('hello')));
// a második hívás a div-et a semmiből hozza létre újra, ahelyett, hogy nem csinálna semmit
m.render(document.body, m(ComponentFactory('hello')));
// ELŐNYBEN RÉSZESÍTENDŐ
var Component = {
view: function (vnode) {
return m('div', vnode.attrs.greeting);
},
};
m.render(document.body, m(Component, { greeting: 'hello' }));
// a második hívás nem módosítja a DOM-ot
m.render(document.body, m(Component, { greeting: 'hello' }));
Kerüljük a komponens példányok létrehozását a nézeteken kívül
Ezzel szemben, hasonló okokból, ha egy komponens példányt a nézeten kívül hoznak létre, a jövőbeli újrarajzolások egyenlőségi ellenőrzést végeznek a csomóponton, és kihagyják azt. Ezért a komponens példányokat mindig a nézeteken belül kell létrehozni:
// KERÜLENDŐ
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];
},
});
A fenti példában a számláló komponens gombjára kattintva növekszik az állapot száma, de a nézete nem fog frissülni, mert a komponenst képviselő vnode ugyanarra a memóriacímre mutat, ezért a renderelési folyamat nem érzékeli a változást. A komponenseket mindig a nézetben kell meghívnia, hogy biztosítsa egy új vnode létrehozását:
// ELŐNYBEN RÉSZESÍTENDŐ
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)];
},
});