Vorrei creare un'app mobile, prodotta da nient'altro che html / css e JavaScript. Anche se ho una discreta conoscenza di come creare un'app Web con JavaScript, ho pensato di poter dare un'occhiata a un framework come jquery-mobile.
All'inizio pensavo che jquery-mobile non fosse altro che un framework di widget che si rivolge ai browser mobili. Molto simile a jquery-ui ma per il mondo mobile. Ma ho notato che jquery-mobile è più di questo. Viene fornito con un sacco di architettura e ti consente di creare app con una sintassi html dichiarativa. Quindi, per l'app più facile pensabile, non avresti bisogno di scrivere una singola riga di JavaScript da solo (il che è interessante, perché a tutti noi piace lavorare di meno, no?)
Per supportare l'approccio alla creazione di app utilizzando una sintassi html dichiarativa, penso che sia una buona idea combinare jquery-mobile con knockoutjs. Knockoutjs è un framework MVVM lato client che mira a portare i super poteri MVVM noti da WPF / Silverlight al mondo JavaScript.
Per me MVVM è un nuovo mondo. Anche se ho già letto molto a riguardo, non l'ho mai usato prima.
Quindi questo post riguarda l'architettura di un'app utilizzando jquery-mobile e knockoutjs insieme. La mia idea era di scrivere l'approccio che mi è venuto in mente dopo averlo guardato per diverse ore e avere un po 'di jquery-mobile / knockout yoda per commentarlo, mostrandomi perché fa schifo e perché non dovrei fare programmazione nel primo posto ;-)
Il file html
jquery-mobile fa un buon lavoro fornendo un modello di struttura di base delle pagine. Sebbene sia ben consapevole che in seguito avrei potuto caricare le mie pagine tramite ajax, ho deciso di tenerle tutte in un file index.html. In questo scenario di base stiamo parlando di due pagine in modo che non dovrebbe essere troppo difficile rimanere in cima alle cose.
<!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>
Il JavaScript
Quindi veniamo alla parte divertente: JavaScript!
Quando ho iniziato a pensare alla stratificazione dell'app, avevo in mente diverse cose (ad es. Testabilità, accoppiamento lento). Ti mostrerò come ho deciso di dividere i miei file e commenterò cose come perché ho scelto una cosa piuttosto che un'altra mentre vado ...
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 è il punto di ingresso della mia app. Crea l'oggetto App e fornisce uno spazio dei nomi per i modelli di visualizzazione (presto disponibili). Ascolta l' evento mobileinit fornito da jquery-mobile.
Come puoi vedere, sto creando un'istanza di una sorta di servizio ajax (che vedremo più avanti) e la salvo nella variabile "service".
Ho anche collegare il pagecreate evento per la home page in cui creo un'istanza del ViewModel che ottiene l'istanza del servizio passata. Questo punto è essenziale per me. Se qualcuno pensa, dovrebbe essere fatto diversamente, per favore condividi i tuoi pensieri!
Il punto è che il modello di visualizzazione deve operare su un servizio (GetTour /, SaveTour ecc.). Ma non voglio che ViewModel ne sappia di più. Quindi, ad esempio, nel nostro caso, sto solo passando un servizio ajax deriso perché il backend non è stato ancora sviluppato.
Un'altra cosa che dovrei menzionare è che ViewModel non ha alcuna conoscenza della vista effettiva. Ecco perché chiamo ko.applyBindings (viewModel, this) dall'interno del gestore pagecreate . Volevo mantenere il modello di visualizzazione separato dalla visualizzazione effettiva per semplificare il test.
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)
Mentre troverai la maggior parte degli esempi di modelli di visualizzazione knockoutj che utilizzano una sintassi letterale dell'oggetto, sto usando la sintassi della funzione tradizionale con oggetti helper "self". Fondamentalmente è una questione di gusti. Ma quando vuoi avere una proprietà osservabile per fare riferimento a un'altra, non puoi scrivere l'oggetto letterale in una volta, il che lo rende meno simmetrico. Questo è uno dei motivi per cui scelgo una sintassi diversa.
Il motivo successivo è il servizio che posso trasmettere come parametro come ho detto prima.
C'è un'altra cosa con questo modello di visualizzazione che non sono sicuro di aver scelto la strada giusta. Voglio interrogare periodicamente il servizio ajax per recuperare i risultati dal server. Quindi, ho scelto di implementare i metodi startServicePolling / stopServicePolling per farlo. L'idea è di avviare il sondaggio su pageshow e interromperlo quando l'utente naviga in una pagina diversa.
È possibile ignorare la sintassi utilizzata per eseguire il polling del servizio. È magia RxJS. Assicurati solo che lo stia interrogando e aggiorni le proprietà osservabili con il risultato restituito, come puoi vedere nella parte Sottoscrivi (funzione (statistiche) {..}) .
App.MockedStatisticsService.js
Ok, c'è solo una cosa da mostrarti. È l'effettiva implementazione del servizio. Non sto entrando molto nei dettagli qui. È solo una simulazione che restituisce alcuni numeri quando viene chiamato getStatistics . Esiste un altro metodo mockStatistics che utilizzo per impostare nuovi valori tramite la console js del browser mentre l'app è in esecuzione.
(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, ho scritto molto di più come inizialmente avevo programmato di scrivere. Mi fa male il dito, i miei cani mi chiedono di portarli a fare una passeggiata e mi sento esausto. Sono sicuro che qui mancano molte cose e che ho inserito un sacco di errori di battitura e grammaticali. Urlami se qualcosa non è chiaro e aggiornerò il messaggio in seguito.
Il post potrebbe non sembrare una domanda ma in realtà lo è! Vorrei che condividessi i tuoi pensieri sul mio approccio e se pensi che sia buono o cattivo o se mi sto perdendo delle cose.
AGGIORNARE
A causa della grande popolarità acquisita da questo post e poiché diverse persone mi hanno chiesto di farlo, ho messo il codice di questo esempio su GitHub:
https://github.com/cburgdorf/stackoverflow-knockout-example
Prendilo finché fa caldo!