zurück zum Artikel

Model View ViewModel mit Knockout.js

Golo Roden

HTML definiert die Struktur, JavaScript das Verhalten einer Webanwendung. Doch wie verbindet man beide Welten auf elegante Art? Die JavaScript-Bibliothek Knockout.js schlägt mit dem MVVM-Entwurfsmuster eine Brücke.

Model View ViewModel mit Knockout.js

HTML und JavaScript sind die Grundbausteine des modernen Webs: HTML definiert die Struktur, JavaScript das Verhalten einer Webanwendung. Doch wie verbindet man beide Welten auf elegante Art? Die JavaScript-Bibliothek Knockout.js schlägt mit dem MVVM-Entwurfsmuster eine Brücke.

Die JavaScript-Bibliothek Knockout in einem Satz zu beschreiben fällt verhältnismäßig leicht: Ihre einzige Aufgabe besteht darin, das Entwurfsmuster Model View ViewModel (MVVM) für HTML zu implementieren und auf diesem Weg das Konzept der Datenbindung zwischen HTML und JavaScript zu ermöglichen.

MVVM geht auf eine Veröffentlichung von John Gossmann [1] im Jahr 2005 zurück und zielte zunächst auf die beiden Microsoft-Techniken Windows Presentation Foundation (WPF) und Silverlight ab, lässt sich aber prinzipiell für jede beliebige deklarative UI-Technik adaptieren.

Die Idee hinter MVVM ist, zwischen Ansicht und Domänenmodell eine weitere Schicht einzufügen, die eine lose Kopplung der Ansicht ermöglicht. Da diese Schicht in ihrem Aufbau der konkreten Ansicht entspricht, bezeichnet man sie als ViewModel. Es enthält für jedes UI-Element passende Daten, die über Datenbindung an die jeweilige Ansicht gekoppelt werden. Verwendet man hierfür eine bidirektionale Datenbindung, wirken sich Eingaben des Anwenders direkt auf das ViewModel und programmatische Änderungen am ViewModel direkt auf die Ansicht aus. Zugleich lösen Schaltflächen entsprechende Funktionen am ViewModel aus (s. Abb. 1).

Ein wesentlicher Vorteil dieses Vorgehens besteht darin, sämtliche UI-Logik aus der Ansicht in das zugehörige ViewModel zu verlagern, das man auf einfachem Weg automatisiert testen kann. Umfangreiche, fehleranfällige und vor allem aufwendige, von Hand durchzuführende UI-Tests können daher entfallen.

Das ViewModel stellt als weitere Schicht eine lose Kopplung zwischen zwischen Ansicht und Domänenmodell her (Abb.1).

Das ViewModel stellt als weitere Schicht eine lose Kopplung zwischen zwischen Ansicht und Domänenmodell her (Abb.1).

Um Knockout zu verwenden, muss man zunächst die passende Skriptdatei von der Website der Bibliothek [2] herunterladen und in eine HTML-Datei einbinden:

<!doctype html>
<html>
<head>
<title>Knockout-Demo</title>
</head>
<body>
<script type="text/javascript" src="knockout-2.2.1.js">
</script>

</body>
</html>

Knockout stellt seine Funktionen anschließend über das globale JavaScript-Objekt ko zur Verfügung. Um es in Aktion zu erleben, muss man der eben erstellten Webseite zunächst einige Steuerelemente hinzufügen, wie ein Eingabefeld und eine Schaltfläche:

<body>
<input type="text" />
<button>Senden</button>

<script type="text/javascript" src="knockout-2.2.1.js">

Um die Datenbindung für diese beiden Steuerelemente aktivieren zu können, benötigt man im nächsten Schritt ein ViewModel. Es muss eine Zeichenkette und eine Funktion enthalten, die man an den Wert des Eingabefelds beziehungsweise das Click-Ereignis der Schaltfläche binden kann.

