wake-up-neo.com

So erstellen Sie eine Webanwendung mit jquery-mobile und knockoutjs

Ich möchte eine mobile App bauen, die nur aus HTML/CSS und JavaScript besteht. Ich habe zwar gute Kenntnisse über das Erstellen einer Web-App mit JavaScript, aber ich dachte, ich könnte mir ein Framework wie jquery-mobile ansehen.

Zuerst dachte ich, dass jquery-mobile nichts anderes ist 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 ausgeliefert und Sie können Apps mit einer deklarativen HTML-Syntax erstellen. Für die denkbar einfachste App müsstest du also nicht eine einzige Zeile JavaScript selbst schreiben (was cool ist, weil wir alle gerne weniger arbeiten, oder?)

Ich halte es für eine gute Idee, jquery-mobile mit knockoutjs zu kombinieren, um die Erstellung von Apps mithilfe einer deklarativen HTML-Syntax zu unterstützen. Knockoutjs ist ein clientseitiges MVVM-Framework, das darauf abzielt, die aus WPF/Silverlight bekannten 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 aufgebaut wird. Meine Idee war es, den Ansatz aufzuschreiben, den ich mir nach mehreren Stunden angeschaut hatte, und ein paar Kommentare zu jquery-mobile/knockout yoda zu machen, um mir zu zeigen, warum es scheiße ist und warum ich nicht gleich programmieren sollte Ort ;-)

Das HTML

jquery-mobile leistet gute Arbeit bei der Bereitstellung eines grundlegenden Seitenstrukturmodells. Mir ist zwar klar, dass ich meine Seiten später über Ajax laden lassen kann, aber ich habe mich dafür entschieden, 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 Schichten der App nachzudenken, hatte ich mehrere Dinge im Sinn (z. B. Testbarkeit, lose Kopplung). Ich werde Ihnen zeigen, wie ich mich dazu entschlossen habe, meine Dateien aufzuteilen und Dinge zu kommentieren, wie zum Beispiel, warum ich während meines Aufenthalts eine Sache einer anderen vorgezogen habe ...

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 stellt einen Namespace für die Ansichtsmodelle bereit (in Kürze). Es wartet auf das Ereignis mobileinit, das jquery-mobile bereitstellt.

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

Ich schließe auch das Ereignis pagecreate für die Homepage an, in der ich eine Instanz des viewModel erstelle, das die übergebene Dienstinstanz erhält. Dieser Punkt ist für mich von wesentlicher Bedeutung. Wenn jemand denkt, sollte dies anders gemacht werden, bitte teilen Sie 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 erfährt. In unserem Fall übergebe ich zum Beispiel nur einen verspotteten Ajax-Service, weil 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 trennen, 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 mit einer objektliteralen Syntax finden, verwende ich die traditionelle Funktionssyntax mit einem 'Selbst'-Hilfsobjekt. Grundsätzlich ist es Geschmackssache. Wenn Sie jedoch eine beobachtbare Eigenschaft haben möchten, um auf eine andere zu verweisen, 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 als Parameter weitergeben kann, wie ich zuvor erwähnt habe.

Es gibt noch eine Sache mit diesem Ansichtsmodell, 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. Also habe ich mich entschieden, startServicePolling/ stopServicePolling Methoden zu implementieren, um dies zu tun. Die Idee ist, das Polling auf Pageshow zu starten und zu stoppen, wenn der Benutzer zu einer anderen Seite navigiert.

Sie können die Syntax, mit der der Dienst abgefragt wird, ignorieren. Es ist RxJS-Magie. Stelle nur sicher, dass ich es abrufe und aktualisiere die beobachtbaren Eigenschaften mit dem zurückgegebenen Ergebnis, wie im Teil Subscribe (function (statistics) {..}) zu sehen ist.

App.MockedStatisticsService.js

Ok, es gibt nur noch eine Sache, die wir Ihnen zeigen können. Es ist die eigentliche Service-Implementierung. Ich gehe hier nicht viel ins Detail. Es ist nur ein Mock, der einige Zahlen zurückgibt, wenn getStatistics aufgerufen wird. Es gibt eine andere Methode mockStatistics, mit der ich neue Werte über die js-Konsole des Browsers festlegen kann, 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 ursprünglich geplant. Mein Finger tut mir weh, meine Hunde bitten mich, mit ihnen spazieren zu gehen, und ich fühle mich erschöpft. Ich bin mir sicher, dass es hier viele Dinge gibt, die fehlen und die ich in eine Reihe von Tipp- und Grammerfehlern gesteckt 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 ob Sie denken, dass es gut oder schlecht ist oder ob ich Dinge verpasse.

[~ # ~] Update [~ # ~]

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!

88
Christoph

Hinweis: Ab jQuery 1.7 ist die Methode .live() veraltet. Verwenden Sie .on() , um Ereignishandler anzuhängen. Benutzer älterer jQuery-Versionen sollten .delegate() anstelle von .live() verwenden.

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

Ansichtsmodell und Seite

Verwenden Sie nur ein (1) Ansichtsmodellobjekt pro jQuery Mobile-Seite. Andernfalls können Probleme mit mehrmals ausgelösten Klickereignissen auftreten.

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 für jede Seite nur einmal auf und verwenden Sie ko.observable's, anstatt ko.applyBinding mehrmals aufzurufen.

pagehide und ko.cleanNode

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

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

seite ausblenden und klicken

Wenn Sie an Click-Events 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');
});
30
finnsson