Я хотів би створити мобільний додаток, створений ні з чого, окрім 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, використовуючи синтаксис літералу об'єкта, я використовую традиційний синтаксис функції з допоміжними об'єктами "self". В основному, це справа смаку. Але коли ви хочете, щоб одна спостережувана властивість посилалася на іншу, ви не можете записати об’єктний літерал за один раз, що робить його менш симетричним. Це одна з причин, чому я вибираю інший синтаксис.
Наступною причиною є послуга, яку я можу передати як параметр, як я вже згадував раніше.
У цій моделі подання є ще одна річ, і я не впевнений, що вибрав правильний шлях. Я хочу періодично опитувати службу ajax, щоб отримати результати з сервера. Отже, я вирішив застосувати для цього методи startServicePolling / stopServicePolling . Ідея полягає в тому, щоб розпочати опитування на pagehow і зупинити його, коли користувач переходить на іншу сторінку.
Ви можете ігнорувати синтаксис, який використовується для опитування служби. Це магія 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
Отримайте, поки гаряче!