Что значит имя 6: BreezeJS и DurandalJS как основные инструменты для создания Single Page Application

ru-RU | создано: 03.07.2014 | опубликовано: 03.07.2014 | обновлено: 29.12.2017 | просмотров за всё время: 3990

В этой статье продолжим работу над приложением "Что значит имя", которое построено по принципу Single Page Application. Добавим новые модули, маршруты, создадим запросы на сервер с предикатами при помощи BreezeJS

Сразу к делу

В прошлый раз мы остановились на том, что на главной странице сайта вывели список букв.

149-10

Разметка для отображения этого представления (view) выглядит так:

<div class="container">
    <h1 data-bind="text: title"></h1>
    <div class="row">
        <div data-bind="ifnot: letters().length">
            <p>Подождите пожалуйста, загружаем алфавит...</p>
        </div>
        <div data-bind="if: letters().length">
            <p>Выберите букву, с которой начинается имя, чтобы узнать толкование:</p>
            <div data-bind="foreach: letters">
                <div class="letter"> <a data-bind="text: $data, attr:{href:'#letter/'+$data}"></a></div>
            </div>
        </div>
    </div>
</div>

Срока 10, обратите внимание, содержит привязку href (data-binfing) к значению “’#letter/’+$data”. Это значит что путь в режиме выполнения при наведении на конкретную букву, выглядит  так:

149-20

Для того чтобы отобразить все имена на выбранную букву, надо создать страницу (модуль+разметка). В нашем случае, этот модуль называется “letters”. Но перед тем как создавать сам модуль, я хочу добавить в список маршрутов новый путь.

Новый маршрут для DurandalJS

В нашем списке маршрутов сейчас определены два маршрута:

var routes = [
    { route: ['', 'home/index'], moduleId: 'site/home', title: 'Выбор буквы', nav: true },
    { route: 'about', moduleId: 'site/about', title: 'О проекте', nav: true },
];

Причем для первого определена коллекция маршрутов. Новый маршрут использует параметр, который передается в модуль, как контекст для функции activate. Маршруты я определю следующим образом:

var routes = [
    { route: ['', 'home/index'], moduleId: 'site/home', title: 'Выбор буквы', nav: true },
    { route: 'about', moduleId: 'site/about', title: 'О проекте', nav: true },
    { route: 'letter/:letter', moduleId: 'site/letter', title: 'Имена по букве', nav: false }
];

Параметр для маршрута задается как:

:letter

И раз уж заговорили про маршруты, давайте разберем возможные варианты задания маршрутов:

route: ''
route: 'tickets'
route: 'tickets/:id'
route: 'users(/:id)'
route: 'settings*details'

Строка 1 задает маршрут по умолчанию.
Строка 2 задает статичный маршрут на модуль.
Строка 3 задает параметризированный маршрут, здесь параметром является id.
Строка 4 задает параметризированный маршрут, но уже с необязательным параметром, в отличии от строки 3.
Строка 5 задает, так называемый, “splat” маршрут. Данный вариант используется в основном для ChildRouter, то есть для дочерних (вложенных) маршрутов.

Подробную информацию с примерами можно почитать на сайте производителя DurandalJS. Итак, маршрут задан, давайте создадим новый модуль для DurandalJS.

Модуль для DurandalJS

Создаем два файла letter.js и letter.html в папках viewmodels и views, соответственно. Для начала ViewModel:

define(['services/logger', 'knockout', 'services/dataServices', 'services/busyIndicator'],
    function (logger, ko, dataService, indicator) {
        var
            currentLetter = ko.observable(),
            type = ko.observable(''),
            namesByLetter = ko.observableArray(),
            selectType = function () {
                var current = type(), newType = this[0];
                if (current !== newType) {
                    type(this[0]);
                    loadData();
                }
            },
            success = function (data) {
                if (data && data.results) {
                    namesByLetter(data.results);
                    indicator.isbusy(false);
                }
            },
            loadData = function () {
                indicator.isbusy(true);
                dataService.getNamesByFirstLetter({ letter: currentLetter(), type: type() }, success);
            },
            activate = function (letter) {
                currentLetter(letter);
                loadData();
            };

        return {
            type: type,
            activate: activate,
            selectType: selectType,
            currentLetter: currentLetter,
            namesByLetter: namesByLetter
        };
    });

Разложим код по “полочкам”. В первой строке определяем модуль, вливаем в него зависимые модули: logger,knockout, dataServices, busyIndicator. В строке 4 определяем переменную, которая будет хранить выбранную букву. В строке 5 определяем переменную, которая будет отвечать за выбранный пользователем фильтр для отображения имен: мужские имена, женские имена, мужские и женские имена. Строка 6 – observableArray, который будет хранить список имен полученных с сервера по установленному фильтру (type и letter). Сюда в переменную namesByLetter будут приходить данные с сервера. В строке номер 7 определяется метод, который устанавливает свойство type, то есть меняет параметр фильтра по половому признаку по клику пользователя на html-контроле:

<div class="btn-group" data-toggle="buttons">
    <label title="Мужские и женские"
            class="btn btn-default"
            data-bind="css:{active: type()==''}, click:selectType.bind('')">
        <input type="radio" name="options">
        <i class="fa fa-male fa-3x"></i>
        <i class="fa fa-female fa-3x"></i>
    </label>
    <label title="Только мужские"
            class="btn btn-default"
            data-bind="css:{active: type()=='М'}, click:selectType.bind('М')">
        <input type="radio" name="options">
        <i class="fa fa-male fa-3x"></i>
    </label>
    <label title="Только женские"
            class="btn btn-default"
            data-bind="css:{active: type()=='Ж'}, click:selectType.bind('Ж')">
        <input type="radio" name="options">
        <i class="fa fa-female fa-3x"></i>
    </label>
