ek

Beiträge zum Thema JS, HTML, CSS & anderem Kram

Require.js-Autoloader

Wenn aus dem “One-Pager” ein “Multi-One-Pager” wird, kann sich ein gewisser Overhead bezüglich der geladenen Skripte einstellen. Angenommen man baut eine Web-App, die aus vielen Einzelseiten besteht, und jede Einzelseite stellt für sich einen “One-Pager” dar, welcher nicht zwingend beim Besuch eines Nutzers aufgerufen wird. Dann werden – benutzt man Require.js zum Laden der Skripte – beim initialen Aufruf der Web-App alle Skripte geladen, auch wenn sie letztlich gar nicht benötigt werden.

In diesem Fall wäre es praktischer, mehrere Require.js-Instanzen anzulegen und durch einen zentralen Router gesteuert bei Bedarf aufzurufen. Dieses Szenario habe ich einmal mithilfe von Backbone.js nachgestellt. Die “Web-App” besteht aus diversen Einzelseiten, welche bestimmte jQuery-UI-Widgets beinhalten. Außerdem gibt es eine Startseite mit einem kurzen Erklärungstext. Ziel ist es, den Startseiteninhalt und die jQuery-UI-Widgets nur bei Bedarf zu laden.

Dafür wird beim Aufruf der Seite lediglich eine fundamentale Require.js-Instanz aufgerufen, die die global benötigten Skripte – jQuery, Underscore, Backbone, den jQuery-UI-Core und einen Observer – laden und einen Backbone-Router initialisieren, der die Autoloader-Funktionalität zur Verfügung stellt.

JavaScript

require(['jQuery', 'Underscore', 'Backbone', 'Observer'], function ($, _, Backbone, Observer) {

	var Router = Backbone.Router.extend({

		routes : {
			':site/*sub' : 'autoLoad'
		},

		initialize : function () {
			location.hash = location.hash || 'index/';

			Backbone.history.start();
		},

		autoLoad : function (site, sub) {
			var path = 'sites/' + site + '/app';

			require([path], function (App) {
				App.init({
					subPages : sub
				});
			});
		}
	}),

	router = new Router();

});

Der Router prüft als erstes, ob ein Location-Hash gesetzt ist. Ist dies nicht der Fall, wird der Hash “/index/” gesetzt, was die Initialisierung der Startseite einleitet. Weiterhin wartet der Router auf Änderungen des Location-Hash und führt im Bedarfsfall die “autoLoad”-Funktion aus, welche das gewünschte Modul/die gewünschte Seite lädt. Das “*sub” im “routes”-String des Routers stellt sicher, dass auch beim initialen Aufruf von Unter-Unterseiten die Autoload-Funktion tut, was sie soll.

Das vom Autoloader geladene Skript übergibt eine “init”-Funktion – dabei kann es sich, je nachdem ob die Seite lediglich statischen Inhalt hat oder tiefergreifende Funktionalität besitzt, entweder um eine Backbone-View- oder eine Backbone-Router-Instanz handeln.

JavaScript

var init = function () {
		return new View();
	};

return {
	init : init
};

JavaScript

var init = function (args) {
		return new Router(args);
	};

return {
	init : init
};

Das Datepicker-Modul habe ich als Beispiel für eine zusätzliche Router-Instanz gewählt. Inhaltlich zugegebenermaßen etwas mau, wird doch deutlich, dass auf diese Weise die Funktionalität der Web-App auch auf Unterebenen gewährleistet werden kann, ohne dass dies beim initialen Aufruf der Seite angemeldet bzw. geladen werden muss.

Beobachtet man den Inhalt des HTML-Head beim Durchklicken der Seite im Firebug bzw. Web Inspector, kann man sehr schön verfolgen, wie die jeweils benötigten Module und Widgets sukzessive nachgeladen werden.

Ich hoffe, mein Versuch, den Sachverhalt hier darzustellen, ist einigermaßen verständlich. Über Anregungen, Anmerkungen und Fragen freue ich mich immer. Sollte diesbezüglich Bedarf bestehen, kann ich nur dazu ermutigen, regen Gebrauch von der Kommentar-Funktion zu machen.

Außerdem sei darauf hingewiesen, dass der Code zur schnellen Inspektion auch auf GitHub zur Verfügung steht.

Clipboard – Digitales Notizbrett mit Node.js und Backbone.js