Damit Knockout in der Lage ist, Änderungen an der Zeichenkette zu bemerken, verwendet man anstelle eines normalen Strings ein spezielles, von Knockout bereitgestelltes Objekt vom Typ observable, dem man optional einen Initialwert übergeben kann:

  <script type="text/javascript" src="knockout-2.2.1.js">
</script>
<script type="text/javascript">
var viewmodel = {
message: ko.observable(''),
send: function () {
// ...
}
};
</script>

</body>

Bevor sich Steuerelemente und ViewModel verbinden lassen, muss man zuerst die Datenbindung mit dem von Knockout bereitgestellten HTML-Attribut data-bind konfigurieren und Knockout anschließend durch einen Aufruf der Funktion applyBindings anweisen, das ViewModel an die Ansicht zu binden:

<body>
<input type="text" data-bind="value: message" />
<button data-bind="click: send">Senden</button>
<script type="text/javascript" src="knockout-2.2.1.js">
[...]
send: function () {
// ...
}
};
ko.applyBindings(viewmodel);
</script>

Ein Klick auf die Schaltfläche erzeugt zwar keinen Fehler, bewirkt aber auch sonst nichts. Eine einfache Aktion könnte sein, den derzeitigen Wert der message-Eigenschaft in einem Dialogfenster auszugeben und anschließend das Eingabefeld auf eine leere Zeichenkette zurückzusetzen.

Da message keine Zeichenkette, sondern ein Objekt vom Typ observable ist, kann man ihren Wert nicht auf dem üblichen Weg auslesen oder zuweisen. Stattdessen muss man die Eigenschaft im Stil einer get- oder set-Funktion aufrufen:

send: function () {
alert(this.message());
this.message('');

}

Klickt man nun auf die Schaltfläche, gibt der Webbrowser den derzeitigen Wert des Eingabefelds als Dialogfenster aus (s. Abb. 2), bevor er ihn auf eine leere Zeichenkette zurücksetzt.

Die Datenbindung an das Click-Ereignis der Schaltfläche bewirkt das Öffnen eines Dialogfensters (Abb. 2).

Die Datenbindung an das Click-Ereignis der Schaltfläche bewirkt das Öffnen eines Dialogfensters (Abb. 2).

Der Abgleich zwischen den Steuerelementen und dem ViewModel erfolgt standardmäßig beim Verlust des Fokus. Gelegentlich benötigt man jedoch eine Aktualisierung in Echtzeit, weshalb man die Datenbindung durch den optionalen Parameter valueUpdate entsprechend konfigurieren kann:

<body>
<input type="text" data-bind="value: message,
valueUpdate: 'afterkeydown'" />
<button data-bind="click: send">Senden</button>

Hat man das Zusammenspiel zwischen den Steuerelementen, dem ViewModel und Knockout einmal verstanden, weiß man im Prinzip, wie die Bibliothek funktioniert. Natürlich gibt es bedeutend mehr Möglichkeiten für Datenbindung, als nur die value- und click-Eigenschaften, zusätzliche Konzepte sind zu deren Einsatz allerdings nicht zu lernen.

So lassen sich beispielsweise die Sichtbarkeit, CSS-Klassen oder CSS-Stile durch Datenbindung beeinflussen. Hierzu dienen die Eigenschaften visible, css und style. Außerdem ist eine einfache Steuerung des Kontrollflusses möglich, um beispielsweise bestimmte Bereiche der Ansicht bedingt anzuzeigen, oder gewisse Steuerelemente wiederholt auszugeben.

In diesem Zusammenhang ist eine spezielle Variante des observable-Objekts interessant, das observableArray. Wie der Name bereits nahelegt, handelt es sich dabei um ein von Knockout gekapseltes Array, das sich für die Anzeige dynamischer Listen verwenden lässt.

Um beispielsweise alle bisher eingegebenen Nachrichten in einer Liste anzuzeigen, kann man das ViewModel um eine entsprechende Eigenschaft messages ergänzen, die ein ebensolches observableArray darstellt:

