Architektur einer Webanwendung mit jquery-mobile und knockoutjs

88

Ich möchte eine mobile App erstellen, die nur aus HTML / CSS und JavaScript besteht. Obwohl ich ein gutes Wissen darüber habe, wie man eine Web-App mit JavaScript erstellt, dachte ich, ich könnte einen Blick in ein Framework wie jquery-mobile werfen.

Zuerst dachte ich, jquery-mobile sei nichts anderes als ein Widget-Framework, das auf mobile Browser abzielt. Sehr ähnlich zu jquery-ui, aber für die mobile Welt. Aber mir ist aufgefallen, dass jquery-mobile mehr ist. Es wird mit einer Reihe von Architekturen geliefert, mit denen Sie Apps mit einer deklarativen HTML-Syntax erstellen können. Für die am einfachsten zu denkende App müssten Sie also nicht selbst eine einzige Zeile JavaScript schreiben (was cool ist, weil wir alle gerne weniger arbeiten, oder?)

Um den Ansatz zu unterstützen, Apps mit einer deklarativen HTML-Syntax zu erstellen, ist es meiner Meinung nach eine gute Wahl, jquery-mobile mit knockoutjs zu kombinieren. Knockoutjs ist ein clientseitiges MVVM-Framework, das darauf abzielt, aus WPF / Silverlight bekannte MVVM-Superkräfte in die JavaScript-Welt zu bringen.

Für mich ist MVVM eine neue Welt. Obwohl ich bereits viel darüber gelesen habe, habe ich es selbst noch nie benutzt.

In diesem Beitrag geht es darum, wie eine App mit jquery-mobile und knockoutjs zusammen erstellt wird. Meine Idee war es, den Ansatz aufzuschreiben, den ich mir ausgedacht hatte, nachdem ich ihn einige Stunden lang angeschaut hatte, und ein paar JQuery-Mobile / Knockout-Yoda zu haben, um ihn zu kommentieren, um mir zu zeigen, warum es scheiße ist und warum ich im ersten nicht programmieren sollte Platz ;-)

Das HTML

jquery-mobile leistet gute Arbeit und bietet ein grundlegendes Strukturmodell für Seiten. Obwohl mir klar ist, dass ich meine Seiten später über Ajax laden kann, habe ich mich entschlossen, alle in einer index.html-Datei zu speichern. In diesem Basisszenario handelt es sich um zwei Seiten, damit es nicht zu schwierig wird, den Überblick zu behalten.

<!DOCTYPE html> 
<html> 
  <head> 
  <title>Page Title</title> 
  <link rel="stylesheet" href="libs/jquery-mobile/jquery.mobile-1.0a4.1.css" />
  <link rel="stylesheet" href="app/base/css/base.css" />
  <script src="libs/jquery/jquery-1.5.0.min.js"></script>
  <script src="libs/knockout/knockout-1.2.0.js"></script>
  <script src="libs/knockout/knockout-bindings-jqm.js" type="text/javascript"></script>
  <script src="libs/rx/rx.js" type="text/javascript"></script>
  <script src="app/App.js"></script>
  <script src="app/App.ViewModels.HomeScreenViewModel.js"></script>
  <script src="app/App.MockedStatisticsService.js"></script>
  <script src="libs/jquery-mobile/jquery.mobile-1.0a4.1.js"></script>  
</head> 
<body> 

<!-- Start of first page -->
<div data-role="page" id="home">

    <div data-role="header">
        <h1>Demo App</h1>
    </div><!-- /header -->

    <div data-role="content">   

    <div class="ui-grid-a">
        <div class="ui-block-a">
            <div class="ui-bar" style="height:120px">
                <h1>Tours today (please wait 10 seconds to see the effect)</h1>
                <p><span data-bind="text: toursTotal"></span> total</p>
                <p><span data-bind="text: toursRunning"></span> running</p>
                <p><span data-bind="text: toursCompleted"></span> completed</p>     
            </div>
        </div>
    </div>

    <fieldset class="ui-grid-a">
        <div class="ui-block-a"><button data-bind="click: showTourList, jqmButtonEnabled: toursAvailable" data-theme="a">Tour List</button></div>  
    </fieldset>

    </div><!-- /content -->

    <div data-role="footer" data-position="fixed">
        <h4>by Christoph Burgdorf</h4>
    </div><!-- /header -->