Um etwas Zerstreuung vom Uni-Lernstress zu bekommen, habe ich einen Ausflug in die bunte Node.js-Welt unternommen. Als Mittel zum Zweck diente mir dabei das digitale Notizbrett “Clipboard”. Serverseitig kommt das Application-Framework Express zum Einsatz. Gespeichert werden die Daten in einer MongoDB-Datenbank mithilfe von Mongoose.

Client-seitig kommt die bewährte Kombo aus Underscore, Backbone und jQuery zum Einsatz. Für Ordnung sorgt überdies Require.js, wobei ich auf folgenden Ansatz zurückgegriffen habe: Modular JavaScript & Backbone.js. Außerdem hat jQueryUI einen “Gast-Auftritt” und sorgt für die Draggability der Notizen.

Das Hosting übernimmt dankenswerterweise AppFog, die Daten werden bei MongoHQ abgelegt. Defenitiv zwei sehr praktische Services.

(Clipboard-Code bei GitHub)

Backbone.js – Logo

Wer hier gelegentlich mitliest, wird eventuell mitbekommen haben, dass ich mich gerne mit dem JavaScript-MVC-Framework Backbone.js beschäftige. Von daher war meine Freude recht groß, als ich gewahr wurde, dass Addy Osmani höchstselbst sich dran gemacht hat, eine unter CC-Lizenz stehende Wissenssammlung zum Thema unter dem Namen “Backbone Fundamentals” zu scheiben.

Diese behandelt die Basis-Themen und gibt einen kurzen Einblick in das Thema Backbone.js. Des weiteren wird die Entwicklung einer “RESTful Application” mithilfe von Node.js, Express, Mongoose und MongoDB beschrieben. Es folgt eine Einführung in das Thema “Modular JavaScript” im Kontext von Backbone.js, abgerundet durch konkrete Anwendungsszenarien. Anschließend wird das nicht uninteressante Thema Backbone.js in Kombination mit jQuery Mobile behandelt. Und zum Schluss steht Unit Testing mit dem Test-Framework Jasmine auf der Agenda.

Alle Themen werden anhand der Backbone-Todo-Application erläutert, so dass der praktische Einsatz der vorgestellten Techniken direkt ersichtlich wird. Meinem Empfinden nach entsteht mit “Backbone Fundamentals” eine sehr wertvolle, breit gefächerte Ressource, was das Thema Backbone.js anbelangt. Vielen Dank deshalb an Addy Osmani und allen weiteren, die mitgeholfen haben.

Zum Schluss möchte ich jedoch auch anmerken, dass es sich bei “Backbone Fundamentals” nicht gänzlich um Einstiegslektüre handelt. Grundsätzlich sollte man ein Verständnis von JavaScript, DOM-Libraries im allgemeinen und Application Development in Ansätzen mitbringen, um sich der Lektüre der Basics zu widmen. Bei den weiterführenden Themen ist ein Grundverständnis der verwandten Frameworks und Techniken, die im Kontext von Backbone.js beleuchtet werden, sicherlich auch hilfreich.

Obwohl meiner Meinung nach auch nichts dagegen spricht, bspw. das Kapitel “Modular JavaScript” zum Anlass zu nehmen, sich endlich einmal mit der Materie auseinander zu setzen. Allerdings wird man in diesem Fall wohl nicht umhin kommen, begleitende Lektüre zu konsultieren.

"Backboned" · AJAX-powered WordPress-Theme · Screenshot

Um mir gelegentlich etwas Zerstreuung vom Lernen zu geben, habe ich einen lange gehegten Plan in die Tat umgesetzt: Ein AJAX-betriebenes WordPress-Theme mit Backbone.js zu bauen. Und zwar keine auf “Graceful Degradation” setzende Kompromisslösung. Alle Inhalte werden asynchron geladen und sind per Hashbang URIs ansteuerbar. Ein Blick in den Quellcode offenbart, was ich meine: Ein JSON-Objekt mit allen grundlegenden Daten, eine Handvoll jQuery-Templates und das HTML-Grundgerüst. Das war es an statischem Content - die Darstellung des Inhalts geschieht über Backbone.js.