message: ko.observable(''),
messages: ko.observableArray([]),
send: function () {

Die send-Funktion verändert man derart, dass sie den eingegebenen Wert der Liste hinzufügt, bevor sie das Eingabefeld leert:

alert(this.message());
this.messages.unshift({ text: this.message() });
this.message('');

Zu guter Letzt bedarf es noch einer Option, die Liste auszugeben. Die einfachste Lösung hierfür besteht in der Verwendung eines ul-Elements, das man mit einer foreach-Datenbindung versieht und so das enthaltene li-Element für jeden Listeneintrag wiederholt:

<button data-bind="click: send">Senden</button>
<ul data-bind="foreach: messages">
<li data-bind="text: text"></li>
</ul>

<script type="text/javascript" src="knockout-2.2.1.js">

In einigen Fällen kann es sinnvoll sein, eine Vorlage wie in diesem Fall das li-Element nicht inline, sondern als eigenständigen Schnipsel zu definieren. Das ist beispielsweise dann nützlich, wenn man sie an verschiedenen Stellen benötigt oder dynamisch vom Webserver nachladen möchte.

Zu diesem Zweck kennt Knockout die template-Eigenschaft, die man eigenständig, aber auch im Zusammenspiel mit dem bereits vorgestellten foreach-Attribut verwenden kann. Die zugrunde liegende Idee ist, die Vorlage in einen Skriptblock auszulagern, den man über eine ID ansprechen kann und den der Webbrowser aufgrund des angegebenen Typs ignoriert:

</ul>
<script type="text/html" id="message-template">
<li data-bind="text: text"></li>
</script>

<script type="text/javascript" src="knockout-2.2.1.js">

Zusätzlich muss man die Definition der Liste derart ändern, dass sie die template-Eigenschaft verwendet, und ihren bisherigen Inhalt entfernen:

<button data-bind="click: send">Senden</button>
<ul data-bind="template: { name: 'message-template',
foreach: 'messages' }">

</ul>

Neben der direkten Datenbindung an Eigenschaften des ViewModels kennt Knockout auch die Möglichkeit, über den sogenannten Kontext [3] auf Eigenschaften zuzugreifen: So kann man beispielsweise mit der Eigenschaft $parent auf dem übergeordneten Kontext der Datenbindung operieren, was innerhalb von Schleifen gelegentlich durchaus nützlich ist.

Von Zeit zu Zeit genügen die in Knockout enthaltenen Features zur Datenbindung nicht. Alternativ lassen sich jedoch eigene Bindungen entwickeln, die man anschließend auf die selbe Art wie value oder click verwenden kann.

Um eine eigene Eigenschaft zu definieren, muss man dem ko.bindingHandlers-Objekt die entsprechende Funktion in Form zweier Funktionen hinzufügen:

ko.bindingHandlers.myBinding = {
init: function (element, valueAccessor, allBindingsAccessor,
viewModel, bindingContext) {
// ...
},
update: function (element, valueAccessor, allBindingsAccessor,
viewModel, bindingContext) {
// ...
}
};

Für den Einsatz genügt es, den Namen myBinding im data-bind-Attribut des gewünschten Elements anzugeben:

<div data-bind="myBinding: 23"></div>

Knockout ruft die init-Funktion für jedes Element, das die myBinding-Eigenschaft verwendet, einmal auf. Direkt im Anschluss, und zusätzlich nach jeder Änderung des zugrunde liegenden Werts, ruft die Bibliothek die update-Funktion auf. Mithilfe der an die Funktionen übergebenen Parameter gewährt Knockout unter anderem Zugriff auf das Element und den aktuellen Wert:

Darüber hinaus bietet Knockout noch einige weitere Möglichkeiten, etwa das Registrieren eigenentwickelter ereignisbehandelnder Funktionen und ein einfaches Serialisieren von ViewModels für den Datenaustausch mit dem Webserver.

Für Letzteres kennt Knockout die beiden Funktionen toJS und toJSON, die sich in ihrer Funktionsweise allerdings nicht wesentlich voneinander unterscheiden. So nimmt toJS ein ViewModel entgegen, klont dessen Struktur, evaluiert die derzeitigen Werte und gibt ein reines JavaScript-Objekt zurück:

