Bei der ersten Phonegap App war mir nicht einmal der Unterschied zwischen Phonegap und Cordova klar. Javascript war für mich ein untypisiertes Übel, welches in der Web Programmierung einfach gemacht werden musste. Doch für eine Phonegap App wollte ich eine Organisation, eine Architektur. Während man zig Code Beispiele findet, scheint sich niemand Gedanken zur Code Struktur zu machen.
Ja schreiben denn alle nur Spaghetti-Code?
Javascript Module Pattern
Zwischenzeitlich habe ich meine Meinung zu Javascript revidiert. Natürlich hat die Sprache durch die fehlende Typisierung Schwächen. Aber es gibt gerade mit JQuery zusammen tolle Features. Am meisten beeindruckt hat mich der einfache Umgang mit Callbacks mit Deferreds. Doch davon später.
Auf der Suche nach einer Code Organisation bin ich auf das Module Pattern gestossen. Als alter Oracler erinnerte mich das an PL/SQL Packages. Mit dem Module Pattern können Public und Private Members definiert werden. Man hat „etwas State“ (Variablen, welche über die ganze Laufzeit hinweg erhalten bleiben) und einen recht guten „Separation of Concerns“.
Weil Javascript nicht in die HTML Seiten gehört, war klar, dass es einen Javascript Module Layer für die Pages braucht. Dabei bekommt jede HTML Page ein eigenes Module. Da diese Page Module UI orientiert sind, entspricht das grob der View im MVC Pattern. Konsequenterweise wird der Controller ebenfalls mit Javascript Modulen aufgebaut und wird vom Code der Page Module getrennt.
In diesem Beispiel gibt es 2 HTML Pages (index.html und mitglieder.html) und somit 2 Javascript „Page“-Module (indexPage und mitgliederPage). Um es nochmals klar zu machen: In diesen Page Modulen gibt es nur Code zu UI und DOM Manipulation. Keine Security, keine Datenzugriff usw.
Page Module
Nun stellt sich natürlich sofort die Frage, wie werden die Page Module organisiert? Da wir im ersten Projekt mit JQuery und JQueryMobile arbeiteten, mussten wir die dafür benötigten Events abbilden können. JQuery Mobile braucht im Wesentlichen die Events pageinit und pageshow. „Init“ für ein Module ist eine gute Sache. Bei Oracle Packages würde das dem Autostart Block, in der Objekt Orientierung dem Konstruktor entsprechen. Also haben alle Module mindestens eine Methode „Init“ (bei uns initialize genannt).
Die index.html muss dabei den Link auf das indexPage Module machen. Dieses initialisiert dann den Rest der Applikation und ruft für alle Module die initialize-Methode auf.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
<html> <head> <meta charset="utf-8" /> <meta name="format-detection" content="telephone=no"/> <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height, target-densitydpi=device-dpi" /> <link rel="stylesheet" type="text/css" href="css/projekt.min.css"/> <link rel="stylesheet" type="text/css" href="css/jquery.mobile.structure-1.3.2.min.css"/> <link rel="stylesheet" type="text/css" href="css/index.css" /> <link rel="stylesheet" type="text/css" href="css/jquery.mobile.alphascroll.css"/> <script type="text/javascript" src="js/lib/jquery-1.10.2.min.js"></script> <script type="text/javascript" src="js/helper.js"></script> <script type="text/javascript" src="js/DAL.js"></script> <script type="text/javascript" src="js/pages.js"></script> <script type="text/javascript"> indexPage.initialize(); </script> <!-- important: load jquery mobile after indexPage.initialize --> <script type="text/javascript" src="js/lib/jquery.mobile-1.3.2.min.js" ></script> <script type="text/javascript" src="js/lib/jquery.mobile.alphascroll.js"></script> <script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=true"></script> <script type="text/javascript" src="js/lib/jquery.ui.map.js"></script> <script type="text/javascript" src="js/lib/jquery.quickflip.min.js"></script> <script type="text/javascript" src="cordova.js"></script> <title>Apptitle</title> </head> <body> <div data-role="page" id="indexPage"> <div data-role="header"> <h1>Willkommen</h1> </div> <div data-role="content"> <div id="indexContainer" class="ausblenden"> <div class="info"> |
Nachdem index.html dein Einstieg in indexPage.initialize gemacht hat, werden dort die nötigen Events gebunden. Für Web Applikationen ist das onDoucmentReady, für JQueryMobile onMobileInit und für Cordova Apps onDeviceReady.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
var indexPage = (function($, appConfig, appHelper, dataManager, sqlManager) { function initialize() { bindDocumentEvents(); bindPageEvents(); } function bindDocumentEvents() { $(document).on("ready", onDocumentReady); $(document).on("mobileinit", onMobileInit); $(document).on("deviceready", onDeviceReady); } function bindPageEvents() { $(document).on("pageinit", "#mitgliederPage", mitgliederPage.initialize); $(document).on("pageshow", "#mitgliederPage", mitgliederPage.show); } function onDocumentReady() { appConfig.initialize(); appHelper.initialize(); dataManager.initialize(); } function onMobileInit() { $.mobile.phonegapNavigationEnabled = true; $.mobile.page.prototype.options.backBtnText = "Zurück"; } function onDeviceReady() { if (!window.device) { appHelper.showMessage("Phonegap Device Plugin ist nicht installiert. Bitte App neu installieren.", "Fehler"); } if (!window.openDatabase) { appHelper.showMessage("Phonegap Web SQL Plugin ist nicht installiert. Bitte App neu installieren.", "Fehler"); } // local device managers sqlManager.initialize(); } // public API return { initialize: initialize }; |
Weiters werden die Events für alle anderen Page Module gebunden.
Im Beispiel wird der JQueryMobile Event pageinit an mitgliederPage.initialize und der Event pageshow an die Funktion mitgliederPage.show gebunden. Das heisst, sobald JQueryMobile die Seite zur MitgliederPage wechselt, wird einmalig der Code von mitgliederPage.initialize ausgeführt. Dort passiert typischerweise das Bindung der Events für diese Page.
Wann immer JQueryMobile den pageshow Event für die Mitglieder Seite aufruft, wird unser Code in mitgliederPage.show ausgeführt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
var indexPage = (function($, appConfig, appHelper, dataManager, sqlManager) { function initialize() { bindDocumentEvents(); bindPageEvents(); } function bindDocumentEvents() { $(document).on("ready", onDocumentReady); $(document).on("mobileinit", onMobileInit); $(document).on("deviceready", onDeviceReady); } function bindPageEvents() { $(document).on("pageinit", "#mitgliederPage", mitgliederPage.initialize); $(document).on("pageshow", "#mitgliederPage", mitgliederPage.show); } function onDocumentReady() { appConfig.initialize(); appHelper.initialize(); dataManager.initialize(); } function onMobileInit() { $.mobile.phonegapNavigationEnabled = true; $.mobile.page.prototype.options.backBtnText = "Zurück"; } function onDeviceReady() { if (!window.device) { appHelper.showMessage("Phonegap Device Plugin ist nicht installiert. Bitte App neu installieren.", "Fehler"); } if (!window.openDatabase) { appHelper.showMessage("Phonegap Web SQL Plugin ist nicht installiert. Bitte App neu installieren.", "Fehler"); } // local device managers sqlManager.initialize(); } // public API return { initialize: initialize }; })(jQuery, appConfig, appHelper, dataManager); var mitgliederPage = (function($, appConfig, appHelper) { function initialize() { $("#mitgliedPicPopup").on({popupbeforeposition: function() { var maxHeight = $(window).height() - 60 + "px"; $("#mitgliedPicPopup img").css("max-height", maxHeight); } }); } function show() { var getMitgliedId = appHelper.getUrlVars()["id"]; var dataManagerPromise = $.when(dataManager.getMitgliedById(getMitgliedId)); dataManagerPromise.done(dataBindMitgliedData); dataManagerPromise.fail(function(response) { appHelper.showMessage(response, "Fehler"); }); } function dataBindMitgliedData(mitgliedDaten, bilderURL) { mitglied = mitgliedDaten; $('#mitgliedPic').attr('src', bilderURL + mitglied.picture); $('#mitgliedPicBig').attr('src', bilderURL + mitglied.picture); $('#fullName').text(mitglied.nachname + ' ' + mitglied.vorname); } // public API return { initialize: initialize, show: show }; })(jQuery, appConfig, appHelper, dataManager); |
Damit wird der Code zwingendermassen klar strukturiert. Alle Bindings kommen automatisch zusammen und es wird sehr übersichtlich, welche Events gebunden werden. Ebenfalls wird klar, wo eine Funktion hingehört. Eine Funktion zur Anzeige von Mitgliederdaten kann nur im Module mitgliederPage sein und dort (zusammen mit JQueryMobile) nur in der show Funktion.
Nicht UI Module
Konsequenterweise werden die Aufgaben wie Datenzugriff vom UI Layer getrennt und ebenfalls in Javascript Modulen organisiert. Im Beispiel ist ersichtlich, dass für den Datenzugriff ein dataManager Module verwendet wird. Der dataManager besitzt die Logik, um zwischen lokalen Cordova Datenzugriffen und Datenzugriffen über JSON auf dem Server zu unterscheiden. Der lokale Datenzugriff ist im Module sqlManager enthalten. Dies ist aber für das Beispiel nicht mehr relevant.
Wie alle Module enthalten auch der dataManager und der sqlManager eine initialize Methode. Diese werden bei der Initialisierung der Applikation aufgerufen. Der dataManager ist von Cordova unabhängig und kann im onDocumentReady Event initialisiert werden. Der sqlManager nutzt die Cordova Datenzugriffe und wird deshalb erst im onDeviceReady Event initialisiert.
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
function onDocumentReady() { appConfig.initialize(); appHelper.initialize(); dataManager.initialize(); } function onMobileInit() { $.mobile.phonegapNavigationEnabled = true; $.mobile.page.prototype.options.backBtnText = "Zurück"; } function onDeviceReady() { if (!window.device) { appHelper.showMessage("Phonegap Device Plugin ist nicht installiert. Bitte App neu installieren.", "Fehler"); } if (!window.openDatabase) { appHelper.showMessage("Phonegap Web SQL Plugin ist nicht installiert. Bitte App neu installieren.", "Fehler"); } // local device managers sqlManager.initialize(); } |
Alle Aufrufe zwischen den Layern werden mit den Javascript Callback Mechanismen aufgerufen. Diese ist teilweise, z.B. bei JQuery getJSON, automatisch enthalten. Der eigene Code wird nach dem selben Prinzip programmiert. Das ergibt einerseits ein UI, welches ständig reagiert kann, weil es durch keine synchronen Calls blockiert bleibt. Anderseits erhält man so ein einfaches, sich wiederholendes Muster, welches von allen Team-Mitgliedern verstanden wird.
56 57 58 59 60 61 62 |
function show() { var getMitgliedId = appHelper.getUrlVars()["id"]; var dataManagerPromise = $.when(dataManager.getMitgliedById(getMitgliedId)); dataManagerPromise.done(dataBindMitgliedData); dataManagerPromise.fail(function(response) { appHelper.showMessage(response, "Fehler"); }); |
Fazit
Die vorgestellte Code Organisation hat sich in der Praxis bewährt. Javascript Module sind aus meiner Sicht ein Muss und die Trennung der Module in UI und Nicht-UI Komponenten ist für Cordova Projekte hilfreich. Die gleiche Architektur kann aber für jede normale Web Applikation genutzt werden.