</div><!-- /page -->

<!-- tourlist page -->
<div data-role="page" id="tourlist">

    <div data-role="header">
        <h1>Bar</h1>
    </div><!-- /header -->

    <div data-role="content">   
        <p><a href="#home">Back to home</a></p> 
    </div><!-- /content -->

    <div data-role="footer" data-position="fixed">
        <h4>by Christoph Burgdorf</h4>
    </div><!-- /header -->
</div><!-- /page -->

</body>
</html>

Das JavaScript

Kommen wir also zum lustigen Teil - dem JavaScript!

Als ich anfing, über das Überlagern der App nachzudenken, hatte ich verschiedene Dinge im Sinn (z. B. Testbarkeit, lose Kopplung). Ich werde Ihnen zeigen, wie ich beschlossen habe, meine Dateien aufzuteilen und Dinge zu kommentieren, wie zum Beispiel, warum ich eine Sache einer anderen vorgezogen habe, während ich gehe ...

App.js.

var App = window.App = {};
App.ViewModels = {};

$(document).bind('mobileinit', function(){
    // while app is running use App.Service.mockStatistic({ToursCompleted: 45}); to fake backend data from the console
    var service = App.Service = new App.MockedStatisticService();    

  $('#home').live('pagecreate', function(event, ui){
        var viewModel = new App.ViewModels.HomeScreenViewModel(service);
        ko.applyBindings(viewModel, this);
        viewModel.startServicePolling();
  });
});

App.js ist der Einstiegspunkt meiner App. Es erstellt das App-Objekt und bietet einen Namespace für die Ansichtsmodelle (in Kürze). Es wartet auf das mobileinit- Ereignis, das jquery-mobile bereitstellt.

Wie Sie sehen können, erstelle ich eine Instanz eines Ajax-Dienstes (auf den wir später noch eingehen werden) und speichere ihn in der Variablen "Dienst".

Ich verbinde auch das pagecreate- Ereignis für die Homepage, auf der ich eine Instanz des viewModel erstelle, mit der die Dienstinstanz übergeben wird. Dieser Punkt ist für mich von wesentlicher Bedeutung. Wenn jemand denkt, dass dies anders gemacht werden sollte, teilen Sie bitte Ihre Gedanken!

Der Punkt ist, dass das Ansichtsmodell auf einem Dienst (GetTour /, SaveTour usw.) ausgeführt werden muss. Aber ich möchte nicht, dass das ViewModel mehr darüber weiß. In unserem Fall übergebe ich beispielsweise nur einen verspotteten Ajax-Dienst, da das Backend noch nicht entwickelt wurde.

Eine andere Sache, die ich erwähnen sollte, ist, dass das ViewModel kein Wissen über die tatsächliche Ansicht hat. Aus diesem Grund rufe ich ko.applyBindings (viewModel, this) im pagecreate- Handler auf. Ich wollte das Ansichtsmodell von der tatsächlichen Ansicht getrennt halten, um das Testen zu vereinfachen.

App.ViewModels.HomeScreenViewModel.js

(function(App){
  App.ViewModels.HomeScreenViewModel = function(service){
    var self = {}, disposableServicePoller = Rx.Disposable.Empty;

    self.toursTotal = ko.observable(0);
    self.toursRunning = ko.observable(0);
    self.toursCompleted = ko.observable(0);
    self.toursAvailable = ko.dependentObservable(function(){ return this.toursTotal() > 0; }, self);
    self.showTourList = function(){ $.mobile.changePage('#tourlist', 'pop', false, true); };        
    self.startServicePolling = function(){  
        disposableServicePoller = Rx.Observable
            .Interval(10000)
            .Select(service.getStatistics)
            .Switch()
            .Subscribe(function(statistics){
                self.toursTotal(statistics.ToursTotal);
                self.toursRunning(statistics.ToursRunning); 
                self.toursCompleted(statistics.ToursCompleted); 
            });
    };
    self.stopServicePolling = disposableServicePoller.Dispose;      

    return self; 
  };
})(App)