</div>

Так я осуществил привязку (строки 4, 11, 17). Таким образом, при клике на иконку с символом пола, происходит вызов команды selectType. Контекстом для вызова передается признак пола или пустой параметр, который соответствующим образом обрабатывается в модуле. Строка 11 предыдущего листинга (вернемся к его разбору по “полочкам”) выполнятся команда loadData, собственно говоря этот метод и выполняет запрос на сервер собирая “в кучу” все параметры. Строка 14 определяет метод, который выполняется в случае удачного (безошибочного) выполнения запрос. Это callback на вызов метода getNamesByFirstLetter. Нам этот метод еще предстоит создать. Далее по листинга метод loadData, мы его уже оговорили. После него по листингу идет метод activate. Этот метод заложен в последовательность событий и методов (pipeline) системы DurandalJS. Опишу доступные для разработчика события каждого модуля:

canDeactivate – перед тем как активировать новую страница (модуль, компонент), активатор проверяет наличие в модуле этого метода. Если метода найден запускает его и проверяет результат. Если результат false, активатор прерывает процесс переключения.

canActivate – убедившись, что canDeactivate возвращает true (или что canDeactivate отсутствует), активатор системы DurandalJS проверяет наличие этого метода. Если метод найден и результат его false, активатор останавливает процесс.

deactivate – если предыдущие два условия выполнились, активатор системы запускает этот метод, опять же, если он присутствует в модуле.

activate – убедившись, что все предыдущие условия были выполнения, все разрешающие функции выполнены с результом true, активатор инициализирует новый модуль и запускает этот метод у запускаемого модуля.

Более подробно с работой активатора можно ознакомиться на официальном сайте. А мы продолжим разбор по “полочкам” предыдущего листинга. Обратите внимание на параметр, который используется в методе activate. Этот как раз тот самый параметр, описанный в маршруте. Безусловно, вы в праве его назвать как вам заблагорассудится, суть от этого не поменяется.

Я уже привел некоторую часть разметки letter.html, приведу и еще один интересный кусочек:

<div class="row">
    <div data-bind="foreach: namesByLetter">
        <div class="col-lg-3 col-sm-12 col-md-3 name">
            <a data-bind="attr:{href:'#info/'+ Name()}">
                <h1 data-bind="text: Name"></h1>
            </a>
        </div>
    </div>
</div>

Во второй строке осуществляется привязка к свойству namesByLetter. В этом свойстве хранятся полученный результат запрос с фильтром по букве (мы уже говорили об этом). В строке 4, которая отмечена задается новый маршрут: info. И этот маршрут тоже с параметром. Перед тем как его определить, давайте в модуле dataSources создадим метод, который мы уже описали, но еще не создали его – getNamesByFirstLetter.

Новый метод dataServices

Даже по названию метода можно понять, что для его выполнения требуется буква. То есть пользователь кликая на букву, должен получить список имен, которые начинаются на букву, которую он выбрал. Перед тем как создать сам метод, я добавлю пару параметров список переменных:

var manager = new breeze.EntityManager('breeze/data'),
    filterOperator = breeze.FilterQueryOp,
    Predicate = breeze.Predicate,

Строка 2 и 3 определяют новые переменные. Скорее всего, из названия вы догадались, для чего они нужны. filterOperator – это класс-перечисление фреймворка BreezeJS, который отвечает за логическую операция фильтрования. В нем перечислены варианты фильтрации: or. and, startWith, contains и другие. Вторая созданная переменная – Predicate. Этот класс системы BreezeJS служит для построения запросов и в частность для определения сущности предикатов. Посмотрите, какой я построил запрос к серверу при помощи этих классов:

getNamesByFirstLetter = function (options, success) {
    if (!options) {
        throw new Error('No parameters found for request by Letter name');
    }
    var byLetter = new Predicate('Name', filterOperator.StartsWith, options.letter);
    if (options.type) {
        byLetter = byLetter.and(new Predicate('Gender', filterOperator.Equals, options.type));
    }
    var query = breeze.EntityQuery.from('Names').where(byLetter);
    manager.executeQuery(query)
        .then(success).fail(queryFailed);
},
queryFailed = function (e) {
    logger.log(e.message, null, 'dataService', true);
};

Помните как вызывался метод getNamesByFirstName? Я как options отправлял такой объект:

{ letter: currentLetter(), type: type() }

Так вот, этот объект используется в 5,6,7 строках, для определения значений фильтра для предикатов. В строке 5 создается предикат, используется тот самый класс Predicat. Первый параметр – название свойства у сущности, по которой будет производиться запрос. Второй параметр – оператор фильтрации, и третий – значение фильтра. У меня создаются два предиката (фильтра) в строке 5 и 7. Но в строке 7 не только создается второй предикат, но еще и первый “присоединяется”, и не просто, а с оператором “and”:

byLetter.and(new Predicate('Gender', filterOperator.Equals, options.type));

После того, как сформированы фильтры, в строке 9 создается запрос (query экземпляр класса breeze.EntityQuery). Сформированных запрос оправляется в метод executeQuery, который являет собой promise. Результат работы success-callback, я передаю при вызове этого метода в модуле letters.js. На случай ошибки при выполнения запроса я определил метод queryFailed.

Запустим приложение и выберем, например, букву “У”:

149-30

Попробуем выбрать только женские имена:

149-40

Всё работает, как и было задумано.

Заключение

На этот раз хватит, продолжим работу на программой в следующей статье. В следующий раз будем использовать гаджеты (gadgets), которые предоставлены во фреймворке BreezeJS. Например, будем выводить модальное окно с информацией по значению для конкретной буквы имени. Ссылка на проект Github.