Kulcsok
Mik azok a kulcsok?
A kulcsok nyomon követett identitásokat jelölnek. Hozzáadhatók elem-, komponens- és fragment vnode-okhoz a key
mágikus attribútumon keresztül, és használatukkor valahogy így néznek ki:
m('.user', { key: user.id }, [
/* ... */
]);
Néhány esetben hasznosak:
- Modelladatok vagy más állapotfüggő adatok renderelésekor kulcsokra van szükség ahhoz, hogy a helyi állapot a megfelelő al-fához legyen hozzárendelve.
- Ha több szomszédos csomópontot egymástól függetlenül animál CSS használatával, és bármelyiket külön-külön eltávolíthatja, kulcsokra van szüksége, hogy biztosítsa, hogy az animációk az elemekhez kapcsolódjanak, és ne ugorjanak váratlanul más csomópontokra.
- Ha parancsra újra kell inicializálnia egy al-fát, adjon hozzá egy kulcsot, majd változtassa meg és rajzolja újra, amikor szükséges.
Kulcsra vonatkozó korlátozások
Fontos: Minden fragment esetében a gyermekeinek vagy kizárólag kulcs attribútummal rendelkező vnode-okat (kulcsolt fragment) vagy kizárólag kulcs attribútum nélküli vnode-okat (kulcsolatlan fragment) kell tartalmazniuk. A kulcs attribútumok csak olyan vnode-okon létezhetnek, amelyek támogatják az attribútumokat, nevezetesen elem-, komponens- és fragment vnode-okon. Más vnode-ok, mint például a null
, undefined
és a stringek, nem rendelkezhetnek attribútumokkal, így kulcs attribútumuk sem lehet, ezért nem használhatók kulcsolt fragmentekben.
Ez azt jelenti, hogy például a [m(".foo", {key: 1}), null]
és a ["foo", m(".bar", {key: 2})]
nem működnek, de a [m(".foo", {key: 1}), m(".bar", {key: 2})]
és a [m(".foo"), null]
igen. Ha ezt elfelejti, egy hasznos hibaüzenetet fog kapni, amely ezt elmagyarázza.
Modelladatok összekötése a nézetek listáiban
Listák renderelésekor, különösen szerkeszthető listák esetén, gyakran találkozunk olyan dolgokkal, mint a szerkeszthető TODO-k és hasonlók. Ezeknek van állapotuk és identitásuk, és meg kell adnia a Mithril.js számára azokat az információkat, amelyekre a nyomon követéshez szüksége van.
Tegyük fel, hogy van egy egyszerű közösségi média bejegyzés lista, ahol hozzászólhat a bejegyzésekhez, és ahol elrejtheti a bejegyzéseket például bejelentés miatt.
// `User` és `ComposeWindow` kihagyva a rövidség kedvéért
function CommentCompose() {
return {
view: function (vnode) {
var post = vnode.attrs.post;
return m(ComposeWindow, {
placeholder: 'Írd meg a hozzászólásod...',
submit: function (text) {
return Model.addComment(post, text);
},
});
},
};
}
function Comment() {
return {
view: function (vnode) {
var comment = vnode.attrs.comment;
return m(
'.comment',
m(User, { user: comment.user }),
m('.comment-body', comment.text),
m(
'a.comment-hide',
{
onclick: function () {
Model.hideComment(comment).then(m.redraw);
},
},
"Nem tetszik"
)
);
},
};
}
function PostCompose() {
return {
view: function (vnode) {
var comment = vnode.attrs.comment;
return m(ComposeWindow, {
placeholder: 'Írd meg a bejegyzésed...',
submit: Model.createPost,
});
},
};
}
function Post(vnode) {
var showComments = false;
var commentsFetched = false;
return {
view: function (vnode) {
var post = vnode.attrs.post;
var comments = showComments ? Model.getComments(post) : null;
return m(
'.post',
m(User, { user: post.user }),
m('.post-body', post.text),
m(
'.post-meta',
m(
'a.post-comment-count',
{
onclick: function () {
if (!showComments && !commentsFetched) {
commentsFetched = true;
Model.fetchComments(post).then(m.redraw);
}
showComments = !showComments;
},
},
post.commentCount,
' hozzászólás' + (post.commentCount === 1 ? '' : 'ok'),
),
m(
'a.post-hide',
{
onclick: function () {
Model.hidePost(post).then(m.redraw);
},
},
"Nem tetszik"
)
),
showComments
? m(
'.post-comments',
comments == null
? m('.comment-list-loading', 'Töltés...')
: [
m(
'.comment-list',
comments.map(function (comment) {
return m(Comment, { comment: comment });
})
),
m(CommentCompose, { post: post }),
]
)
: null
);
},
};
}
function Feed() {
Model.fetchPosts().then(m.redraw);
return {
view: function () {
var posts = Model.getPosts();
return m(
'.feed',
m('h1', 'Hírfolyam'),
posts == null
? m('.post-list-loading', 'Töltés...')
: m(
'.post-view',
m(PostCompose),
m(
'.post-list',
posts.map(function (post) {
return m(Post, { post: post });
})
)
)
);
},
};
}
Mint látható, sok funkcionalitást foglal magában, de két dolgot szeretnék kiemelni:
// A `Feed` komponensben
m(
'.post-list',
posts.map(function (post) {
return m(Post, { post: post });
})
);
// A `Post` komponensben
m(
'.comment-list',
comments.map(function (comment) {
return m(Comment, { comment: comment });
})
);
Ezek mindegyike egy olyan al-fára utal, amelynek a Mithril.js nem ismeri a hozzá tartozó állapotát. (A Mithril.js csak a vnode-okról tud, semmi másról.) Ha ezeket kulcsolatlanul hagyja, a dolgok furcsává és váratlanná válhatnak. Ebben az esetben próbáljon meg a "N hozzászólás" gombra kattintani a hozzászólások megjelenítéséhez, írjon be valamit a hozzászólás író mezőjébe az alján, majd kattintson a "Nem tetszik" gombra egy felette lévő bejegyzésen. Itt van egy élő példa, az összes javítással ellátva, teszt modellel kiegészítve. (Megjegyzés: ha Edge-et vagy IE-t használ, problémákba ütközhet a link hash hossza miatt.)
Ahelyett, hogy azt tenné, amit várna, ehelyett összezavarodik, és helytelenül működik: bezárja a megnyitott hozzászóláslistát, és az a bejegyzés, amelyik után a hozzászólások meg voltak nyitva, most tartósan a "Töltés..." feliratot mutatja, annak ellenére, hogy azt hiszi, hogy már betöltötte a hozzászólásokat. Ez azért van, mert a hozzászólások késleltetve töltődnek be, és a Mithril.js feltételezi, hogy minden alkalommal ugyanaz a hozzászólás kerül átadásra (ami itt viszonylag ésszerűnek hangzik), de ebben az esetben nem. A Mithril.js a kulcsolatlan fragmenteket egyenként, iteratívan, nagyon egyszerű módon javítja. Tehát ebben az esetben a különbség valahogy így nézhet ki:
- Előtte:
A, B, C, D, E
- Javítva:
A, B, C -> D, D -> E, E -> (eltávolítva)
És mivel a komponens ugyanaz marad (mindig Comment
), csak az attribútumok változnak, és nem cserélik ki.
A hiba kijavításához egyszerűen csak adjon hozzá egy kulcsot, hogy a Mithril.js tudja, hogy szükség esetén átrendezze az állapotot a probléma megoldásához. Itt van egy élő, működő példa mindenre kijavítva.
// A `Feed` komponensben
m(
'.post-list',
posts.map(function (post) {
return m(Post, { key: post.id, post: post });
})
);
// A `Post` komponensben
m(
'.comment-list',
comments.map(function (comment) {
return m(Comment, { key: comment.id, comment: comment });
})
);
Megjegyzés: Bár a megjegyzések esetében technikailag működne kulcsok nélkül is ebben az esetben, hasonlóképpen tönkremenne, ha bármilyen beágyazott megjegyzést vagy szerkesztési lehetőséget adna hozzá, és kulcsokat kellene hozzájuk adnia.
Animált elemek gyűjteményének hibamentes kezelése
Bizonyos esetekben listákat, dobozokat és hasonlókat szeretne animálni. Kezdjük ezzel az egyszerű kóddal:
var colors = ['red', 'yellow', 'blue', 'gray'];
var counter = 0;
function getColor() {
var color = colors[counter];
counter = (counter + 1) % colors.length;
return color;
}
function Boxes() {
var boxes = [];
function add() {
boxes.push({ color: getColor() });
}
function remove(box) {
var index = boxes.indexOf(box);
boxes.splice(index, 1);
}
return {
view: function () {
return [
m('button', { onclick: add }, 'Add box, click box to remove'),
m(
'.container',
boxes.map(function (box, i) {
return m(
'.box',
{
'data-color': box.color,
onclick: function () {
remove(box);
},
},
m('.stretch')
);
})
),
];
},
};
}
Elég ártatlanul néz ki, de próbáljon ki egy élő példát. Ebben a példában kattintson a gombra néhány doboz létrehozásához, válasszon egy dobozt, és figyelje meg a méretét. Azt szeretnénk, hogy a méret és a forgás a dobozhoz (színnel jelölve) és ne a rácsban lévő pozícióhoz legyen kötve. Ehelyett azt fogja észrevenni, hogy a méret hirtelen megnő, de a pozíciója állandó marad. Ez azt jelenti, hogy kulcsokat (key) kell hozzárendelnünk.
Ebben az esetben egyedi kulcsok megadása meglehetősen egyszerű: csak hozzon létre egy számlálót, amelyet minden olvasáskor növel.
var colors = ['red', 'yellow', 'blue', 'gray'];
var counter = 0;
function getColor() {
var color = colors[counter];
counter = (counter + 1) % colors.length;
return color;
}
function Boxes() {
var boxes = [];
var nextKey = 0;
function add() {
boxes.push({ color: getColor() });
var key = nextKey;
nextKey++;
boxes.push({ key: key, color: getColor() });
}
function remove(box) {
var index = boxes.indexOf(box);
boxes.splice(index, 1);
}
return {
view: function () {
return [
m('button', { onclick: add }, 'Add box, click box to remove'),
m(
'.container',
boxes.map(function (box, i) {
return m(
'.box',
{
key: box.key,
'data-color': box.color,
onclick: function () {
remove(box);
},
},
m('.stretch')
);
})
),
];
},
};
}
Itt van egy javított demó, amivel kipróbálhatja, hogy lássa, hogyan működik másképp.
Nézetek újra inicializálása egyetlen, kulccsal ellátott gyermekelemet tartalmazó fragmentumokkal
Amikor állapotokkal rendelkező entitásokkal foglalkozik a modellekben és hasonló helyzetekben, gyakran hasznos a modellnézetek kulcsokkal történő renderelése. Tegyük fel, hogy van egy ilyen elrendezésünk:
function Layout() {
// ...
}
function Person() {
// ...
}
m.route(rootElem, '/', {
'/': Home,
'/person/:id': {
render: function () {
return m(Layout, m(Person, { id: m.route.param('id') }));
},
},
// ...
});
A Person
komponens valószínűleg így néz ki:
function Person(vnode) {
var personId = vnode.attrs.id;
var state = 'pending';
var person, error;
m.request('/api/person/:id', { params: { id: personId } }).then(
function (p) {
person = p;
state = 'ready';
},
function (e) {
error = e;
state = 'error';
}
);
return {
view: function () {
if (state === 'pending') return m(LoadingIcon);
if (state === 'error') {
return error.code === 404
? m('.person-missing', 'Person not found.')
: m('.person-error', 'An error occurred. Please try again later');
}
return m(
'.person',
m(
m.route.Link,
{
class: 'person-edit',
href: '/person/:id/edit',
params: { id: personId },
},
'Edit'
),
m('.person-name', 'Name: ', person.name)
// ...
);
},
};
}
Tegyük fel, hogy hozzáadott egy módot arra, hogy ebből a komponensből más emberekre hivatkozzon, például egy "manager" mező hozzáadásával.
function Person(vnode) {
// ...
return {
view: function () {
// ...
return m(
'.person',
m(
m.route.Link,
{
class: 'person-edit',
href: '/person/:id/edit',
params: { id: personId },
},
'Edit'
),
m('.person-name', person.name),
// ...
m(
'.manager',
'Manager: ',
m(
m.route.Link,
{
href: '/person/:id',
params: { id: person.manager.id },
},
person.manager.name
)
)
// ...
);
},
};
}
Ha például a személy azonosítója 1, a menedzser azonosítója pedig 2, a /person/1
-ről a /person/2
-re váltana, ugyanazon az útvonalon maradva. Mivel azonban az útvonal-feloldó render
metódusa került felhasználásra (lásd: ../api/route.md#routeresolverrender), a fa megmaradt, és a m(Layout, m(Person, {id: "1"}))
elem csupán a m(Layout, m(Person, {id: "2"}))
elemre változott. Ebben az esetben a Person
nem változott, így nem inicializálja újra a komponenst. De a mi esetünkben ez rossz, mert ez azt jelenti, hogy az új személy adatai nem kerülnek lekérésre. Itt válnak hasznossá a kulcsok. Az útvonal-feloldó metódust így módosíthatjuk a javításhoz:
m.route(rootElem, '/', {
'/': Home,
'/person/:id': {
render: function () {
return m(
Layout,
// Csomagoljuk egy tömbbe, arra az esetre, ha később más elemeket adunk hozzá.
// Ne feledje: a fragmentumoknak vagy csak kulccsal ellátott gyermekeket
// vagy egyáltalán nem kulccsal ellátott gyermekeket kell tartalmazniuk.
[m(Person, { id: m.route.param('id'), key: m.route.param('id') })]
);
},
},
// ...
});
Gyakori buktatók
Számos gyakori buktató létezik a kulcsok használatával kapcsolatban. Az alábbiakban bemutatunk néhányat, hogy segítsünk megérteni, miért nem működnek bizonyos megoldások.
Kulcsolt elemek becsomagolása
Ez a két kódrészlet nem ugyanúgy működik:
users.map(function (user) {
return m('.wrapper', [m(User, { user: user, key: user.id })]);
});
users.map(function (user) {
return m('.wrapper', { key: user.id }, [m(User, { user: user })]);
});
Az első esetben a kulcs a User
komponenshez van kötve, de a users.map(...)
által létrehozott külső töredék kulcs nélküli. A kulcsolt elem ilyen becsomagolása nem fog megfelelően működni. Az eredmény a lista minden egyes módosításakor történő felesleges újrarajzolástól kezdve a belső űrlapmezők állapotának elvesztéséig bármi lehet. A viselkedés hasonló lehet a posztlista hibás példájához, de az állapotromlás problémája nélkül.
A második esetben a kulcs a .wrapper
elemhez van kötve, biztosítva, hogy a külső töredék kulcsolt legyen. Ez valószínűleg a kívánt működés, és egy felhasználó eltávolítása nem okoz problémát a többi felhasználói példány állapotával kapcsolatban.
Kulcsok elhelyezése a komponensen belül
Tegyük fel, hogy a személy példában ezt csináltad helyette:
// NE HASZNÁLD
function Person(vnode) {
var personId = vnode.attrs.id;
// ...
return {
view: function () {
return m.fragment(
{ key: personId }
// amit korábban a nézetben tartottál
);
},
};
}
Ez nem fog működni, mert a kulcs nem a teljes komponensre vonatkozik. Csak a megjelenítésre vonatkozik, így nem fogod elérni az adatok újratöltését, ahogy remélted.
Ehelyett használd az ott bemutatott megoldást, a kulcsot a komponenst használva a vnode-ba helyezve, nem pedig magába a komponensbe.
// AJÁNLOTT
return [m(Person, { id: m.route.param('id'), key: m.route.param('id') })];
Elemek szükségtelen kulcsolása
Gyakori tévhit, hogy a kulcsok önálló identitások. A Mithril.js megköveteli, hogy egy töredék gyermekeinek vagy mindegyikének legyen kulcsa, vagy egyikének se, és hibát fog dobni, ha ezt elfelejted. Tegyük fel, hogy van ez az elrendezésed:
m('.page', m('.header', { key: 'header' }), m('.body'), m('.footer'));
Ez nyilvánvalóan hibát fog dobni, mivel a .header
-nek van kulcsa, a .body
-nak és a .footer
-nek pedig nincs. De a lényeg: nincs szükséged kulcsokra ehhez. Ha azt veszed észre, hogy kulcsokat használsz ilyen esetekben, a megoldás nem az, hogy kulcsokat adsz hozzá, hanem az, hogy eltávolítod őket. Csak akkor adj hozzá kulcsokat, ha valóban szükséged van rájuk. Igen, a mögöttes DOM csomópontoknak vannak identitásaik, de a Mithril.js-nek nem kell nyomon követnie ezeket az identitásokat a helyes frissítéshez. Gyakorlatilag soha nem teszi. Csak olyan listáknál van szükség kulcsokra, ahol minden bejegyzéshez tartozik valamilyen társított állapot, amelyet maga a Mithril.js nem követ nyomon, akár egy modellben, egy komponensben vagy magában a DOM-ban.
Még egy utolsó dolog: kerüld a statikus kulcsok használatát. Mindig szükségtelenek. Ha nem számítod ki a key
attribútumodat, valószínűleg valamit rosszul csinálsz.
Ne feledd, hogy ha valóban szükséged van egyetlen kulccsal ellátott elemre elkülönítve, használj egygyermekes kulccsal ellátott töredéket. Ez egy tömb egyetlen gyermekkel, amely egy kulccsal ellátott elem, például [m("div", {key: foo})]
.
Kulcstípusok keverése
A kulcsok objektumtulajdonságok neveként vannak értelmezve. Ez azt jelenti, hogy az 1
és a "1"
azonosként van kezelve. Ha nem akarsz meglepetéseket, ne keverd a kulcstípusokat, ha teheted. Ha mégis, akkor duplikált kulcsokkal és váratlan viselkedéssel találhatod szemben magad.
// NE HASZNÁLD
var things = [
{ id: '1', name: 'Book' },
{ id: 1, name: 'Cup' },
];
Ha feltétlenül muszáj, és nincs ráhatásod, használj egy előtagot, amely jelzi a típusát, hogy azok megkülönböztethetőek maradjanak.
things.map(function (thing) {
return m(
'.thing',
{ key: typeof thing.id + ':' + thing.id }
// ...
);
});
Kulccsal ellátott elemek elrejtése lyukakkal
A "lyukak", mint a null
, undefined
és a logikai értékek, nem kulcsolt vnode-oknak minősülnek, így az ehhez hasonló kód nem fog működni:
// NE HASZNÁLD
things.map(function (thing) {
return shouldShowThing(thing)
? m(Thing, { key: thing.id, thing: thing })
: null;
});
Ehelyett szűrd le a listát, mielőtt visszaadod, és a Mithril.js a helyes dolgot fogja tenni. A legtöbb esetben az Array.prototype.filter
pontosan az, amire szükséged van, és mindenképpen ki kell próbálnod.
// AJÁNLOTT
things
.filter(function (thing) {
return shouldShowThing(thing);
})
.map(function (thing) {
return m(Thing, { key: thing.id, thing: thing });
});
Duplikált kulcsok
A töredékelemek kulcsainak egyedinek kell lenniük, különben nem egyértelmű, hogy melyik kulcsnak hova kell kerülnie. Problémáid lehetnek azzal is, hogy az elemek nem úgy mozognak, ahogy kellene.
// NE HASZNÁLD
var things = [
{ id: '1', name: 'Book' },
{ id: '1', name: 'Cup' },
];
A Mithril.js egy üres objektumot használ a kulcsok indexekhez való hozzárendeléséhez, hogy tudja, hogyan kell megfelelően frissíteni a kulccsal ellátott töredékeket. Ha duplikált kulcsod van, akkor nem egyértelmű, hogy az az elem hova került, ezért a Mithril.js ebben az esetben hibásan fog működni, és váratlan dolgokat fog csinálni a frissítéskor, különösen, ha a lista megváltozott. A különálló kulcsok szükségesek ahhoz, hogy a Mithril.js megfelelően összekapcsolja a régi és az új csomópontokat, ezért helyileg egyedi értéket kell választanod kulcsként.
Objektumok használata kulcsokhoz
A töredékelemek kulcsait tulajdonságkulcsokként kezeljük. Az ehhez hasonló dolgok nem úgy fognak működni, ahogy gondolod.
// NE HASZNÁLD
things.map(function (thing) {
return m(Thing, { key: thing, thing: thing });
});
Ha van egy toString
metódusod rajta, akkor az meghívásra kerül, és ki leszel szolgáltatva annak, amit visszaad, esetleg nem is realizálva, hogy az a metódus egyáltalán meghívásra kerül. Ha nincs, akkor az összes objektumod "[object Object]"
-re fog szöveggé alakulni, és így egy csúnya duplikált kulcs problémád lesz.