Что значит имя 5: Разработка клиента на JavaScript при помощи BreezeJS

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

В статье будет показано как можно создать связку Клиент (BreezeJS) + Сервер (BreezeController)

О проделанной работе

В прошлых статьях было создано приложение, которое на данный момент не имеет, практически никакого функционала, кроме как две страницы (два модуля), которые переключаются между собой посредствам DurandalJS фреймворка, созданного для разработки Single Page Application приложений. SPA – это как вы уже знаете приложение построенное специальным образом. Одним из постулатов такого подхода является AMD, то есть модульная система. Давайте создадим новый модуль, который будет отвечать за уведомления пользователя (да и программиста тоже). А назовем этот модуль – Logger.

Logger – первый модуль для DurandalJS

Для начала создам файл logger.js в новой папке App/Services.

148-10

Теперь добавим минимум, из которого должен состоять модуль AMD и в частности DurandalJS:

define( function () {
    
});

У меня уже установлен nuget-пакет toastr.

148-20

Видимо, он установился с одним из пакетом как зависимый. Если у вас не установлен этот пакет – установите его, выполнив команду в Package Manager Console:

PM> Install-Package toastr

Я немного поколдовал на модулем Logger, и вот что у меня получилось:

define(['durandal/system'], function (system) {
    var
        info = function (message) {
            logIt(message, null, null, true, 'info');
        },
        log = function (message, data, source, showToast) {
            logIt(message, data, source, showToast, 'info');
        },
        logSuccess = function (message, source) {
            toastr.success(message);
            system.log(source, message, data);
        },
        logError = function (message, data, source, showToast) {
            logIt(message, data, source, showToast, 'error');
        },
        logIt = function (message, data, source, showToast, toastType) {
            source = source ? '[' + source + '] ' : '';
            if (data) {
                system.log(source, message, data);
            } else {
                system.log(source, message);
            }
            if (showToast) {
                if (toastType === 'error') {
                    toastr.error(message);
                } else {
                    toastr.info(message);
                }
            }
        };

    return {
        log: log,
        info: info,
        logError: logError,
        logSuccess: logSuccess
    };
});

Я сделал модуль по паттерну Revealing Module Pattern, кстати, надо сказать, что я все модули делаю по этому паттерну. В строке 16 определяется основной метод вывода сообщения при помощи toastr, в остальных методах используется основной метод. И в конце возвращается Object Literal. Обратите внимание на первую строку, в которой определяется зависимость от модуля ‘durandal/system’, который как ссылка передается в callback.

Теперь можно попробовать выдать сообщение на открытие, например, страницы “о проекте”. Для этого надо “влить” (DI) вновь созданный модуль в модуль about.js и вызвать метод уведомления в событии activate:

define(['services/logger'],function (logger) {
    var
        title = 'О проекте',
        activate = function() {
            logger.logSuccess('Загружена страница "О проекте"', 'about');
        };

    return {
        title: title,
        activate: activate
    };
});

Согласно настроек в главном модуле main.js сообщение появилось в правом нижним углу.

148-30

Итак первый модуль закончили. Будем его использовать в других модулях, чтобы упростить задачу отладки. Далее создадим модуль utils.js, который, как можно судить из названия, будет содержать утилиты.

Utils или утилиты

В папке App/services я создал файл utils.js с таким содержанием:

define(function () {

    function guid() {
        return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
    };
    
    return {
        guid: guid
    }
});

Этот модуль является прекрасным местом для хранения общих методов, констант и данных подобного рода. Еще одним модулем станет конфигурация системы.

Модуль congif или все настройки в одном месте

Настройки для всего приложения хранить в одном месте, по крайней мере очень удобно. Я специально для настроек проекта выделю целый модуль.

Пытливый читатель может возразить, зачем столько файлов и модулей для такого простого приложения? Предвидя вопрос, отвечаю на него: на самом деле цель статьи показать как работать фреймворком DurandalJS, как строятся модули и как они между собой взаимодействуют. Безусловно, для такого простого приложения такое количество модулей чрезмерно. Но что если этот проект использовать как шаблон для более бОльших проектов? В любом случае, этот проект всего лишь для изучения темы Single Page Application на примере BreezeJS.

Новый модуль я “положил” рядом с главным файлом приложения main.js

148-40

В моем приложении настроек не много, а выглядит модуль так:

define(function () {
    var site = {};
    site.cfg = site.cfg || {};
    site.cfg.busyIndicatorImageName = 'images/ms-loader.gif';

    return {
        site: site
    };
});