Während Sie die meisten Beispiele für Knockoutjs-Ansichtsmodelle finden, die eine Objektliteral-Syntax verwenden, verwende ich die traditionelle Funktionssyntax mit einem 'Selbst'-Hilfsobjekt. Grundsätzlich ist es Geschmackssache. Wenn Sie jedoch möchten, dass eine beobachtbare Eigenschaft auf eine andere verweist, können Sie das Objektliteral nicht auf einmal aufschreiben, wodurch es weniger symmetrisch wird. Das ist einer der Gründe, warum ich eine andere Syntax wähle.

Der nächste Grund ist der Dienst, den ich wie bereits erwähnt als Parameter weitergeben kann.

Bei diesem Ansichtsmodell gibt es noch eine Sache, bei der ich nicht sicher bin, ob ich den richtigen Weg gewählt habe. Ich möchte den Ajax-Dienst regelmäßig abfragen, um die Ergebnisse vom Server abzurufen. Daher habe ich mich dafür entschieden, die Methoden startServicePolling / stopServicePolling zu implementieren . Die Idee ist, die Abfrage auf der Seitenshow zu starten und zu stoppen, wenn der Benutzer zu einer anderen Seite navigiert.

Sie können die Syntax ignorieren, mit der der Dienst abgefragt wird. Es ist RxJS Magie. Stellen Sie nur sicher, dass ich es abfrage, und aktualisieren Sie die beobachtbaren Eigenschaften mit dem zurückgegebenen Ergebnis, wie Sie im Teil Abonnieren (Funktion (Statistik) {..}) sehen können .

App.MockedStatisticsService.js

Ok, es gibt nur noch eins zu zeigen. Es ist die eigentliche Service-Implementierung. Ich gehe hier nicht viel ins Detail. Es ist nur ein Mock, der beim Aufruf von getStatistics einige Zahlen zurückgibt . Es gibt eine andere Methode mockStatistics, mit der ich neue Werte über die js-Konsole des Browsers festlege, während die App ausgeführt wird.

(function(App){
    App.MockedStatisticService = function(){
        var self = {},
        defaultStatistic = {
            ToursTotal: 505,
            ToursRunning: 110,
            ToursCompleted: 115 
        },
        currentStatistic = $.extend({}, defaultStatistic);;

        self.mockStatistic = function(statistics){
            currentStatistic = $.extend({}, defaultStatistic, statistics);
        };

        self.getStatistics = function(){        
            var asyncSubject = new Rx.AsyncSubject();
            asyncSubject.OnNext(currentStatistic);
            asyncSubject.OnCompleted();
            return asyncSubject.AsObservable();
        };

        return self;
    };
})(App)

Ok, ich habe viel mehr geschrieben, als ich ursprünglich geplant hatte zu schreiben. Mein Finger tut weh, meine Hunde bitten mich, mit ihnen spazieren zu gehen, und ich fühle mich erschöpft. Ich bin mir sicher, dass hier viele Dinge fehlen und dass ich eine Reihe von Tippfehlern und Grammatikfehlern eingegeben habe. Schreie mich an, wenn etwas nicht klar ist und ich werde das Posting später aktualisieren.

Das Posting scheint keine Frage zu sein, ist es aber tatsächlich! Ich möchte, dass Sie Ihre Gedanken über meinen Ansatz teilen und wenn Sie denken, dass es gut oder schlecht ist oder wenn ich Dinge verpasse.

AKTUALISIEREN

Aufgrund der großen Beliebtheit dieses Beitrags und weil mich mehrere Leute darum gebeten haben, habe ich den Code dieses Beispiels auf github gestellt:

https://github.com/cburgdorf/stackoverflow-knockout-example

Hol es dir, solange es heiß ist!