Damit Suchmaschinen nicht außen vor bleiben und man sich nicht die Mühe machen muss, einen “Headless Browser” à la HtmlUnit auf seinem Server zum laufen bringen zu müssen, werden grundsätzlich alle Inhalte als GET-Anfrage mit dem Parameter “_escaped_fragment_” abgehandelt - die Ausgabe variiert dann je nachdem zwischen statischem HTML oder einem nackten JSON-Objekt. So ist sichergestellt, dass die Inhalte trotzdem indiziert werden können. Die einzigen, die in die Röhre schauen, sind Besucher ohne JavaScript.

Um diesen Workaround zu realisieren, war jedoch ein hohes Maß an Improvisation vonnöten. Mit WordPress-Bordmitteln habe ich es nicht geschafft, das Frontend-seitige URL-Routing von Backbone.js server-seitig abzubilden und entsprechend zu bearbeiten. Ich habe deshalb auf ein simples MVC-Pattern zurück gegriffen und in das eigentliche Theme eine Art Child-Theme integriert. Das ist insgesamt kein Ansatz der mir - besonders in meiner Umsetzung - gefällt. Des weiteren muss man für einen störungsfreien Betrieb des Themes das URL-Rewriting in den WordPress-Einstellungen deaktivieren.

Darüber hinaus bleibt anzumerken, dass das Theme insgesamt eher rudimentär ist. Ich würde von einem Produktiveinsatz abraten. Allerdings bin ich grundsätzlich von der Idee des Themes überzeugt und freue mich natürlich, wenn jemand sich ebenfalls dafür begeistern kann und daran weiterarbeitet. Gerne auch in Kollaboration mit mir. Zu tun gibt es unter anderem noch:

  1. die grundsätzliche Verbesserung des PHP-Codes (sicherer machen, besser in das WordPress-Environment integrieren,…)
  2. den Funktionsumfang erhöhen (Neueste Kommentare, Tags, Suchfunktion, Sidebar-Widgets(?),…)
  3. das JavaScript straffen (Performance, geschmeidigere GUI-Abläufe,…)

…um ein paar Aspekte zu nennen.

Ansonsten freue ich mich wie immer über Anregungen und Verbesserungsvorschläge - gerade bei einem Vorhaben dieser Größenordnung hat man als Entwickler nicht wirklich einen umfassenden Überblick.

PS: Wahrscheinlich werde ich das Teil zeitnah bei GitHub reinladen. Muss mich da aber erst noch anschlauen.

Update: Okay, die Geschichte ist jetzt auch auf GitHub - https://github.com/herschel666/Backboned. Viel Spaß.

Backbone.js ist ein interessantes JavaScript-MVC, mit welchem ich mich seit etwa zwei Wochen beschäftige. Und nun möchte ich ein kleines Tutorial dazu präsentieren - wir bauen uns ein Merkliste. Das Konzept sieht wie folgt aus: Ständig laufen einem tolle Filme, Bücher und Spiele über den Weg, die man unbedingt noch sehen/lesen/spielen möchte, aber man merkt sie sich nie. Das ist der Punkt, wo die Merkliste ins Spiel kommt.

Bei der Programmierung habe ich mich stark an der Todo List Application orientiert, allerdings ist die Merkliste vom Funktionsumfang her schmaler, beinhaltet dafür aber einen Controller für das URL-Routing.

Aber nun zur Sache:

Als erstes benötigen wir den HTML-Teil.

HTML

<!doctype html>
<html lang="de">
<head>
	<meta charset="utf-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
	
	<title>Backbone.js-Tutorial - Merkliste</title>	
	<link rel="stylesheet" href="style.css" />
	
</head>

<body>

	<ul id="nav"></ul>
	<input type="text" placeholder="Gib einen Titel ein&hellip;" id="list_input" />
	<ul id="list"></ul>
	
	<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script>
	<script src="http://ajax.cdnjs.com/ajax/libs/underscore.js/1.1.6/underscore-min.js"></script>
	<script src="http://ajax.cdnjs.com/ajax/libs/backbone.js/0.3.3/backbone-min.js"></script>
	<script src="js/backbone-localstorage.js"></script>
	<script src="js/list.min.js"></script>
	
	<script type="text/template" id="list-item-template">
		<strong><%= title %></strong>
		<span class="delete_item">x</span>
	</script>
	
	<script type="text/template" id="nav-template">
		<a href="#/category/<%= title %>"><%= title %></a>
	</script>
	
</body>
</html>

