Я хотел бы создать мобильное приложение, сваренное только из html/css и JavaScript. Хотя у меня есть приличные знания о том, как создать веб-приложение с помощью JavaScript, я подумал, что мог бы взглянуть на фреймворк, такой как jquery-mobile.
Сначала я думал, что jquery-mobile — это не что иное, как фреймворк для виджетов, предназначенный для мобильных браузеров. Очень похоже на jquery-ui, но для мобильного мира. Но я заметил, что jquery-mobile — это нечто большее. Он поставляется с кучей архитектуры и позволяет создавать приложения с декларативным синтаксисом html. Таким образом, для самого легко мыслимого приложения вам не нужно было бы писать ни единой строчки JavaScript самостоятельно (что здорово, ведь всем нам нравится меньше работать, не так ли?)
Чтобы поддержать подход к созданию приложений с использованием декларативного синтаксиса html, я думаю, что было бы неплохо объединить jquery-mobile сknoutjs. Knockoutjs — это инфраструктура MVVM на стороне клиента, цель которой — привнести супервозможности MVVM, известные из WPF/Silverlight, в мир JavaScript.
Для меня MVVM — это новый мир. Хотя я уже много читал об этом, я никогда не использовал его раньше.
Итак, эта публикация посвящена тому, как спроектировать приложение, используя jquery-mobile и KnockoutJS вместе. Моя идея состояла в том, чтобы записать подход, который я придумал после нескольких часов просмотра, и попросить какого-нибудь jquery-mobile/knockout yoda прокомментировать его, показав мне, почему это отстой и почему я не должен заниматься программированием в первую очередь. место ;-)
HTML
jquery-mobile хорошо справляется с созданием модели базовой структуры страниц. Хотя я прекрасно понимаю, что впоследствии мои страницы могут загружаться через ajax, я просто решил сохранить их все в одном файле index.html. В этом базовом сценарии мы говорим о двух страницах, так что не должно быть слишком сложно оставаться в курсе событий.
<!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
Итак, давайте перейдем к самой интересной части — JavaScript!
Когда я начал думать о многоуровневом приложении, я имел в виду несколько вещей (например, тестируемость, слабая связанность). Я собираюсь показать вам, как я решил разделить свои файлы и прокомментировать такие вещи, как, почему я выбрал одно, а не другое, пока я иду...
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 — это точка входа моего приложения. Он создает объект App и предоставляет пространство имен для моделей представлений (скоро будет). Он прослушивает событие mobileinit, которое предоставляет jquery-mobile.
Как видите, я создаю экземпляр какой-то службы ajax (которую мы рассмотрим позже) и сохраняю ее в переменной «service».
Я также подключаю событие pagecreate для домашней страницы, в котором я создаю экземпляр модели представления, который получает переданный экземпляр службы. Этот момент важен для меня. Если кто-то считает, что это нужно делать по-другому, поделитесь своими мыслями!
Дело в том, что модель представления должна работать с сервисом (GetTour/, SaveTour и т. д.). Но я не хочу, чтобы ViewModel знала об этом больше. Так, например, в нашем случае я просто передаю имитацию службы ajax, потому что серверная часть еще не разработана.
Еще одна вещь, которую я должен упомянуть, это то, что ViewModel не имеет никаких сведений о фактическом представлении. Вот почему я вызываю ko.applyBindings(viewModel, this) из обработчика pagecreate. Я хотел, чтобы модель представления была отделена от фактического представления, чтобы упростить ее тестирование.
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)
В то время как вы найдете большинство примеров модели представления Knockoutjs, использующих синтаксис литерала объекта, я использую традиционный синтаксис функции с вспомогательными объектами 'self'. В принципе, это дело вкуса. Но когда вы хотите, чтобы одно наблюдаемое свойство ссылалось на другое, вы не можете записать литерал объекта за один раз, что делает его менее симметричным. Это одна из причин, по которой я выбираю другой синтаксис.
Следующая причина — сервис, который я могу передать в качестве параметра, как я упоминал ранее.
Есть еще одна вещь с этой моделью представления, которую я не уверен, правильно ли я выбрал. Я хочу периодически опрашивать службу ajax, чтобы получать результаты с сервера. Итак, для этого я решил реализовать методы startServicePolling/stopServicePolling. Идея состоит в том, чтобы начать опрос на странице и остановить его, когда пользователь переходит на другую страницу.
Вы можете игнорировать синтаксис, который используется для опроса службы. Это магия RxJS. Просто убедитесь, что я опрашиваю его и обновляю наблюдаемые свойства возвращаемым результатом, как вы можете видеть в части Subscribe(function(statistics){..}).
App.MockedStatisticsService.js
Ладно, осталось показать тебе одну вещь. Это фактическая реализация сервиса. Я не буду вдаваться в подробности здесь. Это просто макет, который возвращает некоторые числа при вызове getStatistics. Существует еще один метод mockStatistics, который я использую для установки новых значений через консоль js браузера во время работы приложения.
(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)
Хорошо, я написал намного больше, чем планировал изначально. У меня болит палец, мои собаки просят меня погулять с ними, и я чувствую себя измотанным. Я уверен, что здесь многого не хватает, и я добавил кучу опечаток и грамматических ошибок. Напишите мне, если что-то не понятно, и я обновлю сообщение позже.
Сообщение может показаться не вопросом, но на самом деле это так! Я хотел бы, чтобы вы поделились своими мыслями о моем подходе и считаете ли вы его хорошим или плохим, или я что-то упускаю.
ОБНОВЛЕНИЕ
Из-за большой популярности этого поста и того, что несколько человек попросили меня сделать это, я разместил код этого примера на github:
https://github.com/cburgdorf/stackoverflow-knockout-example
Бери, пока горячо!