Я хотел бы создать мобильное приложение, заваренное только из html/css и JavaScript. Хотя у меня есть приличное знание о том, как создать веб-приложение с помощью JavaScript, я подумал, что могу взглянуть на фреймворк вроде jquery-mobile.
Сначала я думал, что jquery-mobile - это не что иное, как структура виджета, ориентированная на мобильные браузеры. Очень похоже на jquery-ui, но для мобильного мира. Но я заметил, что jquery-mobile - это нечто большее. Он поставляется с кучей архитектуры и позволяет создавать приложения с декларативным синтаксисом html. Итак, для самого легкого мыслимого приложения вам не нужно будет писать отдельную строку JavaScript самостоятельно (что классно, потому что нам всем нравится работать меньше, не так ли?)
Чтобы поддерживать подход к созданию приложений с использованием декларативного синтаксиса html, я считаю, что это хорошо, чтобы объединить jquery-mobile с knockoutjs. 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 для домашней страницы, в которой я создаю экземпляр viewModel, который получает экземпляр службы, переданный. Этот момент очень важен для меня. Если кто-то думает, это нужно делать по-другому, пожалуйста, поделитесь своими мыслями!
Дело в том, что модель представления должна работать на службе (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, используя синтаксис литерала объекта, я использую традиционный синтаксис функции с помощью "собственных" вспомогательных объектов. По сути, это вопрос вкуса. Но когда вы хотите иметь одно наблюдаемое свойство для ссылки на другое, вы не можете записать литерал объекта за один раз, что делает его менее симметричным. Это одна из причин, почему я выбираю другой синтаксис.
Следующая причина - это служба, которую я могу передать как параметр, как я упоминал ранее.
В этой модели представлений есть еще одна вещь, которую я не уверен, правильно ли я выбрал правильный путь. Я хочу периодически проверять службу 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)
Хорошо, я написал гораздо больше, поскольку изначально планировал писать. Мой палец больно, мои собаки просят меня взять их на прогулку, и я чувствую себя измученным. Я уверен, что здесь есть много вещей, и я вложил кучу ошибок опечаток и грамматик. Услышьте меня, если что-то неясно, и я обновлю позже.
Публикация может показаться не вопросом, а на самом деле! Я хотел бы, чтобы вы поделились своими мыслями о моем подходе, и если вы считаете это хорошим или плохим, или если я пропущу вещи.
UPDATE
Из-за большой популярности этой публикации и потому, что несколько человек просили меня сделать это, я поставил код этого примера на github:
https://github.com/cburgdorf/stackoverflow-knockout-example
Получите его, пока он горячий!