Für die Merklisten-App benötigen wir jQuery, Underscore.js, Backbone.js und die Backbone-Erweiterung Local-Storage, damit die Einträge im Browser gespeichert werden können. Des weiteren werden die Container für die Navigation und die Listeneinträge, sowie das Eingabefeld angelegt. Schlussendlich brauchen wir noch zwei Templates - eins für die Navigation, eins für die Liste.

Kommen wir nun zum JavaScript-Teil. Als erstes benötigen wir hier unser Model:

JavaScript

window.List = Backbone.Model.extend();

Als nächstes erstellen wir zwei Collections, eine für die Navigation, eine für die Listeneinträge:

JavaScript

window.NavCollection = Backbone.Collection.extend({
	model : List
});
	
window.ListCollection = Backbone.Collection.extend({				
		
	model : List,				
		
	localStorage : new Store('List'),
		
	getByCategory : function ( category )
	{
		return this.filter( function (item)
		{
			return item.get('category') == category;
		});
	}
});

In der Collection für die Listen-Einträge wird der Local Storage angemeldet und eine Funktion eingefügt, die es ermöglicht, die Einträge der Collection nach ihrer Kategorie zu filtern.

Kommen wir nun zum Controller der App:

JavaScript

window.ListController = Backbone.Controller.extend({
		
	_navModel : new NavCollection([
		{title : 'Filme'},
		{title : 'Buecher'},
		{title : 'Spiele'}
	]),
	_navViews : [],
	_categoryModel : new ListCollection,				
	_inputView : null,
		
	routes : {
		'' : 'init',
		'/category/:category' : 'getItems',
	},
		
	initialize : function ()
	{			
		this._navModel.each( function ( item, i )
		{
			this._navViews[i] = new NavigationView({
				model : item
			});
		}, this);
			
		Backbone.history.start();
	},
		
	init : function ()
	{
		window.location.hash = '/category/Filme';
	},
	
	getItems : function ( category )
	{			
		for ( view in this._navViews )
		{
			this._navViews[view]
				.render()
				.setClass();
		};
							
		if ( this._inputView == null )
		{		
			this._inputView = new ListInputView({
				model : this._categoryModel,
				category : category
			});
		}
		else
		{
			this._inputView.options.category = category;
			this._inputView.model.trigger('refresh');
		}
	}
		
});

Als erstes werden - jeweils für die Navigation und die Liste - neue Instanzen der zugehörigen Collection erstellt und Platzhalter für die jeweiligen Views angemeldet. Danach werden die relevanten Pfade mit Funktionen verknüpft. In diesem Fall wird die Funktion init() ausgeführt, wenn kein Hash vorhanden ist, und die Funktion getItems(), wenn ein Kategorie-Hash vorhanden ist.

Als nächstes folgt die initialize-Funktion, welche als erstes beim Aufrufen des Controllers ausgeführt wird. Dabei wird das Navigation-Model mit den nötigen Einträgen versehen, danach für jeden Eintrag der Navigation-Collection eine View-Instanz erstellt und im _navView-Array gespeichert, sowie die Backbone.history-Funktion gestartet.

Als nächstes wird die init-Funktion definiert. Diese sorgt einfach nur dafür, dass die Kategorie "Filme" gewählt wird, indem der entsprechende Kategorie-Hash gesetzt wird. Die Kategorie ist hierbei willkürlich von mir gewählt.

Danach folgt die Definition der getItems-Funktion. In dieser werden als erstes die Views für die Navigationspunkte ge-rendert. Daraufhin folgt entweder die Initialisierung des Views für die Listeneinträge, oder - falls dies schon geschehen ist - das Überschreiben der aktuellen, dem View übergebenen Kategorie und das Neu-Aufbauen der Liste mit den entsprechenden Einträgen. Dabei kommt die Filter-Funktion der Listen-Collection zum Einsatz.

Nachdem nun Model, Collection und Controller vorhanden sind, geht es an die Views. Davon benötigen wir drei - einen für das Eingabefeld, einen für die Liste und einen für die Navigation. Fangen wir mit dem Eingabefeld an:

JavaScript

