Single-Page-Anwendungen Framework-unabhängig entwickeln

Strukturierung

Anzeige

Immer mehr Logik verlagert sich auf den Client, wodurch auch die Menge an JavaScript zunimmt. Um langfristig eine wartbare und testbare Applikation zu erhalten, ist es unerlässlich, sich über die Strukturierung ihrer Logik Gedanken zu machen. Positiv ist, dass es bereits bewährte Konzepte gibt, die im SPA-Kontext verbreitet sind.

MVC ist der Klassiker unter den Architekturmustern und im SPA-Kontext am verbreitetsten. Frameworks wie Backbone.js oder AngularJS nutzen MVC als Architektur. Es besteht aus den Komponenten Model (Datenrepräsentation), View (Darstellung) und Controller (Steuerung, enthält die Applikationslogik). Jedoch kommt das Pattern nicht immer in Reinform zum Einsatz. AngularJS (in der Version 1.x) beispielsweise verwendet alle Komponenten aus MVC, nutzt darüber hinaus aber auch Konzepte aus MVVM (Data Binding).

Besonders interessant ist es, zu beleuchten, wie die einzelnen Komponenten in den unterschiedlichen Frameworks implementiert sind. Die größten Unterschiede finden sich dabei im Model und innerhalb der View. Der Controller ist immer mit JavaScript umgesetzt, da sich hier die Applikationslogik befindet. Das Model besteht in AngularJS einfach aus JSON-Daten, während Backbone.js es durch JavaScript abstrahiert.

Das Model von Backbone.js ist hier beispielhaft dargestellt:

var Model = Backbone.Model.extend({
...

initialize: function() { ... },

comments: function() { ... }

...
});

Das gleiche gilt für die View. Sie ist in AngularJS reines HTML-Markup, während Backbone.js sie ebenfalls durch JavaScript abstrahiert.

Eine View von Backbone.js sieht wie folgt aus:

var View = Backbone.View.extend({
...

initialize: function() {
this.listenTo(this.model, "change", this.render);
},

render: function() {
...
}

...

});

Werden Model und View durch JavaScript abstrahiert, haben Entwickler die Möglichkeit, durch eigenen JS-Code Einfluss zu nehmen. Beispielsweise um innerhalb der View DOM-Manipulationen vorzunehmen oder um explizit auf Änderungen des Models reagieren zu können.

Sind Model und View hingegen als JSON und HTML realisiert, sind die Einflussmöglichkeiten der Entwickler eingeschränkt. Das Framework gibt in dem Fall vor, wie View und Model durch die API des Frameworks zu verändern sind.

MVVM stellt ein weiteres Architekturmuster im SPA-Kontext dar und besteht aus den Komponenten Model (Datenrepräsentation und Businesslogik), View (Darstellung) und ViewModel (Präsentationslogik). Knockout ist das bekannteste Framework, welches MVVM als Basisarchitektur verwendet.

MVVM wurde von Microsoft mit dem Ziel ins Leben gerufen, die View völlig losgelöst vom Rest der Applikation entwickeln zu können. Die Kapselung der View (reines HTML) ist durch das Verbinden mit dem ViewModel (als Funktion abstrahiert) über Data Binding realisiert. Letzteres ist der typische Indikator für MVVM und ist in zwei Ausprägungen möglich: Beim One-Way Data Binding gibt es eine unidirektionale Verbindung vom ViewModel zur View. Ändert sich etwas im ViewModel, synchronisiert die Anwendung die View automatisch. Beim Two-Way Data Binding findet die Synchronisation in beide Richtungen statt.

Eine beispielhafte Implementierung von MVVM in ES2015 sieht wie folgt aus:

const observable = initialValue => {
let value = initialValue;

const listeners = [];

const subscribe = listener => {
listeners.push(listener);
};

const get = () => value;

const set = newValue => {
value = newValue;
listeners.forEach(listener => listener());
};

return {subscribe, get, set};
};

Um MVVM zu implementieren, bedarf es der Möglichkeit, auf Änderungen reagieren zu können. Dafür eignet sich das Observer-Pattern, das im Beispiel oben durch die Funktion observable implementiert ist. Ein Observable setzt sich aus einem initialen Wert der Möglichkeit, einen neuen Wert zu setzen, und der, den aktuellen abzufragen zusammen.

Durch Aufruf der subscribe-Methode lässt sich von außen auf Änderungen des Observables lauschen. Dafür ist eine Funktion zu übergeben, die bei Veränderungen zum Aufruf kommt.

View und ViewModel werden über Data Binding verbunden. Ein ViewModel sieht dabei folgendermaßen aus:

const viewModel = {
message: observable('Hello World!')
};

