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!