Как создать веб-приложение с помощью jquery-mobile и KnockoutJS

Я хотел бы создать мобильное приложение, сваренное только из 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

Бери, пока горячо!


person Christoph    schedule 22.05.2011    source источник
comment
Я не уверен, что есть достаточно конкретный вопрос для людей. Мне нравятся детали, которые у вас есть здесь, но, похоже, они поддаются обсуждению. Короче говоря: Хороший блог ;)   -  person Bernhard Hofmann    schedule 23.05.2011
comment
Я рад, что тебе понравилось. Я немного беспокоился, что написал так много, что люди боятся писать короткий ответ. Однако любое обсуждение приветствуется. И если stackoverflow не подходит для начала обсуждения, мы можем переключиться на группы Google: groups.google.com/forum/#!topic/knockoutjs/80_FuHmCm1s   -  person Christoph    schedule 24.05.2011
comment
Привет, Кристоф, как этот подход сработал для вас?   -  person hkon    schedule 21.12.2011
comment
На самом деле, я перешел на более крутой фреймворк AngularJS ;-)   -  person Christoph    schedule 24.12.2011
comment
Может быть, будет лучше, если вы оставите только первые пару абзацев в качестве вопроса, а остальную часть переместите в самостоятельный ответ.   -  person rjmunro    schedule 05.09.2012
comment
Ну да. Хотя он довольно старый. Не ожидал, что людям так понравится   -  person Christoph    schedule 05.09.2012
comment
Вот подход, который я использовал для структурирования приложения JQM + Knockout marcinbudny .blogspot.com/2012/11/   -  person Marcin Budny    schedule 08.11.2012


Ответы (1)


Примечание. Начиная с jQuery 1.7 метод .live() устарел. Используйте .on() для присоединения обработчиков событий. Пользователи более старых версий jQuery должны использовать .delegate() вместо .live().

Я работаю над тем же (knockout + jquery mobile). Я пытаюсь написать сообщение в блоге о том, что я узнал, но пока вот несколько советов. Помните, что я также пытаюсь изучить нокаут/jquery mobile.

Модель представления и страница

Используйте только один (1) объект модели представления на странице jQuery Mobile. В противном случае вы можете получить проблемы с событиями кликов, которые запускаются несколько раз.

View-Model и нажмите

Используйте поля ko.observable только для событий щелчка модели представления.

ko.applyBinding один раз

Если возможно: вызывайте ko.applyBinding только один раз для каждой страницы и используйте ko.observable вместо многократного вызова ko.applyBinding.

pagehide и ko.cleanNode

Не забудьте очистить некоторые модели просмотра на странице. ko.cleanNode, кажется, мешает рендерингу jQuery Mobiles, заставляя его повторно отображать html. Если вы используете ko.cleanNode на странице, вам нужно удалить роли данных и вставить обработанный HTML-код jQuery Mobile в исходный код.

$('#field').live('pagehide', function() {
    ko.cleanNode($('#field')[0]);
});

скрыть страницу и нажать

Если вы привязываетесь к click-событиям — не забудьте подчистить .ui-btn-active. Самый простой способ сделать это — использовать этот фрагмент кода:

$('[data-role="page"]').live('pagehide', function() {
    $('.ui-btn-active').removeClass('ui-btn-active');
});
person finnsson    schedule 03.06.2011
comment
Поскольку мой вопрос был очень неконкретным, и именно вы приложили больше всего усилий для ответа, я сделаю ваш ответ принятым. - person Christoph; 20.06.2011
comment
Вы когда-нибудь догадывались об этом? У меня чертовски много времени на интеграцию KO и JQM, и нет хороших руководств о том, как это сделать (или jsFiddle, демонстрирующий сквозную демонстрацию). - person kamranicus; 04.05.2012
comment
Нет, я перешел на фреймворк AngularJS. Я обнаружил, что это лучше нокаута. И есть неплохой проект адаптера, чтобы сделать AngularJS/jqm лучшими друзьями навсегда: github. com/tigbro/jquery-mobile-angular-adapter Однако для того, что я делал до сих пор, использование этого адаптера казалось излишним. В конце концов, довольно просто использовать html/css jqm и превратить элементы управления в директиву Angular: jsfiddle. сеть/zy7Rg/7 - person Christoph; 22.05.2012
comment
Вы можете создать структуру, которую я определил здесь. Я уверен, что таким образом вы будете иметь полный контроль над приложением. - person Muhammad Raheel; 11.04.2014