window.ListInputView = Backbone.View.extend({
	
	el : $('#list_input'),
	
	list : $('#list'),
	
	events : {
		'keypress' : 'createListItem'
	},
	
	initialize : function ()
	{
		_.bindAll(this, 'addListItem', 'addAllListItems');
		
		this.model.bind('add', this.addListItem);
		this.model.bind('refresh', this.addAllListItems);
		
		this.model.fetch();
	},
	
	createListItem : function (e)
	{
		if ( e.keyCode == 13 )
		{			
			this.model.create({
				category : this.options.category,
				title : this.el.val()
			});
			
			this.el.val('');
			this.el.blur();
		}
	},
	
	addListItem : function ( item )
	{					
		var view = new ListItemView({model : item});
		
		!view.model.length && this.list.append( view.render().el );
	},
	
	addAllListItems : function ()
	{
		this.list.empty();
		
		_.each(this.model.getByCategory(this.options.category), function(item)
		{
			this.addListItem(item);
		}, this);
	}
	
});

Der ListInputView nimmt die Eingabe entgegen, erstellt eine neue Instanz des Listen-Eintrag-View und fügt füllt die Liste mit Einträgen. Die Funktion createListItem() erstellt einen neuen Eintrag in der Collection, wenn das Eingabefeld abgefeuert wurde. Über die Angabe this.model.bind('add', this.addListItem); in der initialize-Funktion wird gesorgt, dass daraufhin die Funktion addListItem() aufgerufen wird, die für den neuen Eintrag in der Collection eine Instant des Listen-Eintrag-View erstellt und diesem den Befehl render() mit auf den Weg gibt. Über die Funktion addAllListItems() wird einerseits sichergestellt, dass die Liste geleert wird, bevor neue Einträge nach einem Kategorie-Wechsel reingladen werden, und andererseits, bei Übergabe einer Collection mit mehreren einträgen, für jeden Eintrag die Funktion addListItem() ausgeführt wird.

Als nächstes kommen wir zum Listen-Eintrag-View:

JavaScript

window.ListItemView = Backbone.View.extend({
		
	tagName : 'li',
		
	className : 'list_item',
		
	tmpl : _.template( $('#list-item-template').html() ),
		
	events : {
		'click .delete_item' : 'removeItem'
	},
		
	render : function ()
	{		
		$(this.el).html( this.tmpl( this.model.toJSON() ));
			
		return this;
	},
		
	removeItem : function ()
	{
		this.model.destroy();
		$(this.el).fadeOut( function()
		{
			$(this).remove();
		});
	}
		
});

Hier wird das Element vom Standard (div) auf li gesetzt, die gewünschte CSS-Klasse gesetzt, das Template für den Eintrag angemeldet, ein Klick-Event mit der Funktion removeItem() verknüpft und anschließend die Funktionen render() und removeItem() definiert. Erstere fügt die Daten des Collection-Eintrags in das Template ein, letztere löscht den View und den dazugehörigen Collection-Eintrag.

Schlussendlich benötigen wir noch einen View für die Navigation:

JavaScript

window.NavigationView = Backbone.View.extend({
		
	tagName : 'li',
		
	tmpl : _.template( $('#nav-template').html() ),
		
	hash : function ()
	{
		return window.location.hash.replace('#/category/', '');
	},
		
	render : function ()
	{							
		$('#nav').append($(this.el).html( this.tmpl( this.model.toJSON() ) ));
			
		return this;
	},
		
	setClass : function ()
	{
		var curHash = this.hash();
			
		this.el.className = ( curHash == $(this.el).find('a').text() ) ? 'current' : '';
	}
		
});

Neu und interessant ist hier die Funktion setClass(). Diese sorgt nach dem Rendern der Navigation, dass der aktuelle Reiter die Klasse "current" bekommt.

Um die Listen-App nun zum Laufen zu bringen, erstellen wir eine neue Instanz des Controllers:

JavaScript

var listApp = new ListController();

Der ganze Spaß wird in eine anonyme jQuery-Funktion geschrieben, damit die App gestartet wird, sobald das DOM geladen ist.

Das war es auch schon. Ich hoffe, meine Erklärungen sind einigermaßen nachvollziehbar. Falls nicht, nutzt auf jeden Fall die Kommentar-Funktion. Des weiteren würde ich mich natürlich über Anregungen und Verbesserungsvorschläge freuen, da ich ja doch ein Neuling in Sachen Backbone.js bin. Ansonsten noch der Hinweis, dass bei Gefallen natürlich gerne regen Gebrauch von den unten stehenden Social-Media-Buttons gemacht werden kann.

Vielen Dank!