Я хотел бы создать мобильное приложение, основанное только на 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 вместе. Моя идея заключалась в том, чтобы записать подход, который я придумал после просмотра в течение нескольких часов, и попросить некоторых 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, использующих синтаксис объектного литерала, я использую традиционный синтаксис функций с вспомогательными объектами self. В основном это дело вкуса. Но если вы хотите, чтобы одно наблюдаемое свойство ссылалось на другое, вы не можете записать литерал объекта за один раз, что делает его менее симметричным. Это одна из причин, по которой я выбираю другой синтаксис.
Следующая причина - это сервис, который я могу передать в качестве параметра, как я упоминал ранее.
Есть еще одна вещь, связанная с этой моделью представления, и я не уверен, что выбрал правильный путь. Я хочу периодически опрашивать службу ajax, чтобы получать результаты с сервера. Итак, я решил реализовать для этого методы startServicePolling / stopServicePolling . Идея состоит в том, чтобы начать опрос на pageshow и остановить его, когда пользователь перейдет на другую страницу.
Вы можете игнорировать синтаксис, который используется для опроса службы. Это магия RxJS. Просто убедитесь, что я опрашиваю его и обновляю наблюдаемые свойства с возвращенным результатом, как вы можете видеть в части « Подписка» (функция (статистика) {..}) .
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
Получите, пока жарко!