var json = ko.toJS(viewmodel);
console.log(json);
// => {
// message: 'foo',
// messages: []
// }

Ruft man stattdessen die Funktion toJSON auf, erhält man als Ergebnis ein bereits als Zeichenkette serialisiertes JSON-Objekt:

var json = ko.toJSON(viewmodel);
console.log(json);
// => '{"message":"foo","messages":[]}'

Unter der Haube macht die toJSON-Funktion nämlich nichts anderes, als zunächst die Funktion toJS aufzurufen und deren Ergebnis an die Webbrowser-eigene Funktion JSON.stringify zu übergeben.

Für den Weg in der umgekehrten Richtung, also das Deserialisieren eines JSON-Objekts in ein ViewModel, bietet Knockout von Haus aus keine Unterstützung. Stattdessen muss man jeder Eigenschaft des ViewModels den jeweiligen Wert aus dem JSON-Objekt von Hand zuweisen.

Alternativ kann man hierfür das mapping [4]-Plug-in verwenden, das analog zu den beiden Funktionen toJS und toJSON die beiden Funktionen fromJS und fromJSON enthält, die jeweils versuchen, ein JSON-Objekt beziehungsweise ein als Zeichenkette serialisiertes JSON-Objekt auf Basis von vorgegebenen Konventionen zu deserialisieren.

Keinerlei Hilfsmittel sind für die eigentliche Kommunikation mit dem Webserver vorhanden. Stattdessen verweist die Dokumentation auf Bibliotheken und Funktionen von Drittanbietern, wie das ohnehin häufig eingesetzte jQuery und dessen getJSON [5]- und ajax [6]-Funktion.

Diese Zurückhaltung ist einerseits von Vorteil, erlaubt sie doch die flexible Kombination von Knockout mit der JavaScript-Bibliothek der Wahl. Zusätzlich besteht so die Möglichkeit, andere Kommunikationsmittel als AJAX zu verwenden, beispielsweise WebSockets.

Der Nachteil besteht andererseits darin, dass die meisten Projekte derartigen Infrastrukturcode in stets gleicher Form enthalten und Knockout Entwickler daher dazu verleitet, unnötig viel redundanten Code zu schreiben. Zudem lässt die Bibliothek hierdurch die Architektur und Struktur der Webanwendung komplett offen.

Ähnlich dürftig verhält sich Knockout, wenn eine Webanwendung verschiedene Ansichten enthält, zwischen denen der Anwender hin- und herwechseln kann, idealerweise mit Unterstützung der Zurück-Schaltfläche des Webbrowsers.

Hierfür verwenden Single-Page-Anwendungen üblicherweise HashBang-basierte URLs oder die History-API [7] von HTML5. Beides unterstützt Knockout jedoch nicht, sodass man zu diesem Zweck auf den Einsatz zusätzlicher Bibliotheken wie Sammy.js [8] angewiesen ist.

Auch das dynamische Nachladen und Wechseln von Ansichten ist Knockout fremd: Hier ist man komplett auf sich allein gestellt, die jeweiligen Fragmente sind von Hand nachzuladen und entsprechend zu verdrahten. Abhilfe schaffen wiederum zusätzliche Bibliotheken von Drittanbietern, wie pager.js [9].

Insgesamt zeigt sich an dieser Stelle klar die eingangs erwähnte Ausrichtung von Knockout: Die einzige Aufgabe besteht darin, das MVVM-Entwurfsmuster für HTML zu implementieren und das Konzept der Datenbindung zwischen HTML und JavaScript zu ermöglichen.

Diese Herausforderung meistert Knockout mit Bravour, es eignet sich für Single-Page-Anwendungen ohne den Einsatz zusätzlicher Werkzeuge allerdings nur bedingt.

Mehr Infos

MVC- und MVVM-Framework für JavaScript im Vergleich

