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!