Das ViewModel ist in dem Fall ein einfaches JavaScript Object-Literal (Funktionsobjekte oder ES6-Klassen lassen sich ebenfalls verwenden) mit dem Attribut message. Das Attribut ist ein Observable mit dem initialen Wert "Hello World!".

<!DOCTYPE html>
<html>
<head>
<title>MVVM</title>
</head>
<body>
<p data-bind="message"></p>

<label for="msg">Message:</label>
<input type="text" id="msg" data-bind="message">

<script src="./mvvm.js"></script>
</body>
</html>

Um eine Relation zwischen ViewModel und View herzustellen, kommen innerhalb der View data-Attribute zum Einsatz. data-bind="message" gibt sowohl im Paragraph-Element als auch im Input-Element an, dass das Attribut message des ViewModels innerhalb der View darzustellen ist.

Um Data Binding in JavaScript zu aktivieren, gibt es die Funktion applyDataBinding, die sich mit dem aktuellen ViewModel als Parameter aufrufen lässt.

const viewModel = {
message: observable('Hello World!')
};

applyDataBinding(viewModel);

Funktionsweise und Implementierung von
applyDataBinding zeigt der folgende
Quelltextauzug:

const applyDataBinding = viewModel => {

const applyBindingFromViewModelToView = () => {

const updateViewElements = (selector, newValue) => {
const isInputElement = element => {
return element.nodeName === 'INPUT' ||↵
element.nodeName === 'TEXTAREA';
};

Array.from(document.querySelectorAll(selector)).forEach(element
=> { const propertyToUpdate = ↵
isInputElement(element) ? 'value' : ↵
'textContent';
element[propertyToUpdate] = newValue;
})
};

const updateView = propertyName => {
// Get the newest value of the viewmodel property
const newValue = viewModel[propertyName].get();

// Get the DOM Element with the correct propertyName
const selector = `[data-bind='${propertyName}']`;

updateViewElements(selector, newValue);
};

// Iterate over all properties and register change listener
Object.keys(viewModel).forEach(propertyName => {
if (typeof(viewModel[propertyName].subscribe) !== ↵
'undefined') {
viewModel[propertyName].subscribe(() => {
updateView(propertyName);
});
updateView(propertyName);
}
});

};

const applyBindingFromViewToViewModel = () => {
const onInputChanged = evt => {
const attr = evt.target.getAttribute("data-bind");
if (typeof viewModel[attr] !== 'undefined') {
viewModel[attr].set(evt.target.value);
}
}
document.removeEventListener('input', onInputChanged);
document.addEventListener('input', onInputChanged);
};

applyBindingFromViewModelToView();
applyBindingFromViewToViewModel();
};

Die interne Funktion applyBindingFromViewModelToView iteriert dabei über alle Properties des ViewModels und registriert Change Listener. Ändert sich eine Property, wählt die Anwendung die entsprechenden View-Elemente via Data-Attribut aus und aktualisiert sie mit dem neuen Wert (siehe Funktion updateView). Damit auch die initialen Werte des ViewModels in der View dargestellt werden, wird updateView einmalig zu Beginn für jede ViewModel-Property aufgerufen. Die Funktion updateViewElements kümmert sich um das korrekte Übernehmen der neuen Werte in die DOM-Elemente. Handelt es sich um ein Eingabeelement (Text-Input oder TextArea), so wird der neue Wert via value gesetzt, ansonsten via textContent.]]

Die Funktion applyBindingFromViewToViewModel erweitert die Implementierung zum Two-Way Data Binding. Dabei kommt das Input-Event des Document-Objekts zum Einsatz. Es ermöglicht die zentrale Überwachung von Änderungen aller DOM-Elemente. Ändert sich beispielsweise der Wert eines Text-Inputs, liest die Anwendung innerhalb der Funktion onInputChanged das Data-Attribut data-bind aus. Existiert eine ViewModel-Property mit gleichem Namen, gelangt der aktuelle Wert des Text-Inputs in das ViewModel, indem die set-Methode mit dem neuen Wert aufgerufen wird. Da Change-Listener für jede ViewModel-Property registriert sind, schließt sich hier der Kreis und die View wird erneut aktualisiert. Dadurch entsteht Two-Way Data Binding.

Typische Anwendungsbereiche derartiger Datenbindungen sind Applikationen mit vielen Formulareingaben. Ein Beispiel wäre eine Einstellungsseite, die Daten initial vom Server lädt, in einem Formular verändert und anschließend auf dem Server im gleichen Format speichert. Hier spielt Two-Way Data Binding seine Stärken voll aus.

Ist hingegen ein komplexer Prozess mit vielen beteiligten Komponenten abzubilden, stößt eine derartige Datenbindung an ihre Grenzen. An der Stelle ist es hilfreich, den Kontext und somit den Grund für die Änderung der Daten zu kennen.

Anzeige