Jak zaprojektować architekturę aplikacji internetowej przy użyciu jquery-mobile i knockoutjs

88

Chciałbym zbudować aplikację mobilną, uwarzoną tylko z html / css i JavaScript. Chociaż mam przyzwoitą wiedzę na temat tworzenia aplikacji internetowych za pomocą JavaScript, pomyślałem, że mogę zajrzeć do frameworka takiego jak jquery-mobile.

Na początku myślałem, że jquery-mobile to nic innego jak framework widgetów, który jest przeznaczony dla przeglądarek mobilnych. Bardzo podobny do jquery-ui, ale dla świata mobilnego. Ale zauważyłem, że jquery-mobile to coś więcej. Ma sporo architektury i pozwala tworzyć aplikacje z deklaratywną składnią html. Więc w przypadku najłatwiejszej do pomyślenia aplikacji nie musiałbyś samodzielnie pisać ani jednej linii JavaScript (co jest fajne, ponieważ wszyscy lubimy pracować mniej, prawda?)

Aby wesprzeć podejście do tworzenia aplikacji przy użyciu deklaratywnej składni html, myślę, że dobrze jest połączyć jquery-mobile z knockoutjs. Knockoutjs jest frameworkiem MVVM po stronie klienta, którego celem jest przeniesienie supermocarstw MVVM znanych z WPF / Silverlight do świata JavaScript.

Dla mnie MVVM to nowy świat. Chociaż dużo o tym czytałem, nigdy wcześniej sam z niego nie korzystałem.

Więc ten post dotyczy architektury aplikacji przy użyciu jquery-mobile i knockoutjs. Mój pomysł polegał na zapisaniu podejścia, które wymyśliłem po kilku godzinach patrzenia na to, i poproszeniu o komentarz jquery-mobile / knockout yoda, pokazujący mi, dlaczego jest do bani i dlaczego nie powinienem programować w pierwszej miejsce ;-)

Plik html

jquery-mobile wykonuje dobrą robotę, udostępniając podstawowy model struktury stron. Chociaż doskonale zdaję sobie sprawę, że moje strony mogą być później ładowane przez ajax, zdecydowałem się po prostu przechowywać je wszystkie w jednym pliku index.html. W tym podstawowym scenariuszu mówimy o dwóch stronach, więc nie powinno być zbyt trudno być na bieżąco.

<!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>

JavaScript

Przejdźmy więc do części zabawnej - JavaScript!

Kiedy zacząłem myśleć o warstwowaniu aplikacji, miałem na myśli kilka rzeczy (np. Testowalność, luźne połączenie). Pokażę ci, jak zdecydowałem się podzielić moje pliki i skomentować takie rzeczy, jak dlaczego wybrałem jedną rzecz nad drugą, kiedy będę ...

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 jest punktem wejścia do mojej aplikacji. Tworzy obiekt App i udostępnia przestrzeń nazw dla modeli widoku (wkrótce). To listenes dla mobileinit imprezy, która zapewnia jquery-mobile.

Jak widać, tworzę wystąpienie jakiejś usługi Ajax (której przyjrzymy się później) i zapisuję ją w zmiennej „service”.

Ja też zahaczyć o pagecreate zdarzenie na stronie głównej, w której tworzę instancję ViewModel, który dostaje wystąpienie usług przekazany w. Ten punkt ma zasadnicze znaczenie dla mnie. Jeśli ktoś myśli, że należy to zrobić inaczej, podziel się swoimi przemyśleniami!

Chodzi o to, że model widoku musi działać na usłudze (GetTour /, SaveTour itp.). Ale nie chcę, aby ViewModel wiedział o tym więcej. Na przykład w naszym przypadku po prostu przekazuję fałszywą usługę Ajax, ponieważ backend nie został jeszcze opracowany.

Kolejną rzeczą, o której powinienem wspomnieć, jest to, że ViewModel ma zerową wiedzę o rzeczywistym widoku. Dlatego dzwonię do ko.applyBindings (viewModel, this) z programu obsługi pagecreate . Chciałem, aby model widoku był oddzielony od widoku rzeczywistego, aby ułatwić jego testowanie.

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)

Chociaż większość przykładów modeli widoku knockoutjs znajdziesz przy użyciu składni literału obiektowego, ja używam tradycyjnej składni funkcji z obiektami pomocniczymi „siebie”. Zasadniczo to kwestia gustu. Ale jeśli chcesz, aby jedna obserwowalna właściwość odnosiła się do innej, nie możesz zapisać literału obiektu za jednym razem, co czyni go mniej symetrycznym. To jeden z powodów, dla których wybieram inną składnię.

Następnym powodem jest usługa, którą mogę przekazać jako parametr, jak wspomniałem wcześniej.

Z tym modelem widoku jest jeszcze jedna rzecz, której nie jestem pewien, czy wybrałem właściwy sposób. Chcę okresowo sondować usługę Ajax, aby pobrać wyniki z serwera. Dlatego zdecydowałem się zaimplementować metody startServicePolling / stopServicePolling , aby to zrobić. Chodzi o to, aby rozpocząć sondowanie na pokazie stron i zatrzymać je, gdy użytkownik przejdzie do innej strony.

Możesz zignorować składnię używaną do odpytywania usługi. To magia RxJS. Tylko upewnij się, że sonduję go i aktualizuję obserwowalne właściwości zwracanym wynikiem, jak widać w części Subskrybuj (funkcja (statystyki) {..}) .

App.MockedStatisticsService.js

