Come architettare una webapp usando jquery-mobile e knockoutjs


88

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!


7
Non sono sicuro che ci sia una domanda sufficientemente specifica per le persone a cui rispondere. Mi piace il dettaglio che hai qui, ma sembra che si presti ad essere oggetto di discussione. In poche parole: "Bel blog";)
Bernhard Hofmann

Sono felice ti sia piaciuto. Mi preoccupavo un po 'di aver scritto così tanto che le persone hanno paura di scrivere una risposta breve. Tuttavia, qualsiasi discussione è benvenuta. E se stackoverflow è il posto sbagliato per iniziare una discussione, potremmo passare a google groups: groups.google.com/forum/#!topic/knockoutjs/80_FuHmCm1s
Christoph

Ciao Christoph, come ha funzionato questo approccio per te?
hkon

In realtà, sono passato al più fantastico framework AngularJS ;-)
Christoph

1
Questo potrebbe essere meglio se tenessi solo il primo paio di paragrafi come domanda e spostassi il resto in una risposta automatica.
rjmunro

Risposte:


30

Nota: a partire da jQuery 1.7, il .live()metodo è deprecato. Utilizzare .on()per allegare gestori di eventi. Gli utenti di versioni precedenti di jQuery dovrebbero utilizzare .delegate()di preferenza .live().

Sto lavorando alla stessa cosa (knockout + jquery mobile). Sto cercando di scrivere un post sul blog su ciò che ho imparato, ma nel frattempo ecco alcuni suggerimenti. Ricorda che sto anche cercando di imparare a usare knockout / jquery mobile.

Visualizza modello e pagina

Utilizza solo un (1) oggetto modello di visualizzazione per pagina jQuery Mobile. In caso contrario, potresti riscontrare problemi con eventi di clic attivati ​​più volte.

Visualizza modello e fai clic su

Utilizza solo ko.observable-fields per gli eventi di clic dei modelli di visualizzazione.

ko.applyBinding una volta

Se possibile: chiama ko.applyBinding solo una volta per ogni pagina e usa ko.observable's invece di chiamare ko.applyBinding più volte.

pagehide e ko.cleanNode

Ricorda di ripulire alcuni modelli di visualizzazione su pagehide. ko.cleanNode sembra disturbare il rendering di jQuery Mobiles, provocando un nuovo rendering dell'html. Se usi ko.cleanNode su una pagina devi rimuovere i data-role e inserire l'html jQuery Mobile renderizzato nel codice sorgente.

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

nascondi pagina e fai clic

Se ti stai associando a eventi di clic, ricordati di pulire .ui-btn-active. Il modo più semplice per farlo è utilizzare questo frammento di codice:

$('[data-role="page"]').live('pagehide', function() {
    $('.ui-btn-active').removeClass('ui-btn-active');
});

Poiché la mia domanda era molto aspecifica e tu sei quello che ha messo più lavoro in una risposta, renderò la tua la risposta accettata.
Christoph

L'hai mai capito? Mi sto divertendo un mondo a integrare KO e JQM e non ci sono buone guide su come farlo (o un jsFiddle che dimostri demo end-to-end).
kamranicus

1
No, sono passato al framework AngularJS. Ho scoperto che è il superiour di KO. E c'è un progetto di adattatori abbastanza buono per rendere AngularJS / jqm i migliori amici per sempre: github.com/tigbro/jquery-mobile-angular-adapter Tuttavia, per quello che ho fatto fino ad ora mi è sembrato eccessivo usare quell'adattatore. Dopotutto è abbastanza facile usare solo html / css di jqm e trasformare i controlli in una direttiva angolare: jsfiddle.net/zy7Rg/7
Christoph

Puoi creare una struttura che ho definito qui . Sono sicuro che in questo modo avrai il controllo completo sull'applicazione.
Muhammad Raheel
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.