Komponenten
Struktur
Komponenten sind ein Mechanismus, um Teile einer Ansicht zu kapseln, um Code einfacher zu organisieren und/oder wiederzuverwenden.
Jedes JavaScript-Objekt, das eine view
-Methode besitzt, ist eine Mithril.js-Komponente. Komponenten können über das m()
-Utility verwendet werden:
// Definiere deine Komponente
var Example = {
view: function (vnode) {
return m('div', 'Hello');
},
};
// Verwende deine Komponente
m(Example);
// Äquivalentes HTML
// <div>Hello</div>
Lebenszyklusmethoden
Komponenten können die gleichen Lebenszyklusmethoden wie virtuelle DOM-Knoten haben. Beachte, dass vnode
als Argument an jede Lebenszyklusmethode übergeben wird, sowie an view
(wobei der vorherige vnode zusätzlich an onbeforeupdate
übergeben wird):
var ComponentWithHooks = {
oninit: function (vnode) {
console.log('initialisiert wurde');
},
oncreate: function (vnode) {
console.log('DOM erstellt');
},
onbeforeupdate: function (newVnode, oldVnode) {
return true;
},
onupdate: function (vnode) {
console.log('DOM aktualisiert');
},
onbeforeremove: function (vnode) {
console.log('Exit-Animation kann beginnen');
return new Promise(function (resolve) {
// Aufruf nach Abschluss der Animation
resolve();
});
},
onremove: function (vnode) {
console.log('DOM-Element wird entfernt');
},
view: function (vnode) {
return 'hello';
},
};
Wie andere Arten von virtuellen DOM-Knoten können Komponenten zusätzliche Lebenszyklusmethoden definiert haben, wenn sie als vnode-Typen verwendet werden.
function initialize(vnode) {
console.log('als vnode initialisiert');
}
m(ComponentWithHooks, { oninit: initialize });
Lebenszyklusmethoden in vnodes überschreiben keine Komponentenmethoden und umgekehrt. Komponenten-Lebenszyklusmethoden werden immer nach der entsprechenden Methode des vnodes ausgeführt.
Achte darauf, dass du keine Lebenszyklusmethodennamen für deine eigenen Callback-Funktionsnamen in vnodes verwendest.
Um mehr über Lebenszyklusmethoden zu erfahren, siehe die Seite Lebenszyklusmethoden.
Daten an Komponenten übergeben
Daten können an Komponenteninstanzen übergeben werden, indem ein attrs
-Objekt als zweiter Parameter in der Hyperscript-Funktion verwendet wird:
m(Example, { name: 'Floyd' });
Auf diese Daten kann in der Ansicht oder den Lebenszyklusmethoden der Komponente über vnode.attrs
zugegriffen werden:
var Example = {
view: function (vnode) {
return m('div', 'Hello, ' + vnode.attrs.name);
},
};
HINWEIS: Lebenszyklusmethoden können auch im attrs
-Objekt definiert werden. Vermeide es daher, ihre Namen für eigene Rückruffunktionen zu verwenden, da diese auch von Mithril.js selbst aufgerufen würden. Verwende sie in attrs
nur, wenn du sie explizit als Lebenszyklusmethoden verwenden möchtest.
State
Wie alle virtuellen DOM-Knoten können auch Komponenten-VNodes einen State haben. Der Komponenten-State ist nützlich für objektorientierte Architekturen, Kapselung und Trennung von Verantwortlichkeiten.
Im Gegensatz zu vielen anderen Frameworks ist zu beachten, dass das Verändern des Komponenten-States keine Redraws oder DOM-Aktualisierungen auslöst. Stattdessen werden Redraws durchgeführt, wenn Event-Handler ausgelöst werden, wenn HTTP-Requests, die von m.request gestellt werden, abgeschlossen sind oder wenn der Browser zu anderen Routen navigiert. Die State-Mechanismen von Mithril.js existieren lediglich als Komfortfunktion für Anwendungen.
Wenn eine State-Änderung auftritt, die nicht auf eine der oben genannten Bedingungen zurückzuführen ist (z. B. nach einem setTimeout
), kann man m.redraw()
verwenden, um manuell ein Redraw auszulösen.
Closure-Komponenten-State
In den obigen Beispielen ist jede Komponente als POJO (Plain Old JavaScript Object) definiert, das von Mithril.js intern als Prototyp für die Instanzen dieser Komponente verwendet wird. Es ist möglich, den Komponenten-State mit einem POJO zu verwenden, aber das ist nicht der sauberste oder einfachste Ansatz. Dafür verwenden wir eine Closure-Komponente, die einfach eine Wrapper-Funktion ist, die eine POJO-Komponenteninstanz zurückgibt, die wiederum ihren eigenen, geschlossenen Scope trägt.
Bei Closure-Komponenten kann der State einfach durch Variablen verwaltet werden, die innerhalb der äußeren Funktion deklariert sind:
function ComponentWithState(initialVnode) {
// State-Variable der Komponente, eindeutig für jede Instanz
var count = 0;
// POJO-Komponenteninstanz: jedes Objekt mit einer
// View-Funktion, die einen vnode zurückgibt
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'
)
);
},
};
}
Alle Funktionen, die innerhalb der Closure deklariert werden, haben ebenfalls Zugriff auf ihre State-Variablen.
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'
)
);
},
};
}
Closure-Komponenten können wie POJOs verwendet werden, z. B. m(ComponentWithState, { passedData: ... })
.
Ein großer Vorteil von Closure-Komponenten ist, dass wir uns keine Gedanken über das Binden von this
machen müssen, wenn wir Event-Handler-Rückrufe anhängen. Tatsächlich wird this
überhaupt nicht verwendet und wir müssen nie über this
-Kontext-Unklarheiten nachdenken.
POJO-Komponenten-State
Es wird empfohlen, Closures zur Verwaltung des Komponenten-States zu verwenden. Wenn man jedoch einen Grund hat, den State in einem POJO zu verwalten, kann auf den State einer Komponente auf drei Arten zugegriffen werden: als Blueprint bei der Initialisierung, über vnode.state
und über das this
-Keyword in Komponentenmethoden.
Bei der Initialisierung
Für POJO-Komponenten ist das Komponentenobjekt der Prototyp jeder Komponenteninstanz, sodass jede Eigenschaft, die auf dem Komponentenobjekt definiert ist, als Eigenschaft von vnode.state
zugänglich ist. Dies ermöglicht eine einfache "Blueprint"-State-Initialisierung.
Im folgenden Beispiel wird data
zu einer Eigenschaft des vnode.state
-Objekts der ComponentWithInitialState
-Komponente.
var ComponentWithInitialState = {
data: 'Initial content',
view: function (vnode) {
return m('div', vnode.state.data);
},
};
m(ComponentWithInitialState);
// Äquivalentes HTML
// <div>Initial content</div>
Über vnode.state
Wie man sehen kann, kann auf den State auch über die Eigenschaft vnode.state
zugegriffen werden, die für alle Lebenszyklusmethoden sowie die view
-Methode einer Komponente verfügbar ist.
var ComponentWithDynamicState = {
oninit: function (vnode) {
vnode.state.data = vnode.attrs.text;
},
view: function (vnode) {
return m('div', vnode.state.data);
},
};
m(ComponentWithDynamicState, { text: 'Hello' });
// Äquivalentes HTML
// <div>Hello</div>
Über das this-Keyword
Auf den State kann auch über das this
-Keyword zugegriffen werden, das für alle Lebenszyklusmethoden sowie die view
-Methode einer Komponente verfügbar ist.
var ComponentUsingThis = {
oninit: function (vnode) {
this.data = vnode.attrs.text;
},
view: function (vnode) {
return m('div', this.data);
},
};
m(ComponentUsingThis, { text: 'Hello' });
// Äquivalentes HTML
// <div>Hello</div>
Beachte, dass der Wert von this
in verschachtelten anonymen Funktionen bei Verwendung von ES5-Funktionen nicht die Komponenteninstanz ist. Es gibt zwei empfohlene Möglichkeiten, diese JavaScript-Einschränkung zu umgehen: Verwende Pfeilfunktionen oder, falls diese nicht unterstützt werden, verwende vnode.state
.
Klassen
Wenn es den Bedürfnissen entspricht (wie in objektorientierten Projekten), können Komponenten auch mit Klassen geschrieben werden:
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`);
}
}
Klassenkomponenten müssen eine view()
-Methode definieren, die über .prototype.view
erkannt wird, damit der Baum gerendert wird.
Sie können auf die gleiche Weise wie reguläre Komponenten verwendet werden.
// BEISPIEL: über m.render
m.render(document.body, m(ClassComponent));
// BEISPIEL: über m.mount
m.mount(document.body, ClassComponent);
// BEISPIEL: über m.route
m.route(document.body, '/', {
'/': ClassComponent,
});
// BEISPIEL: Komponentenzusammensetzung
class AnotherClassComponent {
view() {
return m('main', [m(ClassComponent)]);
}
}
Klassenkomponenten-State
Mit Klassen kann der State durch Eigenschaften und Methoden der Klasseninstanz verwaltet und über this
aufgerufen werden:
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'
)
);
}
}
Beachte, dass wir Pfeilfunktionen für die Event-Handler-Rückrufe verwenden müssen, damit der this
-Kontext korrekt referenziert werden kann.
Mischen von Komponententypen
Komponenten können frei gemischt werden. Eine Klassenkomponente kann Closure- oder POJO-Komponenten als Kinder haben usw.
Spezielle Attribute
Mithril.js legt spezielle Semantik auf mehrere Property-Keys, daher sollte man es normalerweise vermeiden, diese in normalen Komponentenattributen zu verwenden.
- Lebenszyklusmethoden:
oninit
,oncreate
,onbeforeupdate
,onupdate
,onbeforeremove
undonremove
key
, das verwendet wird, um die Identität in Keyed Fragments zu verfolgentag
, das verwendet wird, um VNodes von normalen Attributobjekten und anderen Dingen zu unterscheiden, die keine VNode-Objekte sind.
Vermeide Anti-Patterns
Obwohl Mithril.js flexibel ist, werden einige Codemuster nicht empfohlen:
Vermeide aufgeblähte Komponenten
Im Allgemeinen ist eine "aufgeblähte Komponente" eine Komponente, die benutzerdefinierte Instanzmethoden hat. Mit anderen Worten, man sollte vermeiden, Funktionen an vnode.state
oder this
anzuhängen. Es ist äußerst selten, dass es Logik gibt, die logisch in eine Komponenteninstanzmethode passt und die nicht von anderen Komponenten wiederverwendet werden kann. Es ist relativ häufig, dass diese Logik möglicherweise von einer anderen Komponente in der Zukunft benötigt wird.
Es ist einfacher, Code zu refaktorisieren, wenn diese Logik in der Datenebene platziert wird, als wenn sie an den State einer Komponente gebunden ist.
Betrachte diese aufgeblähte Komponente:
// views/Login.js
// VERMEIDEN
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'
),
]);
},
};
Normalerweise existiert im Kontext einer größeren Anwendung eine Login-Komponente wie die obige neben Komponenten für die Benutzerregistrierung und die Passwortwiederherstellung. Stell dir vor, wir möchten das E-Mail-Feld beim Navigieren vom Login-Bildschirm zu den Registrierungs- oder Passwortwiederherstellungsbildschirmen (oder umgekehrt) vorab ausfüllen können, sodass der Benutzer seine E-Mail nicht erneut eingeben muss, wenn er versehentlich die falsche Seite ausgefüllt hat (oder vielleicht möchtest du den Benutzer zum Registrierungsformular weiterleiten, wenn ein Benutzername nicht gefunden wird).
Wir sehen sofort, dass das Teilen der Felder username
und password
von dieser Komponente mit einer anderen schwierig ist. Dies liegt daran, dass die aufgeblähte Komponente ihren State kapselt, was es definitionsgemäß schwierig macht, von außen auf diesen State zuzugreifen.
Es ist sinnvoller, diese Komponente umzugestalten und den State-Code aus der Komponente in die Datenebene der Anwendung zu verschieben. Dies kann so einfach sein wie das Erstellen eines neuen Moduls:
// models/Auth.js
// BEVORZUGEN
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;
Dann können wir die Komponente bereinigen:
// views/Login.js
// BEVORZUGEN
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'
),
]);
},
};
Auf diese Weise ist das Auth
-Modul jetzt die zentrale Datenquelle für den Auth-bezogenen State, und eine Register
-Komponente kann leicht auf diese Daten zugreifen und bei Bedarf sogar Methoden wie canSubmit
wiederverwenden. Wenn außerdem Validierungscode erforderlich ist (z. B. für das E-Mail-Feld), muss man nur setEmail
ändern, und diese Änderung führt die E-Mail-Validierung für jede Komponente durch, die ein E-Mail-Feld ändert.
Als Bonus ist zu beachten, dass wir .bind
nicht mehr verwenden müssen, um einen Verweis auf den State für die Event-Handler der Komponente zu behalten.
Leite vnode.attrs
nicht direkt an andere vnodes weiter
Manchmal ist es wünschenswert, eine Schnittstelle flexibel zu halten und die Implementierung zu vereinfachen, indem Attribute an eine bestimmte Kindkomponente oder ein Element weitergeleitet werden, wie beispielsweise bei Bootstrap's Modal. Es könnte verlockend sein, die Attribute eines vnodes wie folgt weiterzuleiten:
// VERMEIDEN
var Modal = {
// ...
view: function (vnode) {
return m('.modal[tabindex=-1][role=dialog]', vnode.attrs, [
// Weiterleitung von `vnode.attrs` hier ^
// ...
]);
},
};
Wenn man es wie oben macht, könnte man bei der Verwendung auf Probleme stoßen:
var MyModal = {
view: function () {
return m(
Modal,
{
// Dies schaltet es zweimal um, sodass es nicht angezeigt wird
onupdate: function (vnode) {
if (toggle) $(vnode.dom).modal('toggle');
},
},
[
// ...
]
);
},
};
Stattdessen sollte man einzelne Attribute in vnodes weiterleiten:
// BEVORZUGEN
var Modal = {
// ...
view: function (vnode) {
return m('.modal[tabindex=-1][role=dialog]', vnode.attrs.attrs, [
// Weiterleitung von `attrs:` hier ^
// ...
]);
},
};
// Beispiel
var MyModal = {
view: function () {
return m(Modal, {
attrs: {
// Dies schaltet es einmal um
onupdate: function (vnode) {
if (toggle) $(vnode.dom).modal('toggle');
},
},
// ...
});
},
};
Manipuliere nicht children
Wenn eine Komponente eine bestimmte Vorstellung davon hat, wie sie Attribute oder Kindelemente verwendet, sollte man auf benutzerdefinierte Attribute umsteigen.
Oft ist es wünschenswert, mehrere Sätze von Kindelementen zu definieren, z. B. wenn eine Komponente einen konfigurierbaren Titel und einen Textkörper hat.
Vermeide die Destrukturierung der Eigenschaft children
für diesen Zweck.
// VERMEIDEN
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')]);
// umständlicher Anwendungsfall
m(Header, [
[m('h1', 'My title'), m('small', 'A small note')],
m('h2', 'Lorem ipsum'),
]);
Die obige Komponente bricht die Annahme, dass Kindelemente im gleichen zusammenhängenden Format ausgegeben werden, in dem sie empfangen werden. Es ist schwierig, die Komponente zu verstehen, ohne ihre Implementierung zu lesen. Verwende stattdessen Attribute als benannte Parameter und reserviere children
für einheitliche Kindinhalte:
// BEVORZUGEN
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'),
});
// klarerer Anwendungsfall
m(BetterHeader, {
title: [m('h1', 'My title'), m('small', 'A small note')],
tagline: m('h2', 'Lorem ipsum'),
});
Definiere Komponenten statisch, rufe sie dynamisch auf
Vermeide das Erstellen von Komponentendefinitionen innerhalb von Views
Wenn man eine Komponente innerhalb einer view
-Methode erstellt (entweder direkt inline oder durch Aufrufen einer Funktion, die dies tut), hat jedes Redraw einen anderen Klon der Komponente. Beim Vergleich von Komponenten-VNodes wird davon ausgegangen, dass zwei Komponenten unterschiedlich sind, selbst wenn sie letztendlich äquivalenten Code ausführen, falls die Komponente, auf die der neue VNode verweist, nicht strikt mit der Komponente übereinstimmt, auf die der alte VNode verweist. Dies bedeutet, dass Komponenten, die dynamisch über eine Fabrik erstellt werden, immer neu erstellt werden.
Aus diesem Grund sollte man vermeiden, Komponenten neu zu erstellen. Verwende stattdessen Komponenten idiomatisch.
// VERMEIDEN
var ComponentFactory = function (greeting) {
// erstellt bei jedem Aufruf eine neue Komponente
return {
view: function () {
return m('div', greeting);
},
};
};
m.render(document.body, m(ComponentFactory('hello')));
// Das zweite Aufrufen erstellt div von Grund auf neu, anstatt nichts zu tun
m.render(document.body, m(ComponentFactory('hello')));
// BEVORZUGEN
var Component = {
view: function (vnode) {
return m('div', vnode.attrs.greeting);
},
};
m.render(document.body, m(Component, { greeting: 'hello' }));
// Das zweite Aufrufen ändert das DOM nicht
m.render(document.body, m(Component, { greeting: 'hello' }));
Vermeide das Erstellen von Komponenteninstanzen außerhalb von Views
Umgekehrt gilt aus ähnlichen Gründen: Wenn eine Komponenteninstanz außerhalb einer View erstellt wird, führen zukünftige Redraws eine Gleichheitsprüfung des Knotens durch und überspringen ihn. Daher sollten Komponenteninstanzen immer innerhalb von Views erstellt werden:
// VERMEIDEN
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];
},
});
Im obigen Beispiel erhöht das Klicken auf die Schaltfläche der Zählerkomponente den State-Zähler, aber seine View wird nicht ausgelöst, da der VNode, der die Komponente darstellt, denselben Verweis hat und der Renderprozess sie daher nicht vergleicht. Man sollte Komponenten immer in der View aufrufen, um sicherzustellen, dass ein neuer VNode erstellt wird:
// BEVORZUGEN
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)];
},
});