Christoph
quelle
7
Ich bin mir nicht sicher, ob es eine ausreichend spezifische Frage gibt, die die Leute beantworten können. Ich mag das Detail, das Sie hier haben, aber es scheint sich für eine Diskussion zu eignen. In weniger Worten: "Nice blog";)
Bernhard Hofmann
Ich bin froh, dass es dir gefällt. Ich habe mir ein bisschen Sorgen gemacht, dass ich so viel geschrieben habe, dass die Leute Angst haben, eine kurze Antwort zu schreiben. Jede Diskussion ist jedoch willkommen. Und wenn Stackoverflow der falsche Ort ist, um eine Diskussion zu beginnen, könnten wir zu Google Groups wechseln: groups.google.com/forum/#!topic/knockoutjs/80_FuHmCm1s
Christoph
Hallo Christoph, wie hat sich dieser Ansatz für dich entwickelt?
Hkon
Eigentlich bin ich zu dem großartigeren AngularJS-Framework
Christoph
1
Dies ist möglicherweise besser, wenn Sie nur die ersten Absätze als Frage beibehalten und den Rest auf eine Selbstantwort verschieben.
rjmunro

Antworten:

30

Hinweis: Ab jQuery 1.7 ist die .live()Methode veraltet. Verwenden Sie .on()zum Event - Handler anhängen. Benutzer älterer Versionen von jQuery sollten .delegate()bevorzugt verwenden .live().

Ich arbeite an der gleichen Sache (Knockout + JQuery Mobile). Ich versuche, einen Blog-Beitrag über das zu schreiben, was ich gelernt habe, aber hier sind in der Zwischenzeit einige Hinweise. Denken Sie daran, dass ich auch versuche, Knockout / JQuery Mobile zu lernen.

Ansichtsmodell und Seite

Verwenden Sie nur ein (1) Ansichtsmodellobjekt pro jQuery Mobile-Seite. Andernfalls können Probleme mit Klickereignissen auftreten, die mehrfach ausgelöst werden.

Modell anzeigen und klicken

Verwenden Sie ko.observable-Felder nur für Klickereignisse von Ansichtsmodellen.

ko.applyBinding einmal

Wenn möglich: Rufen Sie ko.applyBinding nur einmal für jede Seite auf und verwenden Sie ko.observables, anstatt ko.applyBinding mehrmals aufzurufen.

pagehide und ko.cleanNode

Denken Sie daran, einige Ansichtsmodelle auf Pagehide zu bereinigen. ko.cleanNode scheint das Rendern von jQuery Mobiles zu stören, wodurch das HTML erneut gerendert wird. Wenn Sie ko.cleanNode auf einer Seite verwenden, müssen Sie Datenrollen entfernen und das gerenderte jQuery Mobile-HTML in den Quellcode einfügen.

$('#field').live('pagehide', function() {
    ko.cleanNode($('#field')[0]);
});

Seitenhaut und klicken

Wenn Sie an Klickereignisse gebunden sind, denken Sie daran, .ui-btn-active zu bereinigen. Der einfachste Weg, dies zu erreichen, ist die Verwendung dieses Code-Snippets:

$('[data-role="page"]').live('pagehide', function() {
    $('.ui-btn-active').removeClass('ui-btn-active');
});
finnsson
quelle
Da meine Frage sehr unspezifisch war und Sie derjenige sind, der die meiste Arbeit in eine Antwort gesteckt hat, werde ich Ihre zur akzeptierten Antwort machen.
Christoph
Hast du das jemals herausgefunden? Ich habe verdammt viel Zeit damit, KO und JQM zu integrieren, und es gibt keine guten Anleitungen dafür (oder eine jsFiddle, die eine End-to-End-Demo demonstriert).
Kamranicus
1
Nein, ich bin zum AngularJS-Framework übergegangen. Ich fand, dass das KO überlegen ist. Und es gibt ein ziemlich gutes Adapterprojekt, um AngularJS / jqm für immer zu besten Freunden zu machen: github.com/tigbro/jquery-mobile-angular-adapter Für das, was ich bisher gemacht habe, schien es jedoch übertrieben, diesen Adapter zu verwenden. Immerhin ist es ziemlich einfach, einfach das HTML / CSS von jqm zu verwenden und die Steuerelemente in eine Angular-Direktive umzuwandeln
Christoph
Sie können eine Struktur erstellen, die ich hier definiert habe . Ich bin sicher, dass Sie auf diese Weise die vollständige Kontrolle über die Anwendung haben.
Muhammad Raheel