Во второй и третьей строке создается namespace, который ограничит область действия моих переменных. В строке 4 определяется название файла, который будет использоваться для отображения анимации.

BusyIndicator или пока грузятся данные

Следующим модулем будет специализированный контрол, который будет “завешивать” страницу для пользователя полупрозрачной “пеленой” и показывать анимацию загрузки (пример работы контрола можно посмотреть на сайте примере). Я уже создал в папке App/Services новый файл с названием busyIndicator.js, а теперь наполняем его содержимым:

define(['knockout', 'services/utils', 'config'], function (ko, utils, config) {
    var ctrl = this;
    ctrl.uniqueId = utils.guid();
    ctrl.isbusy = ko.observable(false);
    ctrl.imageName = config.site.cfg.busyIndicatorImageName;
    ctrl.modalCss = '<style type="text/css">' +
       '.modalBusy {' +
       'position:fixed;' +
       'z-index:8888;' +
       'margin-left:0;' +
       'top:0;' +
       'left:0;' +
       'height:100%;' +
       'width:100%;' +
       'background:rgba(200,200,200,.5)url("' + ctrl.imageName + '") 50% 50% no-repeat;}' +
       '</style>';
    ctrl.ctrlTemplate = function () {
        return '<div id="block' + ctrl.uniqueId + '" ' + 'class="modalBusy">&nbsp;</div>\
            </div></div>';
    };
    ctrl.show = function () {
        ctrl.isbusy(true);
    };
    ctrl.hide = function () {
        ctrl.isbusy(false);
    };
    ctrl.init = function () {
        if (!window.hasModelBlocker) {
            $("head").append(ctrl.modalCss);
            window.hasModelBlocker = true;
        }
        return;
    }();

    return {
        isbusy: isbusy,
        on: ctrl.show,
        off: ctrl.hide,
        template: ctrl.ctrlTemplate,
        uniqueId: ctrl.uniqueId
    };
});

У модуля определяются свойства “включения” и “выключения”, а также есть специальное свойство “template”, которое возвращает шаблон для рендеринга контрола. Обратите внимание, как подключаются (делается “иньекция” модулей) модули к контексту текущего модуля в первой строке листинга. Чтобы использовать в разметке вновь созданный busyIndicator, требуется создать bindingHandle для KnockoutJS. Я создал такой:

ko.bindingHandlers.blockUI = {
    init: function (element, valueAccessor) {
        var value = valueAccessor(),
            ctrl = ko.utils.unwrapObservable(value);
        $(element).css('position', 'relative');
        $(element).css('min-height', '70px');
        ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
            var el = $("#block" + ctrl.uniqueId)[0];
            if (el)
                ko.removeNode(el);
        });
    },
    update: function (element, valueAccessor, allBindingAccessor) {
        var value = valueAccessor(),
            ctrl = ko.utils.unwrapObservable(value);
        var el;
        if (ctrl.isbusy()) {
            if (ctrl && ctrl.template) {
                var block = ctrl.template(element);
                $(element).append(block);
            }
        } else {
            el = $("#block" + ctrl.uniqueId)[0];
            if (el)
                ko.removeNode(el);
        }
    }
};

Как раз в 18 строке используется свойство “template”, для формирования html-разметки. Теперь стоит задуматься о том, где разместить данный handler? Одним из вариантов, разместить его в отдельном модуле и инициализировать этот модуль в главном – main.js. Я же на этот раз поступлю проще, разметив его в самом модуле main.js перед запуском приложения:

require.config({
    paths: {
        'text': '../scripts/text',
        'durandal': '../scripts/durandal',
        'plugins': '../scripts/durandal/plugins',
        'transitions': '../scripts/durandal/transitions',
    }
});

define('jquery', function () { return jQuery; });
define('knockout', ko);

define(['durandal/system', 'durandal/app', 'durandal/viewLocator'],
    function (system, app, viewLocator) {

        system.debug(true);

        app.title = 'Что значит имя';

        // инициализация bindingHandlers
        ko.bindingHandlers.blockUI = {
            init: function (element, valueAccessor) {
                var value = valueAccessor(),
                    ctrl = ko.utils.unwrapObservable(value);
                $(element).css('position', 'relative');
                $(element).css('min-height', '70px');
                ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
                    var el = $("#block" + ctrl.uniqueId)[0];
                    if (el)
                        ko.removeNode(el);
                });
            },
            update: function (element, valueAccessor, allBindingAccessor) {
                var value = valueAccessor(),
                    ctrl = ko.utils.unwrapObservable(value);
                var el;
                if (ctrl.isbusy()) {
                    if (ctrl && ctrl.template) {
                        var block = ctrl.template(element);
                        $(element).append(block);
                    }
                } else {
                    el = $("#block" + ctrl.uniqueId)[0];
                    if (el)
                        ko.removeNode(el);
                }
            }
        };

        app.configurePlugins({
            router: true,
            dialog: true
        });

        window.toastr.options.positionClass = 'toast-bottom-right';
        window.toastr.options.backgroundpositionClass = 'toast-bottom-right';
        viewLocator.useConvention();

        app.start().then(function () {
            app.setRoot('viewmodels/shell', 'entrance');
        });
    });