Der vorliegende Artikel ist der erste Teil einer Serie, in der verschiedene JavaScript-MV*-Frameworks vorgestellt werden. Die nächste Folge wird sich mit einem der "Klassiker" beschäftigen: Backbone.js ist das mit Abstand am weitesten verbreitete MVC-Framework für die Entwicklung von Single-Page-Anwendungen.

Die steife Fokussierung von Knockout auf reines MVVM kann man durchaus als Stärke ansehen: Unter anderem verfügt es dadurch über eine ausgesprochen steile Lernkurve, die es Einsteigern vergleichsweise schnell ermöglicht, ansprechende Ergebnisse zu erzielen.

Zugleich lässt es sich aber auch als Schwäche auffassen, schließlich besteht kaum eine moderne Webanwendung heutzutage aus lediglich einer einzigen Ansicht. Der Einsatz von MVVM ohne das gleichzeitige Verwenden von Routen und dem Wechsel zwischen verschiedenen Ansichten kommt in der Praxis kaum vor.

Hierfür stets auf eine zweite – und für die Kommunikation mit dem Webserver gar eine dritte – Bibliothek zurückgreifen zu müssen, erschwert die Entwicklungsarbeit unnötig und erhöht die Gefahr von Kompatibilitätsproblemen und anderen Brüchen. Insofern bietet Knockout zwar einen raschen Einstieg, limitiert den Entwickler allerdings ebenso rasch.

Ein interessanter Aspekt der Bibliothek ist, dass sie außer den modernen, gängigen Webbrowsern auch ältere Versionen, insbesondere des Internet Explorer, unterstützt: So kann man Knockout beispielsweise in Verbindung mit dem Internet Explorer 6.0 und Firefox 2.0 problemlos verwenden.

Bei diesen Browsern empfiehlt es sich dann allerdings, als zusätzliche Bibliothek zumindest JSON3 [10] zu verwenden, um das korrekte Serialisieren und Deserialisieren von JSON-Objekten zu gewährleisten.

Knockout ist ein solides MVVM-Framework, stellt allerdings außer der Datenbindung keine weiteren Funktionen für die Entwicklung von Single-Page-Anwendungen zur Verfügung. Dadurch gelingt der Einstieg zwar ausgesprochen zügig, ebenso rasch stößt man aber auch an die Grenzen der Bibliothek.

Für die Anbindung von Webdiensten und das Verwalten von Routen und Ansichten ist Knockout auf Bibliotheken von Drittanbietern wie beispielsweise jQuery und pager.js angewiesen. Der Nachteil hierbei liegt in den zusätzlichen Abhängigkeiten, die man eingeht, und die sich unter anderem im Hinblick auf die langfristige Kompatibilität negativ auswirken könnten. Zudem steigt der Wartungsaufwand.

Dennoch ist Knockout durchaus einen Blick wert – dann nämlich, wenn eine Anwendung nur aus wenigen Ansichten besteht und Funktionen zu deren Routing und Verwaltung nicht benötigt werden.

Golo Roden
ist Gründer und Geschäftsführer der "the native web UG", eines auf native Webtechniken spezialisierten Unternehmens. Für die Entwicklung moderner Webanwendungen bevorzugt er JavaScript und Node.js und hat mit "Node.js & Co." das erste deutschsprachige Buch zum Thema geschrieben.
(jul [11])


URL dieses Artikels:
https://www.heise.de/-1928690

Links in diesem Artikel:
[1] http://blogs.msdn.com/b/johngossman/
[2] http://knockoutjs.com/
[3] http://knockoutjs.com/documentation/binding-context.html
[4] http://knockoutjs.com/documentation/plugins-mapping.html
[5] http://api.jquery.com/jQuery.getJSON/
[6] http://api.jquery.com/jQuery.ajax/
[7] http://diveintohtml5.info/history.html
[8] http://sammyjs.org/
[9] http://pagerjs.com/
[10] http://bestiejs.github.io/json3/
[11] mailto:jul@heise.de