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

88

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

Получите, пока жарко!

Кристоф
источник
7
Я не уверен, что есть достаточно конкретный вопрос, на который можно было бы ответить. Мне нравятся детали, которые вы здесь изложили, но, похоже, они поддаются обсуждению. Короче: "Хороший блог";)
Бернхард Хофманн
Я рад, что тебе понравилось. Меня немного волновало, что я написал так много, что люди опасаются писать короткий ответ. Однако любое обсуждение приветствуется. И если stackoverflow - неподходящее место для начала обсуждения, мы могли бы переключиться на группы Google: groups.google.com/forum/#!topic/knockoutjs/80_FuHmCm1s
Christoph
Привет, Кристоф, как этот подход сработал для тебя?
hkon
На самом деле, я перешел на более классный фреймворк AngularJS ;-)
Кристоф
1
Это может быть лучше, если вы оставите только первую пару абзацев в качестве вопроса, а оставшуюся часть переместите в самостоятельный ответ.
rjmunro

Ответы:

30

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

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

Просмотр модели и страницы

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

View-Model и щелкните

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

ko.applyBinding один раз

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

pagehide и ko.cleanNode

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

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

скрыть страницу и щелкнуть

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

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