Раз у нас появился индикатор занятости (busyInidator) на запросу к серверу, надо немного изменить разметку shell.js, чтобы подключить визуализацию запроса.

<section>
    <div data-bind="blockUI: indicator">
        <section id="content" class="container" data-bind="router: { transition: 'entrance', cacheViews: true }"></section>
        <div data-bind="compose: 'viewmodels/site/footer'"></div>
    </div>
</section>

Также надо внести изменения в сам модуль shell.js, чтобы определить объект indicator:

define(['plugins/router', 'services/busyIndicator'],
    function (router, indicator) {
        var
            activate = function () {

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

                return router.makeRelative({ moduleId: 'viewmodels' })
                    .map(routes)
                    .buildNavigationModel()
                    .activate();
            };

        return {
            indicator: indicator,
            activate: activate,
            router: router
        };
    })

Теперь при установке значений свойств isbusy у индикатора, пользователю будет показываться индикация того, что запрос выполняется. Пришло время сделать очень важный модуль – модуль доступа к данным dataService.js.

Доступ к данным или breeze.EntityManager

Перед тем как начать работу с BreezeJS следует добавить регистрацию скриптов на главной странице сайта index.cshml, необходимых для работы фреймворка:

<script src="~/Scripts/jquery-2.1.1.js"></script>
<script src="~/scripts/bootstrap.js"></script>
<script src="~/Scripts/knockout-3.1.0.js"></script>
<script src="~/Scripts/toastr.js"></script>
<script src="~/Scripts/q.min.js"></script>
<script src="~/Scripts/breeze.min.js"></script>

Я добавил две предпоследние строчки, потому мини-фреймворк Q.js (реализация promises) используется в BreezeJS.

На страницах документации к системе BreezeJS, есть упоминание о том, что доступ к данным должен быть централизованным. Или говори словами инструкции по пользованию BreezeJS, следует “шарить” EntityManager. Собственно говоря, должна быть это некая реализация паттерна Unit Of Work, который в свою очередь должен “вливаться” как инъекция в модуль.

Если учесть, что Single Page Application являет собой реализацию модульного подхода в проектировании, то нам остается сделать модуль (я назвал его dataService.js) в котором будет обработка EnityManager. Только в этом модуле будет создаваться один экземпляр менеджера сущностей.

Для начал создадим файл в папке App/services под названием dataServices.js.

define(function () {
    
});

Первое, что надо сделать, так это определить переменные и методы, которые будут использоваться в модулях. Переменная будет одна, это сам менеджер сущностей:

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

Далее добавим первый метод:

getLetters = function () {
    return breeze.EntityQuery
        .from('Lookups')
        .noTracking()
        .using(manager)
        .execute();
};

В строка 1 определен метод getLetters, который получает буквы для отображения алфавита на главной страницы, то есть буквы, с которых начинается поиск имени. Определим также object literal для этого модуля:

return {
    getLetters: getLetters,
};

Таким образом, мы уже можем отобразить на странице список букв, используя данный метод в модуле home.js:

define(
    ['knockout', 'services/logger', 'services/dataservices', 'services/busyIndicator'],
    function(ko, logger, dataService, indicator) {
        var
            title ='Что значит имя',
            letters = ko.observableArray(),
            activate = function() {
                indicator.isbusy(true);
                dataService.getLetters().then(function(data) {
                    letters(data.results[0].items);
                    indicator.isbusy(false);
                }).fail(function(data) {
                    logger.logError(data.message);
                });
            };

        return {
            activate: activate,
            title: title,
            letters: letters
        }
    });

Осталось подправить представление (view) модуля home.html и добавить несколько каскадных стилей для отображения букв:

<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>

Строка 4 привязывается к свойству length объекта letters, если буквы еще не загружены с сервера – отображается надпись “Подождите пожалуйста, загружаем алфавит”. В противном случае, отображается другой div, который в строке 9 через “foreach” перебирает буквы и рисует их в строке 10.

148-50

Заключение

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