Я хотел бы создать мобильное приложение, созданное из ничего, кроме HTML / CSS и JavaScript. Хотя я достаточно хорошо знаю, как создать веб-приложение с помощью JavaScript, я подумал, что мог бы взглянуть на такую среду, как jquery-mobile.
Сначала я думал, что jquery-mobile - это не что иное, как среда виджетов, предназначенная для мобильных браузеров. Очень похоже на jquery-ui, но для мобильного мира. Но я заметил, что jquery-mobile - это нечто большее. Он поставляется с множеством архитектур и позволяет создавать приложения с декларативным синтаксисом HTML. Так что для самого простого приложения вам не нужно было бы писать одну строчку JavaScript самостоятельно (что здорово, потому что нам всем нравится работать меньше, не так ли?)
Чтобы поддержать подход к созданию приложений с использованием декларативного синтаксиса html, я думаю, что было бы неплохо объединить jquery-mobile с knockoutjs. Knockoutjs - это инфраструктура MVVM на стороне клиента, целью которой является привнесение в мир JavaScript сверхдержав MVVM, известных в WPF / Silverlight.
Для меня MVVM - это новый мир. Хотя я уже много об этом читал, я сам никогда раньше этим не пользовался.
Итак, эта публикация о том, как создать приложение с использованием jquery-mobile и knockoutjs. Моя идея состояла в том, чтобы записать подход, который я придумал после нескольких часов просмотра, и попросить joda-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, использующих синтаксис литерала объекта, я использую традиционный синтаксис функции с вспомогательными объектами «self». По сути, это вопрос вкуса. Но когда вы хотите иметь одно наблюдаемое свойство для ссылки на другое, вы не можете записать литерал объекта за один раз, что делает его менее симметричным. Это одна из причин, почему я выбираю другой синтаксис.
Следующая причина - это служба, которую я могу передать в качестве параметра, как я упоминал ранее.
Есть еще одна вещь с этой моделью представления, которую я не уверен, выбрал ли я правильный путь. Я хочу периодически опрашивать службу ajax для получения результатов с сервера. Итак, я решил реализовать startServicePolling / stopServicePolling методы для этого. Идея состоит в том, чтобы запустить опрос на странице и остановить его, когда пользователь переходит на другую страницу.
Вы можете игнорировать синтаксис, используемый для опроса сервиса. Это волшебство RxJS. Просто убедитесь, что я опрашиваю его и обновляю наблюдаемые свойства возвращаемым результатом, как вы можете видеть в Subscribe (function (statistics) {..}) part.
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
Получи, пока жарко!