Ok, została tylko jedna rzecz do pokazania. To rzeczywista implementacja usługi. Nie wchodzę tutaj w szczegóły. To tylko makieta, która zwraca pewne liczby, gdy wywoływana jest funkcja getStatistics . Istnieje inna metoda mockStatistics, której używam do ustawiania nowych wartości za pomocą konsoli js przeglądarki, gdy aplikacja jest uruchomiona.

(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, napisałem znacznie więcej, ponieważ początkowo planowałem napisać. Boli mnie palec, psy proszą mnie o wyprowadzenie na spacer i czuję się wyczerpana. Jestem pewien, że brakuje tu wielu rzeczy i że wstawiłem kilka literówek i błędów gramatycznych. Krzycz na mnie, jeśli coś nie jest jasne, a później zaktualizuję wpis.

Publikacja może nie wydawać się pytaniem, ale tak jest! Chciałbym, żebyś podzielił się swoimi przemyśleniami na temat mojego podejścia i czy uważasz, że jest dobre lub złe lub jeśli coś mi brakuje.

AKTUALIZACJA

Ze względu na dużą popularność tego posta i ponieważ kilka osób prosiło mnie o to, umieściłem kod z tego przykładu na github:

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

Weź to, póki jest gorąco!

Christoph
źródło
7
Nie jestem pewien, czy istnieje wystarczająco szczegółowe pytanie, na które można odpowiedzieć. Podoba mi się ten szczegół, który tu masz, ale wydaje się, że nadaje się on do dyskusji. Krótko mówiąc: „Ładny blog”;)
Bernhard Hofmann
Cieszę się, że to lubisz. Martwiłem się trochę, że napisałem tak dużo, że ludzie boją się napisać krótką odpowiedź. Jednak każda dyskusja jest mile widziana. A jeśli stackoverflow to niewłaściwe miejsce do rozpoczęcia dyskusji, możemy przełączyć się na grupy google: groups.google.com/forum/#!topic/knockoutjs/80_FuHmCm1s
Christoph
Cześć Christoph, jak Ci się to udało?
hkon
Właściwie przeniosłem się na bardziej niesamowity framework AngularJS ;-)
Christoph
1
Może być lepiej, jeśli zostawisz tylko kilka pierwszych akapitów jako pytanie, a resztę przeniosłeś do samodzielnej odpowiedzi.
rjmunro,

Odpowiedzi:

30

Uwaga: od wersji jQuery 1.7 .live()metoda jest przestarzała. Służy .on()do dołączania programów obsługi zdarzeń. Użytkownicy starszych wersji jQuery powinni używać .delegate()zamiast .live().

Pracuję nad tym samym (nokaut + jQuery mobile). Próbuję napisać wpis na blogu o tym, czego się nauczyłem, ale w międzyczasie oto kilka wskazówek. Pamiętaj, że próbuję też nauczyć się nokaut / jQuery mobile.

Widok modelu i strony

Używaj tylko jednego (1) obiektu modelu widoku na stronę jQuery Mobile. W przeciwnym razie możesz mieć problemy ze zdarzeniami kliknięcia, które są uruchamiane wielokrotnie.

Wyświetl model i kliknij

Używaj tylko pól ko.observable-fields dla zdarzeń kliknięcia modeli widoku.

ko.applyBinding raz

Jeśli to możliwe: wywołaj ko.applyBinding tylko raz dla każdej strony i użyj ko.observable zamiast wielokrotnie wywoływać ko.applyBinding.

pagehide i ko.cleanNode

Pamiętaj, aby wyczyścić niektóre modele widoku na ukrywaniu strony. ko.cleanNode wydaje się zakłócać renderowanie jQuery Mobiles - powodując ponowne renderowanie html. Jeśli używasz ko.cleanNode na stronie, musisz usunąć role data i wstawić wyrenderowany html jQuery Mobile do kodu źródłowego.

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

ukryj stronę i kliknij

Jeśli wiążesz się ze zdarzeniami kliknięcia - pamiętaj, aby wyczyścić .ui-btn-active. Najłatwiejszym sposobem osiągnięcia tego jest użycie tego fragmentu kodu:

$('[data-role="page"]').live('pagehide', function() {
    $('.ui-btn-active').removeClass('ui-btn-active');
});
finnsson
źródło
Ponieważ moje pytanie było bardzo nieokreślone, a ty włożyłeś najwięcej pracy w odpowiedź, dam ci odpowiedź zaakceptowaną.
Christoph
Czy kiedykolwiek to rozgryzłeś? Mam cholernie dużo czasu, integrując KO i JQM i nie ma dobrych przewodników, jak to zrobić (lub jsFiddle demonstrującego od końca do końca demo).
kamranicus
1
Nie, przeniosłem się do frameworka AngularJS. Uważam, że jest to superiour dla KO. Jest też całkiem niezły projekt adaptera, dzięki któremu AngularJS / jqm stał się najlepszymi przyjaciółmi na zawsze: github.com/tigbro/jquery-mobile-angular-adapter Jednak z powodu tego, co zrobiłem do tej pory, używanie tego adaptera wydawało się przesadą . W końcu całkiem łatwo jest po prostu użyć html / css jqm i zmienić sterowanie w dyrektywę Angular: jsfiddle.net/zy7Rg/7
Christoph
Możesz stworzyć strukturę, którą zdefiniowałem tutaj . Jestem pewien, że w ten sposób będziesz mieć pełną kontrolę nad aplikacją.
Muhammad Raheel